本文原作者: 业志陈,原文发布于: 字节数组
再来看一个不怎么完美的例子,以另外两台真机为例:
华为 nova5: 1080 x 2259 px,480 dpi,屏幕宽度为 1080 / (480 / 160) = 360 dp 三星 Galaxy S10: 1080 x 2137 px,420 dpi,屏幕宽度为 1080 / (420 / 160) = 411 dp
View 体系的适配方案
今日头条方案
宽高限定符方案
smallestWidth 方案
今日头条方案。基于系统将 dp 转换为 px 的公式 px = dp * density 来实现适配,通过在运行时动态修改 density 值的大小,使得修改后计算出的屏幕宽度就等于设计稿的宽度,从而使得在不同屏幕尺寸下我们都可以直接使用设计稿给出的 dp 值,且无需准备多套 dimens 文件。
宽高限定符方案。通过穷举市面上所有 Android 手机的屏幕像素尺寸来实现适配,通过比例换算来为不同分辨率的屏幕分别准备一套 dimens 文件,应用在运行时再去引用和当前设备完全匹配的 dimens 文件,以此来实现屏幕适配。
smallestWidth 方案。适配原理和宽高限定符方案一样,也是通过比例换算来为不同尺寸的屏幕分别准备一套 dimens 文件,应用在运行时再去引用和当前设备最匹配的 dimens 文件,以此来实现屏幕适配。
今日头条方案。优点: 可以直接使用设计稿中的 dp 值,无需准备多套 dimens 文件进行映射,因此不会增大 apk 体积,且在三种方案中 UI 还原度最高,其它两种方案都需要精准命中屏幕尺寸后才能达到此方案的还原度。缺点: 由于此方案会影响到应用全局,如果我们引入了一些第三方库的话,三方库中的界面也会随之被影响到,可能会造成效果变形,此时就需要进行额外处理了。
smallestWidth 方案。优点: 容错率高,在 320 ~ 460 dp 之间每 10 dp 就提供一套 dimens 文件就足够使用了,想要囊括更多设备的话也可以再缩短步长,基本不用担心最终效果会与设计稿偏差太多,且不会影响到三方库。缺点: 需要生成多套 dimens 文件,增大了 apk 体积。
Compose 默认的适配机制
public static float applyDimension(int unit, float value, DisplayMetrics metrics) {
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
继续跟踪 Dp.roundToPx() 方法,可以看到 dp 和 px 之间的转换方式和原生 View 体系一样,也是按照 px = dp * density 的公式来进行转换的,density 值由 Density 接口来定义和提供。
通过一步步查找 Density 接口的实现类,最终可以定位到 AndroidDensity.android 类,Jetpack Compose 就是在这里通过 Context 来获取对应的 density 和 fontScale。
此外,看过一些 Jetpack Compose 内部源码的同学应该知道,连接 Compose 和 View 体系之间的桥梁是 AndroidComposeView 类,而 AndroidComposeView 就通过 fun Density(context: Context) 方法来初始化其内部声明的 density 对象,CompositionLocals 类的 ProvideCommonCompositionLocals 方法又通过 LocalDensity 来将 AndroidComposeView 持有的 density 对象暴露给外部,从而使得框架内部和开发者均可以通过 LocalDensity.current 来获取到当前的 Density 对象,也即通过此方法拿到了 Android 系统的 density 和 fontScale 两个参数。
根据以上线索,我们可以推断出 Jetpack Compose 目前采用的屏幕适配机制其实就和 Android 原生的 View 体系一样,都是以屏幕像素密度作为适配基础,这使得 Jetpack Compose 一样存在文章开头介绍的问题,在不同手机屏幕上的显示效果相比设计稿都会有一点点误差。
在我看来,Jetpack Compose 设计 Density 接口的初衷就是为了实现多平台。如果 Jetpack Compose 仅仅是用于 Android 平台的话,直接获取当前设备的 density 值完成单位转换即可,Density 接口的存在意义并不大;而为了方便 Compose Multiplatform 实现跨平台,Google 官方才设计了 Density 接口。例如,Compose Multiplatform 支持在 Android 和 Windows 平台之间复用同一套 Compose UI,而相同的 dp 值在电脑屏幕上必须显示得更加大才行,通过抽象出 Density 接口,Compose Multiplatform 就可以为 Windows 平台提供更加合适的 density 值,从而使得显示效果更加适合电脑屏幕。
Compose 实现完美适配
看一个小例子:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val displayMetrics = LocalContext.current.resources.displayMetrics
val fontScale = LocalDensity.current.fontScale
val density = displayMetrics.density
val widthPixels = displayMetrics.widthPixels
val widthDp = widthPixels / density
val display =
"density: $density\nwidthPixels: $widthPixels\nwidthDp: $widthDp"
Text(text = display)
Greeting()
CompositionLocalProvider(
LocalDensity provides Density(
density = widthPixels / 360.0f,
fontScale = fontScale
)
) {
Greeting()
}
}
}
}
}
}
}
@Composable
fun Greeting() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Spacer(
modifier = Modifier
.size(
width = 180.dp,
height = 100.dp
)
.background(color = Color.Green)
.align(alignment = Alignment.Start)
)
Spacer(
modifier = Modifier
.size(
width = 180.dp,
height = 100.dp
)
.background(color = Color.Cyan)
.align(alignment = Alignment.End)
)
}
}
在三台不同分辨率的模拟器上运行代码,查看显示效果。
很明显就可以看出来,三台模拟器的屏幕宽度并非刚好就是 360 dp,因此前两个 Spacer 控件并没有达到预期效果;而第二个 Greeting() 函数仅仅是多嵌套在了一个 CompositionLocalProvider 中而已,直接套用设计稿给出的尺寸就让两个 Spacer 控件在不同屏幕上均占据了屏幕的一半宽度,完美达到了设计稿的要求。
由于我们可以在单独一个 Activity 中使用 Jetpack Compose,甚至可以在某个组合函数中局部修改 LocalDensity 值,因此我们可以细粒度地控制今日头条方案的生效范围,使其只在局部生效而不用担心会影响到其它业务模块,真正做到了完美适配且引入成本极低。
此外,我们主动修改 LocalDensity 还有一个好处: 可以自由控制应用内文字的显示大小。在默认情况下,采用了 sp 作为文字单位的应用,文字的显示大小是会随系统字体大小的变化而变化的,这是因为 sp 转换为 px 的公式 px = sp * fontScale * density 多了一个参数 fontScale,fontScale 代表的就是当前系统字体的缩放比例,当我们调大系统字体后,fontScale 会随之变大,连锁导致计算出的 px 值也随之变大。因此,通过主动修改 LocalDensity,我们还可以很方便地设置 Jetpack Compose 的字体大小,进一步完善应用最终的显示效果。
结尾
得益于 Jetpack Compose 预留了 LocalDensity 这个入口,使得今日头条方案在 Jetpack Compose 中显得十分简单易用,读者也可以将这部分代码抽取到 MaterialTheme 中,使其默认生效,进一步减少代码量。
@Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
val fontScale = LocalDensity.current.fontScale
val displayMetrics = LocalContext.current.resources.displayMetrics
val widthPixels = displayMetrics.widthPixels
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = {
CompositionLocalProvider(
LocalDensity provides Density(
density = widthPixels / 360.0f,
fontScale = fontScale
)
) {
content()
}
}
)
}
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"