安卓屏幕现状
目前市面上安卓的屏幕尺寸差异性比较高
- 按照宽度分主流的尺寸为 720px、1080px、1440px等;
- 按照宽高比来分,除了主流的10802400、1080 2340 、10802376、1440 3200等屏幕宽高比大于等于2,还有7201280、1080 1920、10802160、1440 2280、14402560 720 1280、10801920等低于2的设备,还有一些设备虽然是宽高比等于2,但是因为屏幕比较小,亦可以归类于第二种,比如400800等;
- 大部分新的手机都能设置屏幕大小,动态改动屏幕内容显示,在标准的现实比例下展示没问题的内容在调整后可能会出现问题;
以上这种差异或者动态调整给安卓的屏幕适配带来很大的困扰。
视觉出图原则
视觉为了保持统一在出视觉稿的时候基本上只出一套布局,比如以ios为750px*1624px分辨率进行出图,宽高比大于2,按照二倍换算,宽度换算成375的,这样尺寸可以在安卓手机上直接当做dp使用,因为安卓主流机型的宽度都会通过px和dp的换算关系换算成360dp,所以安卓端的宽度尺寸上直接使用视觉稿的标注即可。在高度上谷歌是提供了滑动或者列表的方案,但是如果业务需要页面不可滑动,信息在纵向展示的效果就会遇到适配问题。
名词定义
px:像素基本单位,比如屏幕的分辨率为1080*1920,则在宽度维度上是1080px,在高度维度上是1920px
dp:一个虚拟像素单位,1 dp 约等于中密度屏幕(160dpi:"基准"密度)上的 1 像素
sp:可缩放虚拟像素单位,默认情况下,sp 单位与 dp 大小相同,但它会根据用户的首选文本大小来调整大小,会影响屏幕文字的展示效果,在一般业务中应当谨慎使用
density:根据当前像素密度指定将 dp 单位转换为像素时所必须使用的缩放系数
densityDpi(dpi):像素密度,因为英寸是屏幕斜对角的长度,所以该数值是屏幕斜对角上每平方英寸像素的个数
换算关系
在屏幕为160dpi时,1dp=1px,缩放系数density为1, 所以android中的dp在渲染前会将dp转为px,计算公式:
px = density * dp;
density = densityDpi / 160;
px = dp * (densityDpi / 160);
**
以10801920,屏幕尺寸为5时,那么densityDpi*为440,换算关系如下:
为了计算方便,最后我们以三倍手机为例(density = 3 ,densityDpi = 480),宽高换算成dp为 360dp * 640dp。
获取物理分辨率(px)
ini
//屏幕的宽度
public static int getScreenWidth(Context context) {
if (context == null) {
return -1;
}
if (screenWidth > 0) {
return screenWidth;
}
DisplayMetrics dm = context.getResources().getDisplayMetrics();
screenWidth = dm.widthPixels;
return screenWidth;
}
//屏幕的高度
public static int getScreenHeight(Context context) {
if (context == null) {
return -1;
}
if (screenHeight > 0) {
return screenHeight;
}
DisplayMetrics dm = context.getResources().getDisplayMetrics();
screenHeight = dm.heightPixels;
return screenHeight;
}
px和dp互相转换
ini
public static float convertDpToPixel(float dp, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float px = dp * ((float)metrics.densityDpi / 160.0F);
return px;
}
public static float convertPixelsToDp(float px, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float dp = px / ((float)metrics.densityDpi / 160.0F);
return dp;
}
获取虚拟宽高(dp)
ini
Display mDisplay = activity.getWindowManager().getDefaultDisplay();
float height = ExtViewUtil.convertPixelsToDp(mDisplay.getHeight(), activity);
float width = ExtViewUtil.convertPixelsToDp(mDisplay.getWidth(), activity);
屏幕适配
在适配过程中,避免使用像素px直接来定义尺寸,因为不同的屏幕具有不同的像素密度,所以同样数量的像素在不同的设备上可能对应不同的物理尺寸。
所以要在密度不同的屏幕上保留界面的可见尺寸,必须使用密度无关像素 (dp) 作为度量单位来设计界面。
配置文件适配
资源适配
谷歌也是设计了相关密度限定符文件夹进行图片资源的适配,不同大小的资源应存放在指定的文件路径下,根据以上关系所以得出以下相关dpi的
以下是不同限定符号对应的dpi的数值
smallestWidth限定符适配
通过创建多个values文件夹,系统根据限定符去寻找对应的demins.xml文件,查找原理是从大往小找,例如设备的最小宽度为 360dp,就会先去找 values-sw360dp,发现没有则会向下找 values-sw320dp,如果还是没有才找默认的 values 下的 demens.xml 文件,所以即使没有完全匹配也能达到不错的适配效果。
screenMatch插件链接:plugins.jetbrains.com/plugin/1005...
但是这种适配就是一种穷举的方式,会增加后期维护成本,比如新增加一个尺寸值需要重新跑一遍配置文件,而且多个文件会增加包大小,不利于跨bundle开发,需要进行依赖否则需要在任意ui模块中添加适配文件
更改densiny
上面我们得知dp和px的换算关系,px是物理大小我们无法改变,但是可以使用动态设置dp的方式锚定视觉改稿上面的宽度(320/360)进行适配。如下是代码实践,确实可以通过改变DisplayMetrics的方式修改屏幕的展示比例,但是通过这种方式是对整个app生效,可能其他页面会有展示问题,而且用户手动更改页面缩放大小不会生效会影响用户体验。
ini
private static float mCurrentDensity;
private static float mCurrentScaledDensity;
public static void setCustomDensity(Activity activity, Application application) {
DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
if (0 == mCurrentDensity) {
mCurrentDensity = appDisplayMetrics.density;
mCurrentScaledDensity = appDisplayMetrics.scaledDensity;
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
if (newConfig.fontScale > 0) {
mCurrentScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
float targetDensity = appDisplayMetrics.widthPixels / 360f;
float targetScaledDensity = targetDensity * (mCurrentScaledDensity / targetDensity);
int targetDensityDpi = (int) targetDensity * 160;
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
appDisplayMetrics.scaledDensity = targetScaledDensity;
DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
}
布局语法适配
为了确保布局能够灵活地适应不同的屏幕尺寸,应该对大多数视图组件的宽度和高度使用 "wrap_content" 和 "match_parent",而不是硬编码的尺寸,或者在线性布局LinerLayout里最好使用权重的方式,相对布局RelayLayout中使用相对位置进行布局。
"wrap_content" 指示视图将其尺寸设为适配该视图中相应内容所需的尺寸。
"match_parent" 使视图在父视图中尽可能地展开。
局限性
- 无论使用哪种适配方式,结合语法方式都是必须的选择,但是对于一些pad机型会导致控件纵向拉伸问题,需要限定最大的布局尺寸,一般限定360dp/320dp即可;
- 使用配置文件可以在宽度上完全适配视图,因为要穷举dp转换成px值,适配文件的存在也会增加包大小,不利于跨bundle开发,需要进行依赖否则需要在任意ui模块中添加适配文件,所以该是配方式适合单应用对包大小不敏感的业务开发,该方案每一条都不满足支付宝业务使用条件;
- 使用更改density的方式会导致更改逻辑对全局生效,从而影响其他页面的展示效果,用户修改屏幕大小与期望的放缩展示效果不符,会影响用户使用;
- 均无法在高度上适配,需要依赖列表或者滑动进行适配,如果不可滑动需要进行屏幕动态判断调整
最终实践
如上对于屏幕适配,谷歌官方其实已经做了很多,使用虚拟像素和布局语法方式其实已经解决了大部分问题,在一些特殊机型上进行细微调试即可。
- 宽度维度一般采用控件平铺+margin的方式留白进行适配,内容设置成水平居中
-
- 小屏幕手机:一般采用控件平铺+margin的方式留白进行适配,文案或者控件信息展示过多还是会导致在一些低于360dp手机上展示不下的问题,解决这类问题的方式如果是文案就使用折行或者省略号,控件则按照适配的最小机型进行细微调试
- 宽屏手机(pad/折叠屏):对于需要注意宽高比展示的控件可以设定控件的最大宽度,防止控件过度拉伸
- 高度维度上对于高宽比大于2的机型可以直接使用视觉稿上面标注进行从上向下的排列,对于小屏手机来说可以让视觉出一份7201280或者10801920的视觉稿进行适配,在代码中判断屏幕分辨率和展示的高度对于控件的高度和margin进行调试。当然如果页面允许使用列表或者可滑动自然也就没有高度上的适配问题
但是在一些机型上可以更改屏幕缩放大小,以分辨率为1080px * 2312px的手机为例,density越大则视图展示越大,统计如下:
- 打开底部虚拟按键:
width(dp) | height(dp) | density | densityDpi |
---|---|---|---|
360.0 | 722.6667 | 3.0 | 480 |
338.82352 | 677.3333 | 3.1875 | 510 |
320.0 | 637.03705 | 3.375 | 540 |
- 隐藏底部虚拟按键:
width(dp) | height(dp) | density | densityDpi |
---|---|---|---|
360.0 | 770.6667 | 3.0 | 480 |
338.82352 | 725.3333 | 3.1875 | 510 |
320.0 | 685.03705 | 3.375 | 540 |
假如使用1080*1920的手机进行缩放,得到的高度会更小。所以对于机型适配,简单的使用物理分辨率进行判断显然已经不能满足屏幕适配的需要,需要将宽高转化虚拟dp尺寸后进行动态适配判断。如下在进行多个机型的对比测试下找到了小屏手机的适配阈值,具体业务可动态调整。
arduino
public static boolean isLowDeviceScreen(Activity activity) {
try {
Display mDisplay = activity.getWindowManager().getDefaultDisplay();
float height = ExtViewUtil.convertPixelsToDp(mDisplay.getHeight(), activity);
float density = activity.getResources().getDisplayMetrics().density;
AliUserLog.i(TAG, "isLowDeviceScreen - height = " + height + " density = " + density);
return height <= 685f;
} catch (Exception e) {
AliUserLog.e("isLowDeviceScreen -Exception ", e);
}
return false;
}
对于适配问题其实最重要的还是要和视觉频繁沟通,在有限的屏幕下需要透出多少信息量是有限制的,太多的信息会导致屏幕控件展示比较满,反而会影响用户体验。
未来:
未来RN+Flutter+H5+云渲染等跨平台或者动态化方案会取代部分原生页面,但是会引进其他其他技术栈的适配问题,需要开发者进行持续思考和调整适配方案。