Compose 屏幕适配
原创...大约 4 分钟
Compose 屏幕适配
一种Compose中屏幕适配的解决方案,灵感参考头条屏幕适配、AndroidAutoSize等,以设计稿宽度和屏幕水平方法大小为准,等比拉伸控件大小。
后文附有本方案的 Kotlin 语言实现,使用只需要两个步骤即可:
// 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
。
那么我们不难得到以下方程:
n / dpx = m / rdp
也就可以推导出:
m = n * (rdp / dpx)
上述值中,只有屏幕水平 dp 值rdp
还是未知的,又根据**(density
在每个设备上都是固定的,DPI
/ 160 = density
,屏幕的总 px 宽度wpx
/ density
= 屏幕的总 dp 宽度rdp
)**可知:
rdp = wpx / density
所以,我们可以推导出:
m = n * (rdp / dpx)
= n * ( wpx / density ) / dpx
= n * wpx / (density * dpx)
到这里,等式后面的所有数据都为已知或者在 app 运行时可知,由此我们可以计算出设计稿中的控件在 Compose 中对应的 dp 大小。
下面是以上思路的 kotlin 实现:
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
参考文章
文章标题:《Compose 屏幕适配》
本文地址: https://jixiaoyong.github.io/blog/posts/e15dda2e.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!