小猪浅谈Android屏幕适配

tags: Tutorial


引言

      国庆前在微信群里看到有人在问Android屏幕适配的问题,凑巧自己最近时间
略有闲暇,索性来谈谈Android中屏幕适配相关的一些内容吧,鄙人才疏学浅,
所说的都是自己认知范围以内的,不可能面面俱到,如有疏漏不妥之处,还请
不吝指出,谢谢!配上最近被玩坏的Gif~


Android中的单位与名词


1.相对单位与绝对单位

对于计量单位,人们日常习惯性分为「相对单位」与「绝对单位」两类,前者
根据不同的情景表现出不同的大小,比如Android里的dp,px与sp等;而后者则是
制定了一个标准,cm(厘米),写死了,就那么多,1cm什么情况下都是一样大。

说到这个绝对单位,顺带提下两个单位:

  • in(英寸)1 in = 2.54cm
  • pt(磅,印刷行业常用单位):1 pt = 1 / 72 in

2.px (pixel,像素)

就是一个个的像素点图像的最小组成单元


3.dp (dip,密度无关像素)

Density-independent pixel,抽象意义上的像素与设备的实际物理像素点无关
可以保证在不同的像素密度的设备上显示相同的效果,也是Android独有的长度单位

1dp表示在屏幕像素密度为160dpi的屏幕上1dp = 1px
类推,在320dpi上,1dp = 2px,不难看出这样的转换公式:px = dp * (dpi / 160)


4.sp (sip,独立比例像素)

scale-independent pixel,字体大小专用单位会根据系统设置的字体大小进行
缩放
,推荐使用12sp以上的字体(12sp以下太小),不推荐用奇数和小数,容易造成
精度丢失问题。

Tip

尽管官方建议我们字体都用sp作为单位,但是sp字体会根据系统设置的字体大小进行
缩放,假如用户修改了系统字体大小,可能会导致我们APP的UI受到影响,比如字体
大了,然后各种显示不全,个人还是建议使用dp来做为字体单位


5.手机外观尺寸

整个手机的尺寸不止屏幕,一般在中关村或者其他评测网站都可以查到。
一般是用mm为单位,依次为长 x 宽 x 厚度,比如我的老掉牙的moto x 的尺寸就是:
159.3mm × 83mm × 10.1mm。


6.屏幕尺寸

屏幕对角线的长度,单位是英寸,计算公式如下:

其实就是勾股定理求对角线长度,比如长4寸,宽3寸的手机,他的屏幕

尺寸计算:

7.分辨率(Resolution)

屏幕竖直方向与水平方向的像素个数,比如:1280*720 就说明屏幕的
竖直方向上有1280个像素点,而水平方向上有720个像素点,单位px。


8.dpi与ppi

  • dpi:dot per inch,点密度每英寸多少个点,一般用作表示印刷品点密度;
  • ppi:pixels per inch,像素密度每英寸所包含的像素数目
    一般用作表示显示设备的点密度。

两者的值近乎相等,不用过于纠结!这个值的计算公式如下:


9.density(屏幕密度)

这个单位感觉是用来表示不同dpi的倍数关系,计算公式:density = dpi/160


10.常用单位转换工具类

public class DensityUtil {
    public static int dp2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    public static int px2dp(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

    public static int px2sp(Context context, float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }

    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}


11.获取屏幕尺寸与密度的三种方法

public class GetScreenParameter {  
    //方法一:已过时,可使用,但不建议使用  
    public static void getResolution1(Context mContext) {  
        Display mDisplay = ((Activity) mContext).getWindowManager()  
                .getDefaultDisplay();  
        int W = mDisplay.getWidth();  
        int H = mDisplay.getHeight();  
    }  
    
    //方法二:通过getWindowManager来获取屏幕尺寸的  
    public static void getResolution2(Context mContext) {  
        DisplayMetrics mDisplayMetrics = new DisplayMetrics();  
        ((Activity) mContext).getWindowManager().getDefaultDisplay()  
                .getMetrics(mDisplayMetrics);  
        int W = mDisplayMetrics.widthPixels;  
        int H = mDisplayMetrics.heightPixels;  
        // 屏幕密度(0.75 / 1.0 / 1.5)        
        float density = mDisplayMetrics.density;
        // 就是屏幕密度 * 160而已,屏幕密度DPI(120 / 160 / 240)
        int densityDpi = mDisplayMetrics.densityDpi; 
    }  
    
    //方法三:通过getResources来获取屏幕尺寸的,大部分用这个
    public static void getResolution3(Context mContext) {  
        DisplayMetrics mDisplayMetrics = new DisplayMetrics();  
        mDisplayMetrics = mContext.getResources().getDisplayMetrics();  
        int W = mDisplayMetrics.widthPixels;  
        int H = mDisplayMetrics.heightPixels;  
        float density = mDisplayMetrics.density;   
        int densityDpi = mDisplayMetrics.densityDpi; 
    }  
}  

Tip(可以不看,现在基本不会用这么小屏的手机了):

对于屏幕密度很低的小屏手机,比如240320,计算出来的尺寸,可能为:320427
原因是:没有设置多分辨率支持的话,Android系统会将240x320的低密度(120)尺寸
转换为中等密度160dpi对应的尺寸,如果你想获得正确的物理尺寸,需要在
AndroidManifest.xml里添加下述代码:

<supports-screens  
    android:smallScreens="true"  
    android:normalScreens="true"  
    android:largeScreens="true"  
    android:resizeable="true"  
    android:anyDensity="true"/>

Android适配准备


1.什么是Android适配

答:因为Android系统的开放性,很多厂商都喜欢对Android系统和硬件进行个性化定制,
以达到他们想要的样子,这种结果带来的「Android系统」和「手机屏幕」的碎片化问题
只对市场占用率较高的720和1080进行适配显然是不够的,为了让我们的Android应用在各种
各样的手机上保证界面效果一致,各种手机屏幕的适配显得非常重要,也是每个开发者
为之头痛的问题。


2.Android中的六种通用屏幕密度

屏幕密度 范围(dpi) 标准分辨率 dp与px 图标尺寸
ldpi(QVGA) ~ 120 240 * 320 1dp=0.75px 36 * 36
mdpi(HVGA) 120 ~ 160 320 * 480 1dp=1px 48 * 48
hdpi(WVGA) 160 ~ 240 480 * 800 1dp=1.5px 72 * 72
xhdpi(720P) 240 ~ 320 720 * 1280 1dp=2px 96 * 96
xxhdpi(1080p) 320 ~ 480 1080 * 1920 1dp=3px 144 * 144
xxxhdpi(2K) 480 ~ 640 1440 × 2560 1dp=4px 192 * 192

Tip:图标大小 = px数 * 4 * 12


3.关于UI设计稿的适配

我们可以通过友盟的 全域罗盘 知道当前国内的移动设备使用情况,
以此了解需要适配的趋势,以八月份的统计为例,截取屏幕尺寸与分辨率占比
拍醒前五的数据如下:

  

更多详细信息可自行查看,从上面两个表格不难看出这样的趋势:

5.5寸5寸 屏幕尺寸市场占比最高,而分辨率:1920x10801280x720
依旧是当前主流

也就是说让设计师按照720p和1080p出两套设计稿就可以适配大部分的
设备了,但是不同的公司因为人员或者时间等外部因素,可能有这几
种情况:

  • 三套:720p,1080p,1440p,这是最理想的情况;
  • 两套:720p,1080p,主流,大部分公司都或走两套;
  • 一套:720p,又或者和iOS共用一套 750x1334,可以当做720p的设计稿
    来编写UI,尽管比例和接近16:9,但是还是有些偏差,需要Android对UI进行微调!

UI能做的基本就到这里了,至于其他分辨率的手机则需要我们开通过
相关的手段来适配了。


Android适配开始


1.最简单一些适配技巧

先来说几个烂大街的适配技巧吧

1) 使用dp而非px

dp是像素无关的,而在实际使用中1dp大约等于1/160 in,比如一个160dp * 160dp
的控件,在大多数的屏幕上都能保持1 in * 1 in 的大小。

但是,并不是能解决所有问题的,以下两点要注意:

  • 1.实际效果还是会有些差距的,仅仅是相近而已;
  • 2.当设备的物理尺寸存在差异的时候,dp就显得无能为力了。
    比如,为4.3寸屏幕准备的UI,运行在5.0寸的屏幕上,很可能在
    右侧和下侧存在大量的空白;而5.0寸的UI运行到4.3寸的设备上,很可能显示不下。

2) 少写固定尺寸

少写固定尺寸,而使用 wrap_content, match_parentweight 权重 」


3) 使用相对布局,不要使用绝对布局

常识,而且绝对布局基本退出历史舞台了,可以忽略...


4) 自动拉伸的.9图

常识,.9图的作用是:拉伸的时候特定的区域不会发生图片失真,而不失真的区域
可以由我们自己绘制,从而实现图片适配。


5) 使用shape代替纯色图片

常识,一些纯色的矩形,圆角,圆都可以通过编写shape文件来替换,比起png,
xml文件小太多。


6)使用SVG矢量图替换位图

可能有些朋友对SVG矢量图有些陌生,其实和普通的位图最大的区别就是:
SVG是通过「XML文件」来定义一个图形,通过一些特定的语法和规则来绘制
出我们所需的图像,而不是位图那样通过存储图像中每一点的像素值来保存
与使用图形。

SVG是已经定义好怎么画这个图,需要的时候再去画,因为是按照特定的语法
和规则,理论上支持任何级别的缩放,而且不会失真,相比起多套同样的位图
文件,方便太多。

矢量图虽好,但是有几点要注意的:

  • 1.适用于Android 5.0以上,尽管官方有兼容包,低版本还是会有些问题的!
  • 2.不适合细节过于复杂的图片
  • 3.因为是用到的时候才画,所以加载图片所消耗的时间和资源可能会增加。

至于怎么用这个矢量图,你可以让美工在PS里把图片导出为SVG/PSD格式,然后
AS里右键drawable文件夹:

选中本地文件,确定

然后AS会帮你自动生成一个矢量xml文件,比如我的一个tab图标

右侧可以看到预览图:

然后要用到图片的地方直接引用即可。
至于SVG的规则以及如何自行编写不在本节重点,有兴趣的可自行搜索用法~


2.多套资源文件的套路

常用的套路依旧不能解决大部分的屏幕适配问题,Android给我们提供了
备用资源这个东西,详细可见官网:提供备用资源
简单说就是按照规范,创建屏幕对应的资源文件夹和资源文件,
Android会自动去加载对应文件夹里的资源的值,可能表述得有些不清晰,
举个简单的例子:

比如我们xxhdpi,padding为3dp,而在xxxhdpi下的padding为5dp
我们就可以创建一个对应的values文件夹和dimens.xml文件

相信你看到这里就明了,官网写的这个也就清晰了

而这个qualifier限定符怎么拼接可以看官网,或者看下面的简表:

看到这里,你可能会有问题,我要写多少套?
讲真,我也不知道,只能说看需要,但是想跟你说限定符里一个很常用的:smallestWidth
屏幕区域的最小尺寸,比如 values-sw320dp,只有在屏幕宽度不小于
320dp的才会使用这个文件夹里的dimens.xml文件,我们还可以另外建立一个
values-sw360dp,然后320dp那个文件夹只有在屏幕宽度在320dp到360dp
内才回去调用了,drawable,layout等资源文件也是同样的套路。


3.使用脚本对长度按照不同分辨率进行比例转换

就是拿屏幕宽度,去除以360,得出比例去乘以对应的dp值
比如:sw720dp里一个单位的1 dp = 2dp,而sw800dp里的1 dp = 2.22dp:

Github地址:https://github.com/mengzhinan/PhoneScreenMatch

具体的自己看文档和代码吧,可能有的疑问就是不是win电脑执行不了bat文件
的问题,比如mac,直接命令行,cd到 PhoneScreenMatch/app/src/main
目录下,键入下述命令即可:

java -jar screenMatchPX.jar

项目中还提供了一个px适配的,分开宽高做等值缩放的,对于长宽比很奇葩
的机子可以试试。当然这种适配方式带来的最明显的问题就是资源文件增加
的问题,这需要自行权衡。

类似的方案还有:https://github.com/paulyung541/EasyScreen


4.[过时] 百分比布局库支持库

这个库API 26.0.0 后的版本已经弃用了,在官方仓库可以看到

[DEPRECATED in support lib v26] you should now use ConstraintLayout widget

如果你想使用百分比来编写建议使用约束布局 ConstraintLayout
所以android-percent-support怎么用并不讲,想通过看看源码的方式
来了解百分比的实现套路,没兴趣的可以跳过了,或者看其他关于
百分比布局支持库的其他使用扩展文章:

源码过一过 (流程参照CSDN张鸿洋的博文)

lib库里的东西不多,就三个核心的类而已

如果让我们来做套路可能是:

  • 通过LayoutParams获取布局中设置的与百分比有关的属性
  • 父布局拿到这些属性值后进行动态计算,比如宽*宽占百分比
  • 子控件调用measure(计算后值)进行绘制。

看下代码具体是怎么做的,首先是获取Layout里子控件的属性,打开
PercentFrameLayout,重写了generateLayoutParams方法,
然后新建了一个LayoutParams对象

看看这个LayoutParams里卖的什么药:

通过PercentLayoutHelpergetPercentLayoutInfo方法构造一个
PercentLayoutInfo对象,而getPercentLayoutInfo里做的事则
是把attrs里的东东都拿出来,然后塞到PercentLayoutInfo对象用于返回:

而这个PercentLayoutInfo里定义了一堆百分比相关的属性,
还有一些方法,字面上大概可以知道是设置LayoutParams相关
的方法,就是把onMeasure里的逻辑操作抽取到了这里。

好的,属性拿到了,接着就是跟onMeasure方法了,调用了
PercentLayoutHelper对象里的adjustChildren方法

方法里做的东西也比较简单,拿到布局的宽高,然后
循环遍历布局里的子控件,如果params是百分比类型的
取出PercentLayoutInfo里info,然后去设置宽,高和margin。

再跟下fillLayoutParams方法,保存原本宽高后(后面用来重置),重新设置宽高,
fillMarginLayoutParams也是类似,百分比的基本套路就已经完了。

不过项目中还多了两个细节,计算值过小的情况以及重置原布局尺寸
onMeasure最后还调用handleMeasuredStateTooSmall方法,而方法做的
事是出去容器中所有的子控件,如果params是百分比类型的,则调用
shouldHandleMeasuredWidthTooSmall方法判断值是不是太小,太小则
把宽或高设置为ViewGroup.LayoutParams.WRAP_CONTENT(图中 = -2那里)
needsSecondMeasure设置为true,代表需要二次重绘。

重置布局尺寸的代码则是写在onLayout方法里(onMeasure后会回调)

一样是遍历子控件,然后把参数都重置为之前存mPreservedParams里的值,
而这个值是布局里设置的0dp。(感觉没什么用处...)

因为onMeasure的关键代码都写到Helper类里,扩展一个百分比的线性
布局也很简单,拷贝下PercentFrameLayout改点东西就好。
另外,因为依赖于父容器,导致ScrollView,ListView等容器内高度无法使用百分比
而鸿洋还另外对官方的百分比进行了扩展,写了一个android-percent-support-extend
有兴趣可以移步到他的博客阅读:Android 增强版百分比布局库 为了适配而扩展


5.[作者停止维护] AndroidAutoLayout

又是鸿洋大神的库,不过作者已经不维护了,使用之前还是三思!!!
博客链接:http://blog.csdn.net/lmj623565791/article/details/49990941

大概套路是

定好设计稿尺寸,直接选择对应分辨率的预览,AndroidAutoLayout 库会根据
占屏比例,自动计算转换当前屏幕下适配的大小。加载布局其实是把各自的
Layout都转换为对应的AutoLayout,从而不用在所有的xml中进行更改。

思路很gay,但是也有一些弊端,在onMeasure的时候进行数值计算,存在性能问题
扩展性差,用到的ViewGroup都需要自行对AutoLayout进行扩展。
最重要的是:

issues较多,而且作者不维护,应用到实际项目中还需自行权衡...


6.ConstraintLayout 约束布局

使用约束的方式来指定各个控件的位置和关系的,可以看做更加强大的
RelativeLayout,通过图形化界面拖拽的方式来编写界面,当然也可以
直接用XML进行编写,毕竟拖拽的背后也是XML来实现的,不过属性比较
多,直接编写显得有些繁琐,建议还是拖拽后自己在手动修改参数

ConstraintLayout约束布局除了支持百分比外,相比传统布局一层
套一层的嵌套布局结构,编写的XML元素层次结构更简单(一个外层
ConstraintLayout包全部),而且性能更优。
至于问题的话,可能就是布局中的元素过于复杂时,拖拽可能会有少许问题;

其他的话暂时没发现,关于ConstraintLayout与传统布局方式的性能对比可见:
解析ConstraintLayout的性能优势

而关于图形化拖拽的,郭霖大神有篇详细讲解的博文:
Android新特性介绍,ConstraintLayout完全解析

而关于属性讲解和具体实例的,可以翻看鸿洋的博文:
ConstraintLayout 完全解析 快来优化你的布局吧

附:记下关键点,方便自己日后回忆

  • Inspector:右侧Properties区域的上半部分,竖直和水平的轴用于确定
    位置;图中的四个16确定的是间距;中间的四个 >>> 的箭头确定的是大小
    有三种可选的模式:
    wrap_content、
    固定值、
    any size
    用于填充满当前控件的约束规则,和match_parent是不同的!

 

  • Guidelines:参照线
    ,就是弄个线给别的控件提供约束,有垂直和水平
    两种,用来实现一些百分比的效果很赞,比如下图这种两个控件并排居中的布局:

     
  • 自动添加约束:拖拽控件后,一个个去添加约束非常繁琐,ConstraintLayout中
    支持自动添加约束,有两种自动添加约束的方式:
    AutoConnect:默认关闭,启用后拖拉控件到布局里会自动生成约束

    Infer Constraints:控件拖拉得差不多后,点击后会为所有控件生成约束

    当然,这两种自动生成的约束可能有些问题,我们可以自己另外调整下~

7.等比例缩放的另一种实现方案

同样是等比例缩放的套路,原文可见:一种粗暴快速的Android全屏幕适配方案

上面也说了AndroidAutoLayout在onMeasure中进行数值运算可能会有性能问题。
而这个方案则是规避了这个问题,直接在Android进行长度计算的时候就进行换算;

系统长度计算的入口是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;
    }
}

而作者选择比较冷门的pt单位,从而避免对正常的px,dp,sp造成影响,
PT的计算为:value * metrics.xdpi * (1.0f/72),我们可以动前面
metrics.xdpi,这个值应改为:缩放比例7272的原因是想转px,
metrics可以通过context.getResources().getDisplayMetrics()
拿到。作者另外写了一个Helper类,通过构造方法传入设计稿的宽度除以
当前设备的分辨率得出比例,然后乘以72即可得出对应分辨率下的尺寸。

调用也很简单,直接在Application类启用即可:

new RudenessScreenHelper(this, 750).activate();

等比缩放套路换汤不换药,不过很机智的规避了复杂的计算问题,想法是挺赞的,
有兴趣的可以试试。


小结


      在本节中,笔者对于自己所知的Android原生适配套路都一一进行了复述,相信会对
你在开发中的屏幕适配有所帮助,当然具体要怎么适配还是看需求吧,比如我在的
小作坊,也是只是适配720p,2333;当然有其他的套路也欢迎告知,万分感激~
(PS:说来惭愧,本来想着国庆能发的,后面因为各种琐事拖到今天...)


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容