自从工作以来,一直以屏幕适配斗智斗勇。由与Android碎片化严重,存在各种奇奇怪怪的分辨率,为了开发高质量的app,必然需要尽肯能的适配多机型,其中屏幕适配就是其中一项。经过多年的磨练,学习到了一些奇技淫巧。借此机会,做个总结,也算是给自己一个交代,如果顺便能帮到一些同学,那就再好不过了。计划分成两篇文章来彻底阐述屏幕适配的前世今生。本篇先介绍下为什么需要适配,以及为下篇怎么适配提供些预备知识。
为什么需要屏幕适配?
我们以实际开发为例:
一般情况下,设计mm只会提供一套设计稿并且还是以4.7
英寸的Iphone6
为标准。尺寸一般是750x1334
。
假设我们有小米9
和Pixel xl
两款手机,具体屏幕参数如下表:
机型 | 分辨率 | 屏幕尺寸 | ppi | dpi |
---|---|---|---|---|
小米9 | 1080x2340 | 6.39英寸 | 403 | 440 |
Pixel xl | 1440x2560 | 5.5英寸 | 534 | 560 |
设计稿中有个View
,宽度为375px
,视觉效果为手机宽度的一半。
如果我们直接在布局文件中设计宽度为375px
,那么:
在小米9
上,实际视觉效果仅为手机宽度的35%
(375/1080
);
在Pixel xl
上,实际效果更小,仅为手机宽度的26%
(375/1440
)。
显然实际效果没有还原设计稿要求,设计mm肯定会不开心了.
这时候肯定有小伙伴跳出来说:
怎么能直接用
px
作为单位呢,应该用Android爸爸提供的dp
啊.
- 什么是
dp
借着这个话题,我们继续来聊聊什么是dp
,顺便也说说上表中的ppi
和dpi
是什么.
ppi
是指每英寸像素点个数。通过对角线上像素点个数除以对角线长度(手机尺寸)得到。
以小米9
为例,手机尺寸是6.39英寸
,ppi
= 403
我们也可以得出结论,ppi是个物理值,与手机的分辨率和屏幕尺寸有关。
dpi
是指屏幕像素密度,网上很多人说它也是,实际上我并不想纠结它的定义,可以把每英寸像素点个数
dpi
理解成人为通过软件设置的值。
Android系统
中会在build.prop
文件中定义这个值,我们可以通过getprop ro.sf.lcd_density
命令获取到这个值
同样Android
也提供相应的api
来获取这个值
resources.displayMetrics.densityDpi
Android
框架的很多行为都是利用这个数值,比较常见的:
决定我们优先使用哪个资源文件;
资源文件的匹配顺序,及使用多大的缩放比例;
dp
具体转换成多少px
这样说可能比较抽象,我们一个个举例子:
Android
定义了标准的dpi
及对应的屏幕像素密度等级,当dpi
处于某个区间时,就会优先取对应文件夹下的资源,如下表:
dpi数值 | 120 | 160 | 240 | 320 | 480 | 640 |
---|---|---|---|---|---|---|
屏幕像素密度等级 | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi |
缩放比 | 0.75x | 1.0x 基准 | 1.5x | 2.0x | 3.0x | 4.0x |
假设我们资源目录结构如下
res/
drawable-xxxhdpi/
awesome-image.png //尺寸 50x50
drawable-xxhdpi/
awesome-image.png //尺寸 40x40
drawable-xhdpi/
awesome-image.png //尺寸 30x30
drawable-hdpi/
awesome-image.png //尺寸 20x20
drawable-mdpi/
awesome-image.png //尺寸 10x10
布局文件中ImageView
定义如下:
小米9
我们在小米9
手机上运行时,这个ImageView
宽高是多少呢?
由与小米9
手机的dpi
为440
,处于320~480
之间,那么我们的应用就会优先获取限定符为xxhdpi
文件夹的资源。 又因为标准xxhpid
像素密度dpi
应该是480
。
因此在小米9
手机上,这个ImageView
的宽高为37x37
,计算公式为:(37= 440/480 * 40)
如果我们把drawable-xxhdpi
文件夹中的awesome-image.png
删除,此时应用会从像素密度等级
最高的xxxhdpi
依次向下寻找。
由与drawable-xxxhdpi
文件夹中存在该资源,因此在小米9
手机上,此时ImageView
的宽高为35x35
,计算公式为:(35= 440/640* 50)
。
Pixel xl
同样的场景在Pixel xl
上呢?
由于Pixel xl
手机的dpi
为560
,处于480~640
之间,那么我们的应用就会优先获取限定符为xxxhdpi
文件夹的资源。 又因为标准xxxhpid
像素密度dpi
应该是640
。
因此在Pixel xl
手机上,ImageView
的宽高为44x44
,计算公式为:(44= 560/640 * 50)
。
如果我们把drawable-xxxhdpi
文件夹中的awesome-image.png
删除,此时应用会从像素密度等级
最高的xxhdpi
依次向下寻找。
由于drawable-xxhdpi
文件夹中存在该资源,因此在Pixel xl
手机上,此时ImageView
的宽高为47x47
,计算公式为:(47= 560/480 * 40)
。
我们还剩下两个疑问,dp
是什么以及dp
如何转为px
。
dp
可以理解为Android
设计者为了开发人员更好地做屏幕适配工作,而设计的一种虚拟像素单位。同样sp
也一样。
1dp
在不同dpi
的手机上会表示不同大小的px
,比如
在小米9上 1dp = 440/160 * 1px = 3px,
在Pixel xl上 1dp = 560/160 * 1px = 4px 。
可以发现在不同手机上(实际是不同dpi
)1dp
代表的像素点个数是不同的。这也是为什么有些小伙伴说布局文件中不要使用px要使用dp,以做屏幕适配。
可能有小伙伴质疑或者懵逼,凭什么是按照上面的公式计算,你是不是瞎xx编的。
好嘛,我们来看看Android
源码
// step1 ViewGroup.LayoutParams#setBaseAttributes
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
// 进入step2
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
// step2 TypedArray#getLayoutDimension
public int getLayoutDimension(@StyleableRes int index, String name) {
.......省略无关代码
else if (type == TypedValue.TYPE_DIMENSION) {
// 进入step3
return TypedValue.complexToDimensionPixelSize(data[index + STYLE_DATA], mMetrics);
}
.......省略无关代码
}
// step3 TypedValue#complexToDimensionPixelSize
public static int complexToDimensionPixelSize(int data,
DisplayMetrics metrics)
{
final float value = complexToFloat(data);
// 进入step4
final float f = applyDimension(
(data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
value,
metrics);
final int res = (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f));
if (res != 0) return res;
if (value == 0) return 0;
if (value > 0) return 1;
return -1;
}
// step4 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;
}
我们在xml
中设置layout_width
属性,属性值最终会映射到ViewGroup.LayoutParams
的width
字段,具体数值通过TypedValue#applyDimension
方法计算得到。
如果我们设置的单位是dp
,会走到如下分支:
return value * metrics.density
显然这里的转换与DisplayMetrics
的density
字段有关,我们继续看下这个类中几个关键字段的赋值。
// DisplayMetrics#setToDefaults
public static int DENSITY_DEVICE = getDeviceDensity();
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
public static final int DENSITY_MEDIUM = 160;
public void setToDefaults() {
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
densityDpi = DENSITY_DEVICE;
scaledDensity = density;
}
private static int getDeviceDensity() {
// qemu.sf.lcd_density can be used to override ro.sf.lcd_density
// when running in the emulator, allowing for dynamic configurations.
// The reason for this is that ro.sf.lcd_density is write-once and is
// set by the init process when it parses build.prop before anything else.
return SystemProperties.getInt("qemu.sf.lcd_density",
SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
}
从源码可以看出,getDeviceDensity()
就是获取build.prop
中的厂商设置的dpi
,三个关键字段的赋值也很清晰,如下:
densityDpi = build.prop文件中ro.sf.lcd_density的值
density = densityDpi/160
scaledDensity = density
再回到TypedValue#applyDimension
方法中,很容易得出dp
与px
的转换公式,顺便无意间也参透了sp
与px
转换公式:
density = densityDpi/160
scaledDensity = density
px = density * dp
px = scaledDensity * sp
如果不调整手机设置中的字体缩放比例,默认情况下 density = scaledDensity
这里需要注意的是applyDimension
方法返回的是float
类型,而像素是int
类型,在TypedValue#complexToDimensionPixelSize
中有float
转int
的操作:
final int res = (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f));
其实就是简单的四舍五入。(插个题外话:我猜你项目也有UIUtils工具类,里面有dp2px方法,而方法的内容也就是类似上面那样)
对照下上面的逻辑,我们再验证之前的转换结果是否正确:
在小米9上 1dp = 440/160 * 1px = 3px,
在Pixel xl上 1dp = 560/160 * 1px = 4px
是不是千真万确,我没骗你。
回到文章开头的例子:
设计稿尺寸为750*1334,其中有个
View
,宽度为375px
,视觉效果为手机宽度的一半.
Iphone6理论上来讲,density是2,因此375px
转换为dp为375/2=187.5dp
。
我们尝试使用dp
作为单位,在小米9和Pixel xl上实际是什么样子呢。
// 怕你忘了公式,再来一次:
px = densityDpi/160 * dp
小米9
上 516px = 440/160 * 187.5px
,实际视觉效果仅为手机宽度的48%
(516/1080
);
Pixel xl
上 656px = 560/160 * 187.5px
,实际视觉效果仅为手机宽度的46%
(656/1440
);
是不是都比较接近50%
,最起码比使用px
的效果要好得多。
但还是不可避免的存在误差,设计mm肯定还是会不开心。
那我们到底怎么做才会完美还原设计稿的视觉效果呢,因为篇幅原因,下篇再来揭晓。