Compose屏幕适配

Compose屏幕适配

一种Compose中屏幕适配的解决方案,灵感参考头条屏幕适配AndroidAutoSize等,以设计稿宽度和屏幕水平方法大小为准,等比拉伸控件大小。

后文附有本方案的Kotlin语言实现,使用只需要两个步骤即可:

1
2
3
4
5
6
7
8
9
10
// 1 初始化
class MainApp : Application() {
override fun onCreate() {
super.onCreate()
SizeEtx.init(this, 375) // 375为设计稿宽度
}
}

// 2 使用
size(width = 9.composeDp, height = 16.composeDp)

主要的设计思想

假设如下变量:设计稿总宽度dpx,控件在设计稿中的大小n,屏幕的实际水平dp大小rdp,以及我们需要求得的控件在设备中的dp值m

那么我们不难得到以下方程:

1
n / dpx = m / rdp

也就可以推导出:

1
m = n * (rdp / dpx)

上述值中,只有屏幕水平dp值rdp还是未知的,又根据density 在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度wpx / density = 屏幕的总 dp 宽度rdp可知:

1
rdp = wpx / density

所以,我们可以推导出:

1
2
3
m = n * (rdp / dpx)
= n * ( wpx / density ) / dpx
= n * wpx / (density * dpx)

到这里,等式后面的所有数据都为已知或者在app运行时可知,由此我们可以计算出设计稿中的控件在Compose中对应的dp大小。

下面是以上思路的kotlin实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Point
import android.os.Build
import android.view.WindowManager
import androidx.compose.ui.unit.Dp

/**
* @author : jixiaoyong
* @description :Compose 屏幕适配方案
*
* 根据设计稿宽度(设计稿宽度对应设备水平方向)和设计稿对应物体大小,计算实际应该填写的dp
*
* 使用:
* 在Application onCreate()方法中执行
* SizeEtx.init(this, 375)
* 其中375位设计稿屏幕宽度,然后在代码中使用width(315.composeDp)作为大小单位即可,
* 其中315为设计稿中的控件大小
*
* 计算方式为:
* wpx 屏幕实际像素宽度
* dpx 设计稿屏幕宽度
* n 控件设计稿中的宽度(dp、px都可,与dpx单位保持一致)
* m 控件在app中对应的dp
* rpx 控件在屏幕中应该展示的像素大小
* 已知条件:dp = px / density
* (density 在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度)
*
* DisplayMetrics#density 就是上述的density
* DisplayMetrics#densityDpi 就是上述的dpi
*
* 综上得出如下结论(以竖屏情况下屏幕宽度为例):
* 屏幕宽度总dp : rdp = wpx / density
* m / rdp = n / dpx
* 那么,m = (rdp / dpx) * n
* 其中(rdp / dpx)被我们当做设计稿中控件大小与设备中控件dp大小之间的缩放系数:dpWidthScale
* 所以:m = dpWidthScale * n
*
* @email : jixiaoyong1995@gmail.com
* @date : 2021/8/2
*/
class SizeEtx private constructor(context: Context, dpx: Int) {

init {
val density = Resources.getSystem().displayMetrics.density
var wpx = Resources.getSystem().displayMetrics.widthPixels
dpWidthScale = wpx.toFloat() / (dpx * density)
dpHeightScale =
getScreenRealHeightPx(context).toFloat() / (dpx * density)
pxWidthScale = wpx.toFloat() / dpx.toFloat()
// 以下数据为Redmi Note 5 的测试数据
// "getScreenRealHeight ${getScreenRealHeightPx(context)}".logd() // 2160,实际设备高度为2160
// "Resources.getSystem().displayMetrics.heightPixels ${Resources.getSystem().displayMetrics.heightPixels}".logd() // 2033,实际设备高度为2160
// "Resources.getSystem().displayMetrics.density ${Resources.getSystem().displayMetrics.density}".logd() // 2.7
// "Resources.getSystem().displayMetrics.densityDpi ${Resources.getSystem().displayMetrics.densityDpi}".logd() // 432
}

/**
* 获得屏幕真实高度(包含底部导航栏)
*/
private fun getScreenRealHeightPx(context: Context): Int {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = windowManager.defaultDisplay
val outPoint = Point()
if (Build.VERSION.SDK_INT >= 19) {
// 可能有虚拟按键的情况
display.getRealSize(outPoint)
} else {
// 不可能有虚拟按键
display.getSize(outPoint)
}
// 手机屏幕真实高度
return outPoint.y
}

companion object {
/**
* 初始化大小适配工具类
* @param context
* @param dpx 设计稿中的屏幕宽度,例如375,在使用到本工具的所有地方,都应该以此宽度为准来获取其他控件的大小
*/
fun init(context: Context, dpx: Int) {
SizeEtx(context, dpx)
}

var dpWidthScale = 1.0f
var dpHeightScale = 1.0f
var pxWidthScale = 1.0f
var pxHeightScale = 1.0f
}
}

// Compose 屏幕适配方案
/**
* 获取Compose中对应的dp,输入值为设计稿中对应的控件大小
*/
inline val Number.composeDp: Dp
get() {
val isPortrait = isPortrait()
return Dp(this.toFloat() * if (isPortrait) SizeEtx.dpWidthScale else SizeEtx.dpHeightScale)
}

/**
* 获取Compose中对应的px,输入值为设计稿中对应的控件大小
*/
inline val Number.composePx: Int
get() {
val isPortrait = isPortrait()
return (this.toFloat() * if (isPortrait) SizeEtx.pxWidthScale else SizeEtx.pxHeightScale).toInt()
}

// 是否竖屏
fun isPortrait() =
Resources.getSystem().configuration.orientation == Configuration.ORIENTATION_PORTRAIT

参考文章

AndroidAutoSize

一种极低成本的Android屏幕适配方式