学习android避免不掉要进行屏幕适配,因为android碎片化极其严重,尤其是在国内这种环境下,不受google所约束,奇葩屏幕更是多种多样,由于android的开源性,从小到手表,大到电视以及企业影院屏幕,不得已给开发者更多的任务和困难去进行处理,没办法,来就来了,总结一下吧。
相关概念
屏幕相关及换算:
- 屏幕尺寸:
手机对角线的物理尺寸,单位英寸(inch),1英寸=2.54cm,Android手机常见的尺寸有5寸、5.5寸等。 - 屏幕分辨率:
手机在横向、纵向上的像素点数总和,一般用AxB表示,例如1920x1080,单位是px。 - 屏幕像素密度:
每英寸的像素点数,单位是dpi(dots per inch)。
屏幕尺寸、分辨率、像素密度三者关系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sBOWg9En-1587557725620)(https://note.youdao.com/yws/api/personal/file/DF83EDF37C6942EA9C4809F49344C962?method=download&shareKey=0511f23127e1722bba01649ca6ee480c)]
三个常用单位:
- 像素:
画面中最小的点,也叫单位色块,单位是px。 - 密度无关像素:
density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关,单位是dp,可以保证在不同屏幕像素密度的设备上显示相同的效果。在Android中,规定以160dpi(即屏幕分辨率为480x320)为基准:1dp=1px。 - 独立比例像素:
scale-independent pixel,叫sp或sip,单位是sp。Android开发时用此单位设置文字大小,可根据系统字体大小首选项进行缩放,如果想保持字体大小不随系统变化可采用dp做单位
,推荐使用12sp、14sp、18sp、22sp作为字体设置的大小,不推荐使用奇数和小数,容易造成精度的丢失问题;小于12sp的字体会太小导致用户看不清。
关系表:
密度类型 | 代表的分辨率(px) | 屏幕像素密度(dpi) | 倍数 | 换算(dp/px) |
---|---|---|---|---|
低密度(ldpi) | 320x240 | 120 | 0.75 | 1dp = 0.75px |
中密度(mdpi) | 480x320 | 160 | 1 | 1dp = 1px |
高密度(hdpi) | 800x480 | 240 | 1.5 | 1dp = 1.5px |
超高密度(xhdpi) | 1280x720 | 320 | 2 | 1dp = 2px |
超超高密度(xxhdpi) | 1920x1080 | 480 | 3 | 1dp = 3px |
屏幕尺寸匹配
布局匹配
灵活运用各种布局方式自适应屏幕
我们知道android传统通用有5种布局方式:
- 相对布局(RelativeLayout)
- 线性布局(Linearlayout)
- 帧布局(FrameLayout)
- 表格布局(TableLayout)
- 绝对布局(AbsoluteLayout)
开发过程中尽量不要用绝对布局,因为绝对布局是根据元素的具体坐标位置来定的,所以随着屏幕大小不一,其呈现的效果也不一样。其他的布局根据情况来使用,平时开发的过程中尽量使用相对布局RelativeLayout,因为无论屏幕大小,其相对位置是不会变的,因此整体效果也会统一。
根据屏幕的配置来加载相应的UI布局
有些时候我们需要为不同屏幕尺寸的设备设计不同的布局,这种情况下项目中一套布局是不行的,通用的做法是采用限定符,常用的限定符有:
- 尺寸(size)限定符
- 最小宽度(Smallest-width)限定符
- 布局别名
- 屏幕方向(Orientation)限定符
尺寸(size)限定符
当一款应用想在手机上显示一套布局,在平板或者电视上(>7英寸)显示另外的一套布局使其内容显示更多些,可以使用尺寸限定符(layout-large)通过再创建一个文件来实现。
res/layout/main.xml
res/layout-large/main.xml
这里需要注意两套布局文件的命名是一样的,另外内部必要组件的id也要一样。
最小宽度(Smallest-width)限定符
上边的layout-large是系统3.2之前推出的,仅仅限于以7英寸为分界,系统3.2之后google为了更精细化推出了最小宽度(Smallest-width)限定符,例如我想实现在宽度600dp以上显示一种布局可采用layout-sw600dp,宽度900dp以上显示另外一种布局可采用layout-sw900dp。
res/layout/main.xml //默认兼容布局
res/layout-sw600dp/main.xml //宽度大于600dp时显示的布局
res/layout-sw900dp/main.xml //宽度大于900dp时显示的布局
布局别名
试想一种情况同时拥有低于系统版本3.2的设备和高于系统版本3.2的设备,同时又包含大小屏几套布局,低于3.2的设备我们只能采用layout-large方式,高于3.2我们又建议layout-swxxxdp方式,这维护起来可就繁琐了,要维护好几套。反正都是大小屏,在某种尺寸下layout-large和layout-swxxdp的布局肯定是一样的。所以我们有必要解决一下这种重复文件的问题,通过某种方式使得layout-large和layout-swxxxdp下同时指向一个文件,我们只需要这一个文件即可,这里引入“布局别名”方案。
针对普通手机适配和大屏适配可同时在layout下添加两个文件:
res/layout/main.xml
res/layout/main_large.xml
然后根据系统版本3.2前后分别在values-large和values-swxxxdp下添加如下别名设置:
1.res/values-large/layout.xml(3.2之前的大屏布局):
<resources>
<item name="main" type="layout">@layout/main_large</item>
</resources>
2.res/values-sw600dp/layout.xml(3.2之后的大屏布局):
<resources>
<item name="main" type="layout">@layout/main_large</item>
</resources>
屏幕方向(Orientation)限定符
针对大屏可通过横向布局、纵向布局别名设置。
针对普通手机适配和大屏下纵屏、横屏适配可同时在layout下添加几个文件:
res/layout/main.xml //默认手机兼容布局
res/layout/main_large_port.xml //大屏纵向布局
res/layout/main_large_land.xml //大屏横向布局
res/values-large-port/layouts.xml
(大屏、纵向、单面板带操作栏-Andorid 3.2版本前)
<resources>
<item name="main" type="layout">@layout/main_large_port</item>
<bool name="has_two_panes">false</bool>
</resources>
res/values-large-land/layouts.xml
(大屏、横向、双面板、宽-Andorid 3.2版本前)
<resources>
<item name="main" type="layout">@layout/main_large_land</item>
<bool name="has_two_panes">true</bool>
</resources>
res/values-sw600dp-port/layouts.xml
(大屏、纵向、单面板带操作栏-Andorid 3.2版本后)
<resources>
<item name="main" type="layout">@layout/main_large_port</item>
<bool name="has_two_panes">false</bool>
</resources>
res/values-sw600dp-land/layouts.xml
(大屏、横向、双面板、宽-Andorid 3.2版本后)
<resources>
<item name="main" type="layout">@layout/main_large_land</item>
<bool name="has_two_panes">true</bool>
</resources>
针对双面板设置可在onCreat里直接通过getResources().getBoolean(R.bool.has_two_panes)获取值来判断是否进行展示双面板。
TAP
有些时候我们可能需要在特殊情况下进行限定某些布局或图片的引用,我们要用限定符进行限定,上文“限定符”仅仅针对尺寸匹配用到,按照多种要求这点限定符是远远不够的,这里我就偷个懒,推荐大家看一下官方文档:提供备用资源。
根据布局组件的相关属性自适应屏幕
通过属性相关值,使用"wrap_content"、"match_parent"和"weight“来控制视图组件的宽度和高度也是通用的做法,这里就不再详说了。
图片资源匹配
针对尺寸情况下图片资源自适应常用的就是自动拉伸位图:Nine-Patch,后缀名是.9.png,是一种被特殊处理过的PNG图片,设计时可以指定图片的拉伸区域和非拉伸区域;使用时,系统就会根据控件的大小自动地拉伸你想要拉伸的部分。androidstudio自带有.9图制作工具。
用户界面流程匹配
这种方式主要目的是根据屏幕的相关配置进而改变用户的操作流程。
举例:
1.纵屏的时候是一个列表,则点击列表跳转到详情页;横屏的时候左侧是一个列表,右侧是一个内容展示区域,点击左侧列表右侧显示相关内容。
2.纵屏的时候是一个列表,横屏的时候判断是否是双面板,不是双面板直接销毁当前页面返回到首页。
屏幕密度匹配
布局匹配
从单位说起
dp:为了使一套布局在不同的密度上显示相同的效果,采用px肯定是不行的了,h5开发的时候采用更多的单位em,即表示相对像素,android开发采用的单位是密度无关像素(dp),即表示与实际物理像素没有关系,是以480x320像素下160dpi作为基准,1dp = 1px,其他像素下进行换算,因此在基准分辨率下屏幕宽度>=控件宽度总和时,高分辨率下效果整体差不多。
sp:android建议textview字体单位采用独立比例像素(sp),这种单位同样与px没有直接关系,并且能跟随系统进行大小缩放,建议采用12sp、14sp、18sp、22sp作为字体设置大小,不推荐奇数和小数,容易造成精度的丢失问题。上边也提到如果想让应用不跟随系统字体大小进行字体缩放,可采用dp作为字体单位。
解决控件的屏幕尺寸和屏幕密度的适配问题
上边提到采用dp做单位满足布局匹配的条件是在基准分标率下屏幕宽度>=控件宽度总和,高分辨率下才能正常显示,假如在更低的分辨率下则有可能出现控件重叠的现象,这是不友好的,那么如何解决这个问题呢?
方案1.采用以父布局为基准子布局百分比的形式
即控件宽高的采用父布局的百分比形式,具体使用形式可参阅:https://github.com/JulienGenoud/android-percent-support-lib-sample
方案2.采用分辨率百分比的形式
编写脚本将长度转换成各分辨率下的长度,缺点是生成的分辨率文件很多并且根据设计稿的长宽去匹配很麻烦。具体可参看鸿阳的Android 屏幕适配方案“百分比引入”部分。
方案3.采用分辨率百分比变种的形式
这种方式还是采用百分比,只不过百分比的基准方式和前两种有所不同(思想和第二种相同),试想屏幕最终绘制的时候其原型仍然最终转换成分辨率进行屏幕绘制的。单位dp最终会转换成分辨率,sp最终也会转换成分辨率,所以我们干脆以分辨率为基准进行处理。假设当前设备的分辨率为1280*720,而设计搞的分辨率为1920*1080,只需要将两个分辨率做比得出一个比率,进而在设备上按照比率进行排布。
系统进行单位转换的入口为TypedValue中的applyDimension函数,传入单位与value将其计算为对应的px数值。
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;
}
可以看到这个方法中除了采用px为单位没有涉及DisplayMetrics的参数,其他单位都用到了,所以我们可以根据DisplayMetrics的相关参数按照比率重新赋值的方式来进行完美匹配。
下边我们仍然采用dp为单位:
/**
* 重置屏幕密度
*/
public static void resetDensity(Context context) {
//绘制页面时参照的设计图尺寸
final float DESIGN_WIDTH = 800f;
final float DESIGN_HEIGHT = 1280f;
final float DESTGN_INCH = 5.0f;
//大屏调节因子,范围0~1,因屏幕同比例放大视图显示非常傻大憨,用于调节感官度
final float BIG_SCREEN_FACTOR = 0.8f;
DisplayMetrics dm = context.getResources().getDisplayMetrics();
//确定放大缩小比率
float rate = Math.min(dm.widthPixels, dm.heightPixels) / Math.min(DESIGN_WIDTH, DESIGN_HEIGHT);
//确定参照屏幕密度比率
float referenceDensity = (float) Math.sqrt(DESIGN_WIDTH * DESIGN_WIDTH + DESIGN_HEIGHT * DESIGN_HEIGHT) / DESTGN_INCH / DisplayMetrics.DENSITY_DEFAULT;
//确定最终屏幕密度比率
float relativeDensity = referenceDensity * rate;
if (ORIGINAL_DENSITY == -1) {
ORIGINAL_DENSITY = dm.density;
}
if (relativeDensity > ORIGINAL_DENSITY) {
relativeDensity = ORIGINAL_DENSITY + (relativeDensity - ORIGINAL_DENSITY) * BIG_SCREEN_FACTOR;
}
dm.density = relativeDensity;
dm.densityDpi = (int) (relativeDensity * DisplayMetrics.DENSITY_DEFAULT);
dm.scaledDensity = relativeDensity;
}
注意获取放大缩小比率时是相对应作比的,这样计算出来的屏幕密度是固定的。
这种方式基本上可以避免控件在低分辨率的情况下出现重叠的现象,因为是相对整个设计图来的,这里只给出了一套高分辨率设计图的方案,如果开发过程中为了考虑到性能和全覆盖采用多套设计图,大家可根据以上思想加以扩展。
android8.0之前,整个应用长宽缩放比率均是采用一套,所以只需要在Application配置一次即可,但是在Android8.0的时候,系统架构调整,由原来的统一现在分配到每个Activity和全局Application中,Activity中设置的时候要注意一定要设置setContentView()之前,Application的设置即设置在onCreat()即可。为了使这个应用产生效果,建议将Activity形式的配置在BaseActivity中。
Activity中:
/**
* 使得在“setContentView()"之前生效,所以配置在此方法中。
* @param newBase
*/
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
ScreenUtil.resetDensity(this);
}
/**
* 在某种情况下需要Activity的视图初始完毕Application中DisplayMetrics相关参数才能起效果,例如toast.
* @param
*/
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
ScreenUtil.resetDensity(this.getApplicationContext());
}
Application中:
@Override
public void onCreate() {
super.onCreate();
ScreenUtil.resetDensity(this);
}
图片资源匹配
针对图片资源一般我们的做法是多套位图进行适配,那目前主流的屏幕来说我们至少需要UI设计师出两套图片(1080p和760p),但是对于某些大厂我们需要适配多种就需要多套位图了。
Android上图片资源匹配是按照屏幕密度范围自动匹配不同分辨率图片的。
密度类型 | 代表的分辨率(px) | 屏幕像素密度(dpi) |
---|---|---|
低密度(ldpi) | 320x240 | 120 |
中密度(mdpi) | 480x320 | 160 |
高密度(hdpi) | 800x480 | 240 |
超高密度(xhdpi) | 1280x720 | 320 |
超超高密度(xxhdpi) | 1920x1080 | 480 |
注意:针对.9图和某些不需要多套分辨率的图片直接放在drawable文件夹下即可。
针对mipmap和drawable文件夹的区别,这一点我在14年刚刚接触androidstudio时,也是认为mipmap应该比drawable引用图片的性能处理更好写,但是经翻看官方文档并没有说所有图片都要放在mipmap中,而是建议将android应用的启动图标放在各个mipmap文件夹中,而其他图片还是放在各个drawable文件夹中。
多套位图的缺点相比大家都很清楚了,第一是耗费UI设计师的时间,第二apk的包非常大,尤其第二个在某些场景也是致命的。有没有更好的解决方案解决这两个缺点呢?
答案是当然有。解决这个问题就是剔除多套位图的做法,直接在支持最大分辨率的drawable文件夹下放一套大分辨位图,android图片资源引用规则是系统根据设备的屏幕密度优先选取最合适的那套图片,如果找不到则向大一点分辨率文件去寻找图片来适配(为什么选取向大一级的文件夹下去寻找,目的是寻找到大图可以采用缩小处理,至少不会失真,反过来想想也是不可取)。
有同学这里问了,放大图片同样apk包很大,并且加载图片时特耗内存,这里我推荐一个工具:TinyPng,支持单个图片压缩,也支持多图片同时压缩,再配合目录“解决控件的屏幕尺寸和屏幕密度的适配问题”第三种方案,简直完美。
参考
- https://developer.android.com/guide/practices/screens_support?hl=zh-cn#qualifiers