系列介绍
临时抱佛脚(也说急来抱佛脚) 指平时不烧香,遇到危难时才求佛保佑。比喻事到临头才慌忙想办法应付。
少壮没有努力,所以现在知识不给力。
抱佛脚的目的只有一个,就是斩获自己期望中的offer.
灵魂拷问:你们 Android 开发的时候,对于 UI 稿的 px 是如何适配的?
我只会:dp加上自适应布局以及weight布局比例来适配(也就是传统屏幕适配方案)。
我的ui适配知识深度止步于此。
简直是十分非常太菜了。
名词解释
dpi
:像素密度是屏幕单位面积内的像素数,称为 dpi(每英寸的点数)。通常以尺寸作为手机大小衡量单位,所以dpi计算公式为 : 对角线px/ 手机尺寸 。也就是如下图所示
目录
- 大家都在用的屏幕适配方案
- 今日头条屏幕适配方案学习
- 官方屏幕适配方案
- 头条方案的第三方加强版 AndroidAutoSize
- 总结
一,大家都在用的屏幕适配方案
传统适配方案解释
Android官方提供了 dp单位来适配屏幕,传统适配方案中,我们通常会结合约束布局和weight比例来实现布局的还原。
dp和px的转换
android中的dp在渲染前会将dp转为px,计算公式:
px = density * dp;
density = dpi / 160;
px = dp * (dpi / 160);
屏幕的dpi则是每单位尺寸像素密度。
dpi计算公式查看名词解释部分。
传统方案问题
由dpi计算方式可知,dp可以实现大部分相似宽高屏幕,以及相似比例像素密度的ui适配。但却不是完全的屏幕等比关系。
同样的20dp可能占用屏幕的宽高比例会因手机而异。
这就导致了不同设备之间通过约束和dp适配可能会呈现不同的效果。
如下图demo中所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:src="@drawable/ic_launcher_background"
android:layout_width="match_parent"
android:scaleType="centerCrop"
android:layout_height="200dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="150dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
可以看到相同的布局文件,在不同尺寸相同dpi的设备中,表现就十分迥异。
可见对于各种形状和大小的安卓手机,ui展示变得奇怪,也就十分常见了。
传统方案的优势
传统适配方案按照像素密度来做显示适配。这样在同样像素密度,不同屏幕大小的设备中,会有这一致的大小体验。
比如一个小屏手机中的块,在大屏设备中也是相似的大小。
这样就可以在大屏设备中也就能显示更多的内容,在做好多布局适配的情况下,能有更符合美学的展示效果。
而不是粗暴的等比放大。
二,今日头条开源屏幕适配方案使用
方案目标
从文章中可知,今日头条的目标是,以宽度为基准,等比还原设计图。
这样在宽度大同小异的手机设备中,将会有更加优秀的还原体验。
方案原理
该方案是为了以宽度为基准还原ui图。
前面提到 px和dp的转换是 px = dp * density; 而想要让所有手机的宽度都有一样的dp。只需要修改density的值,就能转换出想要的px。
-
计算出density
比如 当以360dp为设计宽度基准的时候。
需要的 density 计算方式如下:
dp
为设计dp
sW
为屏幕宽度px
-
找到要替换density的值对象
通过阅读源码,我们知道布局文件中dp转成px
- 首先会调用 TypedArray#getDimensionPixelSize
- 然后调用 TypedValue#complexToDimensionPixelSize
- 最终调用TypedValue#applyDimension
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和sp转换是使用的 metrics.density
和 metrics.scaledDensity
头条文章中也说,还有些其他dp转换的场景,基本都是通过DisplayMetrics 来计算的。不再赘述。
所以我们只需要替换resource.mMetrics 的density 和 densityDpi 以及 scaledDensity 的值就行了
-
替换application和activity的 resource.mMetrics
替换之后,系统转换px的时候自然就会使用该density了,和简单就实现了以屏幕宽度为基准的适配方案。
项目使用
//动态代理减少模板代码
inline fun <reified T> noOpDelegate(): T {
return Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
arrayOf(T::class.java)
) { _, _, _ ->
} as T
}
class App : Application() {
override fun onCreate() {
super.onCreate()
fun setDisplay(resources: Resources) {
with(resources.displayMetrics) {
val originDensityRatio = scaledDensity / density
density = widthPixels / 360f
densityDpi = (density * 160).toInt()
scaledDensity *= originDensityRatio
}
}
///修改app.resources.displayMetrics
setDisplay(resources)
///字体改变回调
registerComponentCallbacks(object : ComponentCallbacks by noOpDelegate() {
override fun onConfigurationChanged(newConfig: Configuration) {
if (newConfig.fontScale > 0) {
setDisplay(resources)
}
}
})
///修改activity.resources.displayMetrics
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
setDisplay(activity.resources)
}
})
}
}
在添加之后,就可以看到手机完美得以360dp为全屏宽度基准了
用我的小米10演示下适配前后的样子:
可以看到效果还是挺不错的。
字体适配
当然文章中还提到了文本大小的适配,也就是 scaledDensity的值计算。
文章中的计算原理如下:
通过计算之前scaledDensity和density的比获得现在的scaledDensity
最后调用 Application#registerComponentCallbacks 注册下 onConfigurationChanged 监听
,在字体变化的时候去更新scaledDensity。
代码参考前面 项目使用 的代码
头条适配问题思考
头条适配方案以宽度为基准,适合手机设备,按宽度比例还原设计图,达到美观的效果。
然后这个方案的问题也很明显。在大屏设备上,也是按照宽度等比还原设计图。
这就导致了app大屏幕运行就只是手机的放大版,完全没有体验性可言。
解决方案:我思考了一下,这就需要在检测屏幕为大屏幕的时候,主动取消该适配方案。并做对应布局更换适配处理。
而更换的大屏适配方案,可以使用 下一节的 smalllest 屏幕适配方案
三,官方屏幕适配方案-限定符 or .9png
首先放上官方说明
smallestWidth 限定符
最小宽度限定符是谷歌官方支持的屏幕多布局多资源方案。
使用“最小宽度”屏幕尺寸限定符,您可以为具有最小宽度(以密度无关像素 dp 或 dip 为度量单位)的屏幕提供备用布局。
比如:
res/layout/main_activity.xml # 用于手机设备 (小于 600dp 屏幕宽度的设备)
res/layout-sw600dp/main_activity.xml # 用于 7寸平板 (600dp 或者更宽屏幕的设备)
最小尺寸参考
最小宽度限定符指定屏幕两侧的最小尺寸,而不考虑设备当前的屏幕方向,因此这是一种指定布局可用的整体屏幕尺寸的简单方法。
下面是其他最小宽度值与典型屏幕尺寸的对应关系:
320dp:典型手机屏幕(240x320 ldpi、320x480 mdpi、480x800 hdpi 等)。
480dp:约为 5 英寸的大手机屏幕 (480x800 mdpi)。
600dp:7 英寸平板电脑 (600x1024 mdpi)。
720dp:10 英寸平板电脑(720x1280 mdpi、800x1280 mdpi 等)。
再结合屏幕方向限定符
如下:
res/layout-land/main_activity.xml # For handsets in landscape
res/layout-sw600dp/main_activity.xml # For 7” tablets
这样就能完成大多数场景下的屏幕适配工作
.9 png 九宫格位图
普通位图在放大或者缩小时候,会失真,被拉伸挤压。
解决方案是使用九宫格位图,这种特殊格式的 PNG 文件会指示哪些区域可以拉伸,哪些区域不可以拉伸,以及安全的内容区域。
九宫格位图基本上是一种标准的 PNG 文件,但带有额外的 1 像素边框,指示应拉伸哪些像素(并且带有 .9.png
扩展名,而不只是 .png
)。
安卓框架默认支持。
这种开发者基本上很常用。不再赘述,不清楚的可以百度。
屏幕分辨率限定符(不建议使用)
该限定符官方文档并没有说明,我也找到了一篇文章,从解析源码处入手分析了该限定符的用法和逻辑。链接在这。该作者是个大佬,直接扒了框架的源码,羡慕大佬的厉害中。
本人以前使用的时候也是云里雾里的。
资源文件夹用法如下 :
- values-480x320
- layout-480x320
现在总结一下改文章的结论:
- 分辨率限定的优先级排序十分靠后,仅仅先于平台版本。
- 分辨率限定会排除任一维度大于实际分辨率的配置。比如有 2020x1080、1080x740以及960x540限定的资源。一台1920x1080的手机,会排除掉2020x1080的资源,匹配1080x740或960x540中的一个资源。
限定分辨率总结:
这个限定符生效逻辑十分诡异,有悖常理,不是只适配特定分辨率,而是会影响到所有完全大于该分辨率的屏幕,用了得不偿失,建议不要使用。
四,头条方案的第三方加强版 AndroidAutoSize
首先送上的是作者的介绍文章
AndoridAutoSize库是作者根据头条屏幕适配方案,经过不断的优化和扩展完善的一个屏幕适配库。里面支持了 dp、sp、pt、in、mm 等各种单位进行布局。
因为没有详细查阅该库源码,所以不在这里进行赘述。写在这里作为一个备选方案以备后续使用学习
五,总结
在手机场景下,可以使用头条的适配方案完美还原手机的设计稿。
而在大屏或者特殊屏幕尺寸场景,可以使用sw 和 方向限定符结合,使用不同的layout 或者dimens文件夹,将会有不错的结果。
通过这次屏幕适配方案,我也有不少收获
- 了解了 sw 限定符的规则
- 了解了分辨率限定符的规则
- 了解了今日头条屏幕适配的原理