Android屏幕适配从0->1

3,998 阅读14分钟

Android屏幕适配 android官方说明:guide->devices->Device->device compatibility

框架推荐

全是文字,阅读需要耐心。

一、屏幕基础知识

1.1 分辨率(px)

分辨率就是手机屏幕的像素点数,一般表示方式为 “ 高 * 宽 ” ,例如 1280 * 720 、 1920 x 1080 等。如 1920 * 1080 表达意思则为宽度方向有 1080 个像素,竖向方向有 1920 个像素,像素表示方式为 px。

1.2 屏幕尺寸大小(in)

屏幕尺寸大小是手机屏幕(最长)对角线的物理尺寸,一般以英寸(inch)为单位,例如某手机以6寸大屏手机为卖点,换算单位:1英寸 ≈ 2.54cm。

1.3 屏幕密度(ppi)

密度一般表示单位空间(有一维的、二维的、多维的)内某个特征值的多少。例如水的密度为 1g/cm³、某区域内羊的密度为 60只/亩 或 60只/km² 等。那么在屏幕密度中的密度则为单位长度内,像素的个数,即 px/in(ppi: pixels per inch),这里的长度为最长方向,即对角线。 例如一部手机的分辨率为 1080 * 1920 ,屏幕大小为 6 英寸(in),根据勾股定理,ppi = sqrt{w^2 + h^2}。 那么此设备的屏幕密度为 sqrt{1920^2 + 1080^2}/6 = sqrt{4852800}/6 = 2202.9/6 ≈ 367ppi。

1.4 Android中的屏幕密度(dip与dp)

dip 全称为 Density-independent pixel 简写为 dp,也就是说 dip 和 dp 其实是一个意思,只是 dp 是 dip 的简写而已。dip 翻译过来即独立密度像素。是跟尺寸像素无关的一种表示单位。1dp 的意思为:在屏幕像素密度为 160ppi 时1px的长度。也就是说,在屏幕密度为160ppi时,1dp = 1px。 换算单位为 dp(dip) = px /(ppi / 160)= px * (160 / ppi)。

(公式理解:320ppi 时,1dp = 2px ,1dp = 2px * 160 / 320)

使用dp几乎可以保证在不同屏幕分辨率上显示的物理大小尺寸一致。举个例子,假如物理尺寸(对角线)同样大小的屏幕,一个屏幕的 ppi = 160 ,另一个的屏幕的 ppi = 320 ,此时需要显示1dp,320ppi的显示为2px,160ppi的显示为1px,那么在物理尺寸上,他们显示的对角线长度则一致。so,相同尺寸情况下,显示同样像素大小的元素,屏幕ppi越大,dp代表实际的px值越大,所需dp则越小。

那么,其实关于dp,也可以这么理解,他等同于ppi,不过是ppi的一种缩放(不是真的缩放,只是表达方式上的缩放),换算比为 160。

1.5 sp

sp全称为scale-independent pixel。当文字尺寸是“正常”的时候: 1sp = 1dp;当文字“尺寸”比正常大的时候,1sp的实际像素 > 1dp实际像素,当比正常小的时候,1sp的实际像素 < 1dp实际像素。

换算公式:sp = dp * 缩放系数。

1.6 其他显示单位定义

  • px :pixel,像素,屏幕上组成一幅图画最基本单元

  • ppi: pixel per inch,每英寸上的像素数,该值越高,则屏幕显示越细腻

  • dp : 同dip,Density-independent pixel,安卓用的长度单位,1dp表示在屏幕像素点密度为160ppi时1px长度,若为320ppi,则为2px的长度

  • sp : scale-independent pixel,安卓用的字体大小单位。

  • pt : point,点,印刷行业常用单位,固定等于1/72英寸(用于印刷,纸质或非电子屏幕上,若用在电子屏幕上,等价于dp(等价于dp的时候,换算单位为72,pt = px /(ppi / 72))

  • dpi: dot per inch,每英寸多少点,该值越高,则图片越细腻(一般用于印刷,纸质或非电子屏幕上,若用在电子屏幕上,等价于ppi)

也就是说,在Android开发过程中,我们主要关注 px 、ppi、dp 与 sp。

二、资源限定符知识

各种限定符 资源适配,顾名思义,即通过不同资源适配不同屏幕尺寸。资源适配包括有:屏幕尺寸、屏幕密度、系统版本、屏幕方向、屏幕宽高比、限定符(屏幕最小宽度、屏幕可用宽度、屏幕可用高度)

使用方法:在具体资源目录后拼接 “-XXX”。(XXX表示为具体资源适配模式,例如 -land),

Tips:此顺序可能不是最新版本,只是举例,如需最新顺序版本,请查看官网Docs->guide->App Basics->App resources->Overview

拼接顺序如下

1.MCC 和 MNC
2.语言和区域
3.布局方向
4.smallestWidth
5.可用宽度
6.可用高度
7.屏幕尺寸
8.屏幕纵横比
9.圆形屏幕
10.屏幕方向
11.UI 模式
12.夜间模式
13.屏幕像素密度
14.触摸屏类型
15.键盘可用性
16.主要文本输入法
17.导航键可用性
18.主要非触摸导航方法
19.平台版本

2.1 屏幕尺寸

屏幕尺寸包括:

small		小屏幕
normal		基准屏幕
large		大屏幕
xlarge		超大屏幕
w x h		具体的 宽 x 高 的像素(一般在dimens文件下见得多)

具体表示写法:

1.res/layout/my_layout.xml
2.res/layout-small/my_layout.xml
3.res/layout-normal/my_layout.xml
4.res/layout-large/my_layout.xml 
5.res/layout-xlarge/my_layout.xml
6.res/layout-xlarge-land/my_layout.xml
7.res/values-1280x720/my_layout.xml

ps:此方式在3.2版本之后已被弃用,不建议使用。

2.2 屏幕密度(density)

屏幕密度包括:官网介绍地址Docs->guide->Devices->Device compatibility->Support different pixel densities

1.ldpi		<= 120dpi
2.mdpi		<= 160dpi
3.hdpi		<= 240dpi
4.xhdpi		<= 320dpi
5.xxhdpi	<= 480dpi
6.xxxhdpi	<= 640dpi
7.nodpi		与屏幕密度无关的资源.系统不会针对屏幕密度对其中资源进行压缩或者拉伸(例如drawable目录的selector或者.9.png、layout目录)
8.tvdpi		介于mdpi与hdpi之间,特定针对213dpi左右,专门为电视准备的,手机应用开发一般不需要关心这个密度值.
9.anydpi	任何密度

具体表示写法:

res/drawable/graphic.xml			(此时nodpi省略,一般放置xml或者.9图)
res/mipmap-xhdpi/my_icon.png
res/drawable-hdpi/graphic.png
res/mipmap-anydpi-v26/my_icon.xml	(一般都是svg矢量图所使用)

2.3 系统版本(version)

系统版本包括:

v19
v21
v23
etc..

具体表示写法:

res/drawable-v21/graphic.png
res/layout-v23/graphic.png

2.4 屏幕方向(Orientation)

land		横向
port		纵向

具体表示写法:

res/layout-land/my_layout.xml
res/layout-port/my_layout.xml

2.5 屏幕宽高比(基本不用)

long		比标准屏幕宽高比明显的高或者宽的这样屏幕
notlong		和标准屏幕配置一样的屏幕宽高比(一般都省略)

2.6 尺寸限定符

android3.2之后引入的,目前推荐使用的

2.6.1 屏幕最小宽度限定符(smallWidth)

表现形式			sw<N>dp
example			   res/layout-sw600dp/my_layout.xml
说明:
	屏幕的最小尺寸,就是屏幕可用区域的最小尺寸,是指屏幕可用高度或宽度的最小值(你可以默认是屏幕的最小宽度).你能用这个限定符确保,无论屏幕方
向如何,这个限定符修饰下的布局需要的屏幕最小尺寸是Ndp.例如,如果你的布局在运行时需要的最小屏幕宽度是600dp,则你可以利用这个限定符创建布局资
源目录res/layout-sw600dp.只有当屏幕的最小宽度或最小高度是600dp时,系统才会使用这些布局文件或者资源文件.最小屏幕宽度是固定设备的特有屏幕
尺寸,当屏幕方向发生变化时,设备的最小宽度值不变.设备的最小宽度值要考虑屏幕的尺寸和系统UI.例如,如果在屏幕上有一些系统持久化UI元素,则系统的
最小宽度值要比实现的屏幕尺寸小一些,因为这些系统的UI元素你的应用是无法使用到的.当你使用之前的广义限定符是,你可以定义连续的一系列限定符.用最
小宽度来决定广义屏幕尺寸是有意义的,是因为宽度是影响你UI设计的关键因.UI在竖直方向上会经常滚动,但是在水平方向上往往是固定的布局.可见不论
是适配手机或者平板,宽度往往都是布局的关键因素.因此,你需要关心你手机上的最小宽度值.

简单来说,这个宽度无关方向,是取屏幕物理上的最小值,

Tips: 在使用过程中,计算出来的ppi建议修正为系统认可的屏幕分辨率ppi(ldpi,xxhdpi等对应的值),这样可以尽量避免额外适配文件的创建,和Apk大小的增大,修正规律:取最小大于算出来的ppi,例如算出286ppi,取值320ppi,这里为什么取最小的最大值,看看1.4中的公式。

example:
	某安卓手机屏幕参数为:5.0 寸,1920 * 1080。那么他的 ppi(dpi) = sqrt{1920^2 + 1080^2}/5 = sqrt{4852800}/5 ≈  2202.91/5 
≈ 440.58 ppi。
	但在实际计算的时候,参考本文2.2屏幕密度,440.58 最大能够小于 480 dpi,所以实际取值ppi的时候取值 480ppi 。
计算最小宽度限定值 N(dp) 。
	根据定义,公式 dp = px * 160 /ppi , N = px * 160 /ppi(此时px=最小宽度像素1080,ppi=480)= 1080 * 160 / 480 = 360(dp)。
所以最后命名为:
	res/layout-sw360dp/my_layout.xml

2.6.2 屏幕可用宽度限定符

表现形式			w<N>dp
example			   res/layout-w720dp/my_layout.xml
说明:
指定资源使用时需要的最小宽度.当屏幕方向发生变化时,系统会调整这个值,使其始终为你UI显示的宽度.这个属性经常被用来判断当前是否需要显示多屏布
局,因为哪怕用户当前正在使用平板,你也可能不希望用户在平板竖屏时显示多个屏幕的布局样式.这时,你就可以使用这个限定符来标明你布局需要的最小宽,
而无需同时使用屏幕尺寸和方向限定符。

简单来说,这是方向上的宽度,可能会变化。

2.6.3 屏幕可用高度限定符

表现形式			h<N>dp
example			   res/layout-h720dp/my_layout.xml
说明:
指定资源应该使用的最小屏幕高度(dp 单位) — 由 <N> 值定义。当屏幕的方向在横屏与竖屏之间切换时,系统 对应的高度值将会变化,以 反映 UI 可
用的当前实际高度。使用此方式定义 布局需要的高度很有用,它与使用 w<N>dp 定义 所需宽度的方式相同,无需同时使用屏幕尺寸和方向限定符。 但大多
数应用不需要此限定符,考虑到 UI经常垂直滚动, 因此高度更弹性,而宽度更刚性

三、屏幕适配(官方推荐

1).在 XML 布局文件中指定尺寸时使用 wrap_content、match_parent 和 dp、sp 尺寸单位 。
2).不要在应用代码中使用硬编码的像素值(尽量使用dp、sp)
3).不要使用 AbsoluteLayout(已弃用)
4).使用RelativeLayout(稍微提下性能:使用减少布局层次的ViewGroup,例如ConstraintLayout,布局层次最好不要超过10层)。
5).使用资源限定符(尺寸、方向等,详见二章)
6).使用尺寸和密度特定资源(xhdpi,xxhdpi,etc)
7).使用.9图

1..9图参考1.9图参考2、[.9图官方说明](developer.android.google.cn/guide/topic…)

2.SVG图参考1、SVG图官方说明

Tips: 使用LinearLayout中的权重,在源码中,会测量计算两次,影响性能,权重是取剩余空间的分配,第一次需要先计算剩余空间,第二次分配才会完成

四、今日头条适配方案(原文链接

4.1.问题阐述

UI 设计图是按屏幕宽度为360dp来设计的,在分辨率1920 * 1080,尺寸为5寸的屏幕上,ppi = sqrt{1920^2 + 1080^2}/6 ≈ 440,那么实际宽度 dip(dp) = px/(ppi/160) = 1080/(440/160) ≈ 392dp,也就是说实际屏幕比设计宽度大。那么就造成按照设计尺寸也无法在不同设备上显示同样的效果的。如果屏幕宽度不足360dp,会造成显示不全等各种问题。

4.2 需求

  • 支持以宽或者高一个维度去适配,保持该维度上和设计图一致。

  • 支持dp和sp单位

4.3 突破口

dp的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换。 源码如下:

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;
}

上面源码中用到的 DisplayMetrics 正是从 Resources 中获取的。

源码中dp和px的转换公式 :px = dp * density。也就是说如果设计图宽为360dp,想要保证在所有设备计算得出的px值都正好是屏幕宽度的话,我们只需要修改 density 的值。

DisplayMetrics 中和适配相关的几个变量

  • DisplayMetrics#density(就是与基准屏幕缩放比,值等于ppi/160,详见1.3)。

  • DisplayMetrics#densityDpi(实际就是上文说的ppi,详见1.3、1.4、4.1)

  • DisplayMetrics#scaledDensity(字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值,详见1.5)

4.4 实现

4.4.1 DisplayMetrics#density计算

假设UI 设计宽度为1080px,density为3,总宽度为360dp。现需要使用在宽度为720px的设备上,为了让目标设备的总宽度为360dp,根据公式 px = dp * density(density = ppi / 160) ,那么,目标 density = px / dp = 720/360 = 2。

得出: DisplayMetrics#density = 屏幕宽度(px) / 设计宽度dp。

4.4.2 DisplayMetrics#densityDpi计算

android中的dpi其实就是ppi,所以 DisplayMetrics#densityDpi = DisplayMetrics#density * 160 。

4.4.2 DisplayMetrics#scaledDensity计算

scaledDensity 是 字体缩放比例因子,通常情况下和 density 相等,调节后,这个缩放因子会改变。不管这个比例因子是多少,我们通过拿到之前的比例,和现在的值,就可以获取到现在的缩放因子的值。

所以 DisplayMetrics#scaledDensity = 开始的appDisplayMetrics.scaledDensity / 开始的appDisplayMetrics.density 。

4.4.5 字体缩放因子监听

在应用运行期间,用户在系统设置中,改变字体大小,缩放因子会变化,但是应用不知道已经发生变化,导致字体大小还是原来的大小。监听缩放因子变化代码如下。

application.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(Configuration newConfig) {
                if (newConfig != null && newConfig.fontScale > 0) {
                    DisplayMetrics.scaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                }
            }

            @Override
            public void onLowMemory() {

            }
        });

4.5 核心代码

private static float sSysDensity;
private static float sSysScaledDensity;
public void setCustomDensity(@NonNull Activity aty, @NonNull final Application application) {
    final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
    if (sSysDensity == 0) {
        sSysDensity = appDisplayMetrics.density;
        sSysScaledDensity = appDisplayMetrics.scaledDensity;
        application.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(Configuration newConfig) {
                if (newConfig != null && newConfig.fontScale > 0) {
                    sSysScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                }
            }
            @Override
            public void onLowMemory() {
            }
        });
    }
    final float targetDensity = appDisplayMetrics.widthPixels / 360//(暂定宽度为360,也可以改为高度方向等);
    final float targetScaleDensity = targetDensity * (sSysScaledDensity / sSysDensity);
    final int targetDensityDpi = (int) (160 * targetDensity);
    appDisplayMetrics.density = targetDensity;
    appDisplayMetrics.scaledDensity = targetScaleDensity;
    appDisplayMetrics.densityDpi = targetDensityDpi;

    final DisplayMetrics atyDisplayMetrics = aty.getResources().getDisplayMetrics();
    atyDisplayMetrics.density = targetDensity;
    atyDisplayMetrics.scaledDensity = targetScaleDensity;
    atyDisplayMetrics.densityDpi = targetDensityDpi;

在Activity#onCreate中使用就好了。

4.6 优点

  • 1). 开发成本低,操作极其简单,使用方式较之前无异。

  • 2). 侵入性极其低,若早前项目按照google推荐使用dp,几乎无需更改任何代码。

  • 3). 因为更改的是全局性的,因此所有控件都适配。

  • 4). 性能好,几乎无性能损耗,损耗的也就代码调用执行,目前的主要流行设备时间花费1ms不到。

4.7 缺点

全局性改变density,所有控件都适配,当某个控件(3方或系统)的设计尺寸与我们自身设计尺寸差距较大时,例如其他设计总尺寸为100dp,控件设计尺寸为10dp,控件占比10%,而我们设计尺寸为50dp,那么他的控件用在我们设计上,占比为20%。就会造成显示效果差异极大。

4.8 缺点解决方案

针对3方设计尺寸与自身设计尺寸的差异:1).可以更改自身设计尺寸,改为3方设计尺寸中折中的尺寸;2).若能拿到3方全部源码,且能更改,将3方设计尺寸改为自身设计尺寸;3).取消此使用此3方控件的适配;

Tips:尽量使用dp、sp 作为单位,养成好的习惯,会成功避免不必要的麻烦。

五、屏幕适配全方案

  • 1.在使用差异较小、可以容许视觉效果差的3方控件布局界面中,仅仅使用今日头条方案即可。

  • 2.在有较大差异的3方控件界面中,不使用今日头条方案,自身UI尺寸匹配3方折中的尺寸,具体做法可以使用资源限定符,在不可容差的范围外的布局尺寸中,匹配3方设计尺寸。