高级UI<第三十一篇>:屏幕适配

屏幕适配问题算是Android让人比较头疼的问题之一了,由于屏幕分辨率的碎片化增多,导致屏幕适配越来越繁琐,屏幕适配的难度也在不断加深。目前,又出现一些异形机,比如刘海屏、滴水屏、曲面屏。

屏幕适配的基础

在学习屏幕适配之前,必须了解屏幕适配的一些基本概念。

【屏幕尺寸】

什么叫屏幕尺寸?屏幕尺寸是指屏幕对角线的长度,单位是英寸

看下图:

图片.png

上图标注的对角线就是屏幕的尺寸,单位为英寸,英寸和厘米的换算如下:

1英寸(in)=2.54厘米(cm)

【屏幕分辨率】

屏幕分辨率是指纵向和横向的像素点数,单位为px,1px=1个像素。假设纵向分辨率为1920,横向分辨率为1080,我们就说,该屏幕的分辨率是 1920 * 1080,也就是纵向分辨率 * 横向分辨率。横向分辨率就是屏幕的宽度,纵向分辨率就是屏幕的高度。

如图:

图片.png

上图的标注就是屏幕的分辨率。

【像素密度】

像素密度是指每英寸上的像素点数,单位是dpi,即dot per inch的缩写。

屏幕像素密度与屏幕尺寸和分辨率息息相关,现在出一道题目:

假设某手机出厂时,屏幕分辨率是1920*1080,6英寸,求其像素密度(dpi)?

根据像素密度的概念,我们只需要将屏幕的对角线分成6等份,求出每等份的像素点数。如图:

图片.png

上图中已经对角线分成6等份,我们只需要求出每一段的像素点数即可。

DPI = Math.sqrt(1920 * 1920 + 1080 * 1080) / 6 = 367.15(dpi)

【dp、sp、px之间的关系】

  • px:构成像素的最小单位,屏幕分辨率的单位就是px。

  • dp也叫dip,是Desity Independent pixels的缩写,即密度无关像素。

为了防止dpi和dip混淆,我在下文中就使用dp了。

  • sp:Independent pixels的缩写,可以根据文字大小首选进行缩放。(Google推荐,使用sp时,数值为偶数)

dp是项目中常用的屏幕单位,sp是项目中常用的文字单位,它们和px互转的公式如下:

dp(sp)转px

    float pxValue  = 屏幕像素密度/160 * dpValue;
    float pxValue  = 屏幕像素密度/160 * spValue;

px转dp(sp)

    float dpValue  = 屏幕像素密度/160 / pxValue;
    float spValue  = 屏幕像素密度/160 / pxValue;

在Android中,可以通过DisplayMetrics获取density值,这个值我把他称之为屏幕像素密度因子

公式如下:

displayMetrics.density = 屏幕像素密度/160;

160是什么鬼?160是像素密度,单位是dpi,即160dpi,160dpi是手机的适配基准。

题目:小明买了一台320dpi屏幕的手机,请问该手机像素密度因子是多少?

scale = 320dpi  / 160dpi = 2(倍)

 displayMetrics.density = 2

由此可见,px、sp、dp的互转公式需要调整一下,如下:

dp(sp)转px

    float pxValue  = scale  * dpValue;
    float pxValue  = scale  * spValue;

px转dp(sp)

    float dpValue  = scale  / pxValue;
    float spValue  = scale  / pxValue;

在项目中,那些单位很少使用除int之外的类型,所以需要将float转成int,假设现在有两个浮点数:1.1和1.9,那么强转成int类型的值都是1,那是不可取的。

为了类型转换的精准度,我们采用四舍五入法,最终的公式为:

dp(sp)转px

    int pxValue  = Math.round(scale * dpValue);
    int pxValue  = Math.round(scale * spValue);

px转dp(sp)

    int dpValue  = Math.round(scale / pxValue);
    int spValue  = Math.round(scale / pxValue);

当然,一些资料上说到的公式是这样的:

dp(sp)转px

    int pxValue  = (int)(scale * dpValue + 0.5f);
    int pxValue  = (int)(scale * spValue + 0.5f);

px转dp(sp)

    int dpValue  = (int)(scale / pxValue + 0.5f);
    int spValue  = (int)(scale / pxValue + 0.5f);

在后面加0.5之后再强转成int的做法其实就是四舍五入,和使用Math.round是一样的。

【DisplayMetrics】

DisplayMetrics displayMetrics = getResources().getDisplayMetrics();

以上代码可以获取DisplayMetrics对象,这个对象中存入了有关屏幕的相关信息,具体信息如下:

    displayMetrics.widthPixels;//屏幕的宽度,即横向分辨率
    displayMetrics.heightPixels;//屏幕的高度,即纵向分辨率
    displayMetrics.scaledDensity;//文字像素密度(单位sp的像素密度)
    outMetrics.densityDpi;//一般像素密度(单位dp的像素密度)
    displayMetrics.density;//屏幕像素密度的缩放因子

其中scaledDensityscaledDensity就是我们屏幕适配中经常用到的像素密度,在上文中,我们需要知道屏幕的分辨率和尺寸才能求出像素密度,在实际开发中,我们并不知道手机屏幕的尺寸,需要像素密度和分辨率反推出屏幕的尺寸。

屏幕尺寸 = Math.sqrt(displayMetrics.widthPixels * displayMetrics.widthPixels + displayMetrics.heightPixels * displayMetrics.heightPixels) / displayMetrics.densityDpi

【mdpi、hdpi、xdpi、xxdpi、xxxdpi如何计算和区分?】

当我们新建项目时,肯定会自定生成以下目录:

图片.png

这些文件夹都是IDE自动生成的,这些文件夹的目的是为了做图片适配。

就以适配Launcher图标为例,我将它的适配整理成一个表格,如下:

名称 像素密度范围 图片大小(px) 图片大小(dp)
ldpi 0dp~120dp 36×36px 48dp
mdpi 120dp~160dp 48×48px 48dp
hdpi 160dp~240dp 72×72px 48dp
xhdpi 240dp~320dp 96×96px 48dp
xxhdpi 320dp~480dp 144×144px 48dp
xxxhdpi 480dp~640dp 192×192px 48dp

后来,随着屏幕适配的变迁,Google认为适配ldpi已经没有必要了,在Android Studio中新建项目没有自动生成mipmap-ldpi文件夹就是最好的证据,当然,我们自己可以新建一个mipmap-ldpi也是可以的,比如:

图片.png

不过,个人认为,ldpi已经没必要适配了,为了让图片更加完美适配,Google新增了mipmap-anydpi-v26文件夹,在Android8.0手机上的图标可以自适应,mipmap-anydpi-v26文件夹中的图标一般存放矢量图,矢量图的格式一般是.svg,在Android中用.xml表示。

所以,适配表需要改进一下,如下:

名称 像素密度范围 图片大小(px) 图片大小(dp)
mdpi 0~160dpi 48×48px 48dp
hdpi 160dpi~240dpi 72×72px 48dp
xhdpi 240dpi~320dpi 96×96px 48dp
xxhdpi 320dpi~480dpi 144×144px 48dp
xxxhdpi 480dpi~640dpi 192×192px 48dp
anydpi-v26 和像素密度无关(Android 8.0以上生效) 和像素无关 48dp

矢量图和位图不一样,矢量图永远不会失真,无论图片设置的多大。但是,位图不一样,位图的是由像素组成,如果一张图片的分辨率是48*48,将这张图片大小设置成100dp,那么这张图片在屏幕上显示必然会失真,为了防止失真现象,我们需要对图片进行适配了。

名称 像素密度范围 图片大小(px) 图片大小(dp) 比例
mdpi 0~160dpi 48×48px 48x48dp 1
hdpi 160dpi~240dpi 72×72px 48x48dp 1.5
xhdpi 240dpi~320dpi 96×96px 48x48dp 2
xxhdpi 320dpi~480dpi 144×144px 48x48dp 3
xxxhdpi 480dpi~640dpi 192×192px 48x48dp 4
anydpi-v26 和像素密度无关(Android 8.0以上生效) 和像素无关 48x48dp 不需要适配

在上表中,位图之间的大小比例已经标注,这个数据是怎么计算的呢?

我们知道,mdpi最大支持的像素密度是160dp,hdpi最大支持的像素密度是240dp,xhdpi最大支持的像素密度是320dp,xxhdpi最大支持的像素密度是480dp,xxxhdpi最大支持的像素密度是480dp,xxhdpi最大支持的像素密度是640dp,它们的比例计算如下:

160dp : 240dp : 320dp : 480dp : 640dp = 1 : 1.5 : 2 : 3 : 4

上表中所示,假设在代码中将图片设置为48dp,那么不同像素密度的手机需要的图片大小是不一样的,为了保证图片完全不失真,需要按照规定对图片进行适配。

上表中的图片大小是适配只是举例,在大型项目中,我们需要先适配完一种像素密度的机型,保证图片在某机型上不会失真,在这样的前提下,开始按照比例适配。

【举例】

假设小明的手机屏幕的像素密度是250dpi,代码中设置图片的大小为 x dp,怎么适配图片保证不失真?

答:250dpi对应的文件夹是xhdpi(mipmap-xhdpi),需要将图片首先放入xhdpi文件夹,如果图片在屏幕显示明显失真,那么请更换更大的图片,直到看起来不失真为止,接下来就简单了,按照 1 : 1.5 : 2 : 3 : 4的比例将不同分辨率的图片放入对应的文件夹中。

另外,是有关内存消耗问题的适配,这个问题很重要,我必须补充一下,就以Launcher图标适配为例。
假设小明将一张1000x1000px的图片分别放入mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi文件夹中,这样就可以保证不失真了,小明觉得很对。
其实,小明的做法是错误的,图片适配不仅仅是防止图片失真,还要防止内存过多的浪费,那么怎么才能防止因为图片导致内存过多浪费呢?

在Android Studio中的Image Asset给出了答案,当制作一张Launcher图标时,给出了一些标准,如图:

图片.png
图片.png
图片.png

更多的适配我就不发了,我们发现那些有关图片大小的适配正好对应了上表,下面我将针对因为图片的错误适配导致内存过多浪费的问题重新整理出一张表,如下:

名称 像素密度范围 大小(px) 大小(dp) 比例
mdpi 0~160dpi 1px 1dp 1
hdpi 160dpi~240dpi 1.5px 1dp 1.5
xhdpi 240dpi~320dpi 2px 1dp 2
xxhdpi 320dpi~480dpi 3px 1dp 3
xxxhdpi 480dpi~640dpi 4px 1dp 4
anydpi-v26 和像素密度无关(Android 8.0以上生效) 和像素无关 48x48dp 不需要适配

我想表达的意思都体现在了上表,那么我就提出问题了。

问题:假设小明手机的屏幕的像素密度是250dpi,代码中某ImageView控件设置的大小为300dpx400dp,那么请问mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi文件夹中放置的图片最适合的大小分别是什么?需要保证不失真且不造成没必要的内存浪费。

答:250dpi对应的文件夹是xhdpi,根据表格上的比例运算,应该在xhdpi文件夹中方式的图片大小为:

300x2px * 400x2px = 600 * 800px

也就是说,xhdpi文件夹中的图片分辨率一定是600 * 800px。

依次类推,不同文件夹中所放置图片的分辨率分别是:

mdpi:300 * 400px
hdpi:450* 600px
xhdpi:600 * 800px
xxhdpi:900 * 1200px
xxxhdpi:1200 * 1600px

当然,项目的需求可能会改变,领导要求图片再小一点,改成200x300px,那么,如果您不调整本地图片的大小,必然会造成没必要的内存浪费。如果领导要求图片再大一点,改成400x500px,那么如果您不调整本地图片的大小,那么图片必然会失真。

对布局做适配(比较简单,稍微说一下)

常见的布局有:LinearLayout(线性布局)、RelativeLayout(相对布局)、ConstraintLayout(约束布局),合理的使用这些布局对屏幕适配有一定的作用,另外match_parent、wrap_content、weight使用也和布局的适配有关。

根据分辨率适配(采用px适配)(可以不用看,这种方案是要被排除的)

在古老的故去,有这样一段代码,如下:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;

public class MakeXml {
    private final static String rootPath = "D:\\layout\\values-{0}x{1}\\";

    private final static float dw = 320f;
    private final static float dh = 480f;

    private final static String WTemplate = "<dimen name=\"x{0}\">{1}px</dimen>\n";
    private final static String HTemplate = "<dimen name=\"y{0}\">{1}px</dimen>\n";

    public static void main(String[] args) {
        makeString(320, 480);
        makeString(480, 800);
        makeString(480, 854);
        makeString(540, 960);
        makeString(600, 1024);
        makeString(720, 1184);
        makeString(720, 1196);
        makeString(720, 1280);
        makeString(768, 1024);
        makeString(800, 1280);
        makeString(1080, 1812);
        makeString(1080, 1920);
        makeString(1440, 2560);
    }

    public static void makeString(int w, int h) {

        StringBuffer sb = new StringBuffer();
        sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        sb.append("<resources>");
        float cellw = w / dw;
        for (int i = 1; i < 320; i++) {
            sb.append(WTemplate.replace("{0}", i + "").replace("{1}",
                    change(cellw * i) + ""));
        }
        sb.append(WTemplate.replace("{0}", "320").replace("{1}", w + ""));
        sb.append("</resources>");

        StringBuffer sb2 = new StringBuffer();
        sb2.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        sb2.append("<resources>");
        float cellh = h / dh;
        for (int i = 1; i < 480; i++) {
            sb2.append(HTemplate.replace("{0}", i + "").replace("{1}",
                    change(cellh * i) + ""));
        }
        sb2.append(HTemplate.replace("{0}", "480").replace("{1}", h + ""));
        sb2.append("</resources>");

        String path = rootPath.replace("{0}", h + "").replace("{1}", w + "");
        File rootFile = new File(path);
        if (!rootFile.exists()) {
            rootFile.mkdirs();
        }
        File layxFile = new File(path + "lay_x.xml");
        File layyFile = new File(path + "lay_y.xml");
        try {
            PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
            pw.print(sb.toString());
            pw.close();
            pw = new PrintWriter(new FileOutputStream(layyFile));
            pw.print(sb2.toString());
            pw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }

    public static float change(float a) {
        int temp = (int) (a * 100);
        return temp / 100f;
    }

}

将横向屏幕分成320等份,将纵向屏幕分成480等份,使用javac命令编译和java命令执行,生成以下文件夹:

图片.png

随便打开一个文件夹,里面有两个文件:

图片.png

xml文件中自动生成一些数值

图片.png

那些数值是以px为单位,使用方法如下:

【一】 将生成的文件夹放入res文件夹目录下

图片.png

【二】 在布局中使用

图片.png

使用也比较简单。在demo中,已经适配了好多种分辨率的机型,如果当前机型的分辨率在项目中没有适配,那么就寻找分辨率小于实际分辨率且最接近实际分辨率的适配,即

    找到的分辨率 <= 实际分辨率,且找到的分辨率最接近实际分辨率

这种屏幕适配方式固然比较好,但是我们必须考虑具有虚拟按键的屏幕,部分手机底部有虚拟键盘,由于虚拟按键占用了一部分屏幕,所以这种适配方式其实并不准确。为什么呢?

假设屏幕本来的分辨率是1920 * 1080,在底部加上虚拟按键之后,屏幕的分辨率变成了1870 * 1080,这样我们还必须要针对1870 * 1080的分辨率做下适配,由于各种手机的虚拟按键的高度不一定一致,所以这种适配方式受到了屏幕高度的影响。

那么,问题来了,市面上有没有一种方式可以忽视屏幕高度的屏幕适配方案?

答案:有的,采用限定符方式适配。

限定符适配方案

限定符适配方案采用dp单位适配。

(1)尺寸限定符

尺寸限定符已经被废弃,目前不推荐使用,不过我们还需要知道有这个东西。

限定符 描述
small 小屏幕
normal 基准屏幕
large 大屏幕
xlarge 超大屏幕

在Android 3.2以前,屏幕适配一般只需要考虑小屏幕、基准屏幕、大屏幕、超大屏幕,它的使用也比较简单,如下:

res/layout/my_layout.xml              // 基准屏幕,默认
res/layout-small/my_layout.xml              // 小屏幕
res/layout-normal /my_layout.xml              // 基准屏幕
res/layout-large/my_layout.xml        // 大屏幕
res/layout-xlarge/my_layout.xml       // 超大屏幕
res/layout-xlarge-land/my_layout.xml  //超大屏幕-横屏

(2)最小宽度限定符

在Android 3.2之后,Google为了更加精准的适配,推出了最小宽度限定符

如:

res/layout-sw600dp/main.xml

sw是small width的简称,那么600dp是什么意思呢?

600dp就是宽度,它实际匹配的屏幕宽度为:600 * 像素密度缩放因子,即

实际宽度 = 600 * displayMetrics.density

您可以在Android Studio中下载ScreenMatch插件

图片.png

下载并安装之后,鼠标右击项目,弹出一个对话框

图片.png

选择ScreenMatch,最终自动生成一堆含有sw字样的文件夹,如图:

图片.png

sw的中文意思是:最小宽度。所以sw后面的数字大小就是宽度值,单位是dp。dp这个值在不同手机上显示的实际宽度不一致,您不一定一下子了解这个最小宽度的概念,那么我来举一个例子。

【举例】

面试官问小明,这个values-sw600dp中的600dp实际大小是多少呢?

答:所谓实际大小就是以px为单位的大小,即像素个数,这里的600dp只是Android屏幕上显示的大小,不代表实际大小,那么就需要将dp转化为px,公式如下:

 float 600dp = 600 * 像素密度的缩放因子 =  600 *  displayMetrics.density

关于displayMetrics.density到底等于多少,有些手机是1,有些手机是2,有些手机是3,有些手机是4,甚至在不久的将来还会出现像素密度缩放因子为5、6、7完全有可能,但是目前市场上像素密度缩放因子最多的是2和3。

假设像素密度缩放因子为2,那么

 float 600dp = 600 * 2 = 1200px,即可以适配宽度为1200px的屏幕

假设像素密度缩放因子为3,那么

 float 600dp = 600 * 3 = 1800px,即可以适配宽度为1800px的屏幕

使用ScreenMatch插件后,会自动生成配置文件screenMatch.properties,配置信息如下:

############################################################################
# Start with '#' is annotate.                                              #
# In front of '=' is key, cannot be modified.                              #
# More information to visit:                                               #
#   http://blog.csdn.net/fesdgasdgasdg/article/details/52325590            #
#   http://download.csdn.net/detail/fesdgasdgasdg/9913744                  #
#   https://github.com/mengzhinan/PhoneScreenMatch                         #
############################################################################
#
# You need to refresh or reopen the project every time you modify the configuration,
# or you can't get the latest configuration parameters.
#
#############################################################################
#
# Base dp value for screen match. Cut the screen into [base_dp] parts.
# Data type is double. System default value is 360.
# I advise you not to modify the value, be careful !!!!!!!!! _^_  *_*
base_dp=360
# Also need to match the phone screen of [match_dp].
# If you have another dp values.
# System default values is 384,392,400,410,411,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365
match_dp=
# If you not wanna to match dp values above. Write some above values here, append value with "," .
# For example: 811,961,1365
ignore_dp=
# They're not android module name. If has more,split with , Symbol.
# If you set, it will not show in SelectDialog.
# If you have, write here and append value with "," .
# For example: testLibrary,commonModule
# System default values is .gradle, gradle, .idea, build, .git
ignore_module_name=
# Use which module under the values/dimen.xml file to do the base file,
# and generated dimen.xml file store in this module?
# Default value is 'app'.
match_module=app
# Don't show select dialog again when use this plugin.
# System screen match will use the last selected module name or default module name.
# You can give value true or false. Default value is false.
not_show_dialog=false
# Do you want to generate the default example dimens.xml file?
# In path of .../projectName/screenMatch_example_dimens.xml, It does not affect your project code.
# You can give value true or false. Default value is false.
not_create_default_dimens=false
# Does the font scale the same size as the DP? May not be accuracy.
# You can give value true or false. Default value is true. Also need scaled.
is_match_font_sp=true
# Do you want to create values-wXXXdp folder or values-swXXXdp folder ?
# I suggest you create values-swXXXdp folder,
# because I had a problem when I was working on the horizontal screen adapter.
# values-swXXXdp folder can solve my problem.
# If you want create values-swXXXdp folder, set "create_values_sw_folder=true",
# otherwise set "create_values_sw_folder=true".
# Default values is true.
create_values_sw_folder=true

以上配置中,有三个必须了解(其它的您可以自己查些资料去了解)

  • match_dp:匹配的最小宽度值

添加数值,可以匹配任意最小宽度值,如

match_dp=110

这样,除了默认宽度之外,还会多创建一个values-sw110dp的文件夹。

  • ignore_dp:忽视的最小宽度值

这个比较容易理解,如:

ignore_dp=110

那么values-sw110dp文件夹不会被创建。

  • base_dp:基准dp值

在默认情况下,基准dp值为360dp,什么意思呢?如图:

图片.png

这个values文件夹就是基准文件夹,相当于values-sw360dp命名的文件夹。当然,这个属性您是可以随意更改的。但是,ScreenMatch插件将360dp设置为基准,最小宽度的计算方法如下:

最小宽度 = 屏幕宽度 / 屏幕像素密度的缩放因子 = displayMetrics.widthPixels / displayMetrics.density

这里的最小宽度其实就是屏幕宽度

以上的计算公式需要牢记,因为当您做适配的时候需要以上公式计算出您当前设备的屏幕宽度是多少,根据当前设备的屏幕宽度与对应的dimens.xml文件调整大小。

ScreenMatch插件默认的最小宽度值有360,384,392,400,410,411,432,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365,我适配了N个手机设备,计算出最小宽度值在320dp~411dp之间,令我惊讶的是,除了320dp的屏幕之外,剩下了最小宽度值与实际设备几乎完全一致,所以总结出了ScreenMatch基本适配方案:

(1)牢记以下公式

    最小宽度 = 屏幕宽度 / 屏幕像素密度的缩放因子 = displayMetrics.widthPixels / displayMetrics.density

根据公式,计算出您当前手机的最小宽度,并将这个值设置为基准值,假设您当前设备的最小宽度为360dp,那么配置如下:

    base_dp=360

有一点需要注意,您当前设备的最小宽度可能不是整数,假设计算出的最小宽度为411.1234dp,这种情况采用去尾法,将411dp设置为基准值,如:

    base_dp=411

(2)将match_dp的值设置为320,即

    match_dp=320

这样做的话ScreenMatch插件会多适配一种机型,也是ScreenMatch插件遗漏的机型。

(3)目前ScreenMatch插件适配的最小宽度有:

360,384,392,400,410,411,432,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365,加上遗漏的320

由于大部分手机设备的宽度大致在320dp~480dp之间,您可以将其他的适配忽视,配置如下:

base_dp=360(根据您当前设备的最小宽度而定)
match_dp=320
ignore_dp=533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365

533~1365之间的宽度简直大的离谱,这让我想起公司买了一个比我家电视屏幕还大的液晶显示屏,为了适配公司的产品,我想,为了在将来适配超大屏幕的设备,还是不要忽视那些最小宽度值吧,配置如下:

base_dp=360(根据您当前设备的最小宽度而定)
match_dp=320
ignore_dp=

(4)您可能会遇到这种情况,今天使用360dp的屏幕适配,明天使用411dp的屏幕适配,说不定设备不小心被偷了,所以换成了320dp的手机,像这种情况请不要慌张,马上给出解决方案:

    修改base_dp值,将base_dp设置为当前设备的宽度,然后将所有自动生成的含有sw字符的文件夹中的dimens.xml文件全部删除,最后使用ScreenMatch插件重新自动生成一次。

也许,您还是不怎么理解?那么我来整理一个表格吧,假设ScreenMatch插件默认匹配的最小宽度值有:320,360,384,392,400,410,411,432,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365,并且像素密度缩放因子为2或者3(因为市场上大部分像素密度缩放因子为2和3),那么,表格如下:

文件夹名 像素密度缩放因子为2和3时对应的px值 像素密度缩放因子为2和3时适配屏幕宽度范围
values-sw320dp 640px 和 960px 0~640px 和 0~960px
values-sw360dp 720px 和 1080px 640px ~720px 和 960px~1080px
values-sw384dp 768px 和 1152px 720px ~ 768px 和 1080px~1152px
values-sw392dp 784px 和 1176px 768px~784px 和 1152px~1176px
values-sw400dp 800px 和 1200px 784px~800px 和 1176px~1200px
values-sw410dp 820px 和 1230px 800px~820px 和 1200px~1230px
values-sw411dp 822px 和 1233px 820px~822px 和 1230px~1233px
values-sw432dp 864px 和 1296px 822px~864px 和 1233px~1296px
values-sw480dp 960px 和 1440px 864px~960px 和 1296px~1440px
values-sw533dp 1066px 和 1599px 960px~1066px 和 1440px~1599px
values-sw592dp 1184px 和 1776px 1066px~1184px 和 1599px~1776px
values-sw600dp 1200px 和 1800px 1184px~1200px 和 1776px~1800px
values-sw662dp 1324px 和 1986px 1200px~1324px 和 1800px~1986px
values-sw720dp 1440px 和 2160px 1324px~1440px 和 1986px~2160px
values-sw768dp 1536px和 2304px 1440px~1536px和 2160px~2304px
values-sw800dp 1600px 和 2400px 1536px~1600px 和 2304px~2400px
values-sw811dp 1622px 和 2433px 1600px~1622px 和 2400px~2433px
values-sw820dp 1640px 和 2460px 1622px~1640px 和 2433px~2460px
values-sw960dp 1920px 和 2880px 1640px~1920px 和 2460px~2880px
values-sw961dp 1922px 和 2883px 1920px~1922px 和 2880px~2883px
values-sw1024dp 2048px 和 3072px 1922px~2048px 和 2883px~3072px
values-sw1280dp 2560px 和 3840px 2048px~2560px 和 3072px~3840px
values-sw1365dp 2730px 和 4095px 2560px~2730px 和 3840px~4095px

另外,需要补充的是,最小宽度屏幕适配,即可以适配dp,也可以适配sp,所以,dimens文件中的数值一般都是dp和sp混杂,如:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    //视图大小
    <dimen name="aaa">26dp</dimen>

    //文字大小
    <dimen name="bbb">26sp</dimen>

</resources>

(3)布局别名

布局别名适配方案是针对布局的适配方案,而非针对px、sp或dp单位。

【方法一】

res/layout/activity_main.xml:布局1
res/layout-sw360dp/activity_main.xml:布局2
res/layout-sw392dp/activity_main.xml:布局2

【方法二】

(1)加载布局

setContentView(R.layout.main);

(2)在 values-sw360dp文件夹新建任意名称的文件,比如layout.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="main" type="layout">@layout/activity_main</item>
</resources>

(2)在 values-sw392dp文件夹新建任意名称的文件,比如layout.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="main" type="layout">@layout/activity_main_2</item>
</resources>

(3)两个布局

layout/activity_main.xml
layout/activity_main_2.xml

(4)屏幕方向限定符

屏幕方向有:横屏、竖屏,这种适配方式是针对布局的适配方案,而非针对px、sp或dp单位。

【方法一】

图片.png
图片.png

图片.png
图片.png

【方法二】

(1)加载布局

setContentView(R.layout.main);

(2)在values-port(values-sw360dp-port)文件夹新建任意名称的文件,比如layout.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="main" type="layout">@layout/activity_main</item>
</resources>

(2)在values-land(values-sw392dp-land)文件夹新建任意名称的文件,比如layout.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="main" type="layout">@layout/activity_main_2</item>
</resources>

(3)两个布局

layout/activity_main.xml
layout/activity_main_2.xml

自动拉伸位图

自动拉伸位图,即使用Nine-Patch类型的图片,图片可以自动拉伸而不会变形。

现在准备一张背景图片,如下:

chat.png

将这张图片设置为TextView的背景,效果如下:

图片.png

很明显,实在是太难看了,背景图片不仅变形了,而且左 、上、右、下四个方位的位置都不是很理想,为了解决这个问题,我们需要将这张.png图片转换为.9.png图。

Android Studio可以将普通图片转换为Nine-Patch类型的图片,步骤如下:

【第一步】

选中指定的png图片,鼠标右击,选择Greate 9-Patch file...

图片.png

【第二步】

选择存放的目录

图片.png

【第三步】

双击生成的.9图片,并拉动下图所示的四条线,设置图片水平和垂直方向的padding

图片.png

这样,最终的效果如下:

图片.png

百分比屏幕适配

百分比屏幕适适配,即将子view的宽度设置为屏幕宽度的百分比,或将高度设置为屏幕高度的百分比。

当时Google官方提供了一个百分比屏幕适配的库:PercentRelativeLayoutPercentFrameLayout,但是它的生命是短暂的,在API 24被引用,API 26被废弃。

这个比较简单,相当于线性布局的权重(weight),除了线性布局的权重可以设置百分比外,约束布局ConstraintLayout也可以完美实现百分比适配,比如以下配置:

app:layout_constraintWidth_percent=".5"
app:layout_constraintHeight_percent=".5"

自定义View适配方案

有关自定义View的代码,代码如下:

public class CustomRelativeLayout extends RelativeLayout {

    private int displayMetricsWidth;//当前屏幕的宽度
    private int displayMetricsHeight;//当前屏幕的高度

    //标准值  这里以UI设计师给的设计标准为准  目前市面上主流分辨率为 1080x1920
    public static float STANDARD_WIDTH = 1080f;
    public static float STANDARD_HEIGHT = 1920f;

    private boolean flag;//是否已经测量,该变量是个标识,在RelativeLayout中,当子控件不满足父容器分配的宽高大小时,会再次进行测量。

    private float scaleX;//屏幕宽度缩放因子
    private float scaleY;//屏幕高度缩放因子

    public CustomRelativeLayout(Context context) {
        this(context, null);
    }

    public CustomRelativeLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //DisplayMetrics对象含有屏幕宽度,高度,DPI等的属性
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        //屏幕宽度
        displayMetricsWidth= displayMetrics.widthPixels;
        //屏幕高度,需要注意的是,如果屏幕底部有虚拟键盘,代码获取的
        //displayMetrics.heightPixels = 实际高度 - 虚拟键盘的高度
        //displayMetricsHeight = displayMetrics.heightPixels - 状态栏高度
        displayMetricsHeight= displayMetrics.heightPixels - getStatusBarHeight(context);
        scaleX = displayMetricsWidth/STANDARD_WIDTH ;//计算屏幕宽度缩放因子
        scaleY = displayMetricsHeight/STANDARD_HEIGHT;//计算屏幕高度缩放因子
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //获取测量大小
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if(!flag){
            //获取子控件个数
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {//遍历子控件,重新设置子控件宽高以及margin
                View child = getChildAt(i);
                LayoutParams params = (LayoutParams) child.getLayoutParams();
                //如果横向布局不是wrap_content,并且不是match_parent
                if(widthMode != MeasureSpec.AT_MOST && widthSize != displayMetricsWidth){
                    params.width = (int) (scaleX * params.width);
                }
                //如果纵向布局不是wrap_content,并且不是match_parent
                if(heightMode != MeasureSpec.AT_MOST && heightSize != displayMetricsHeight){
                    params.height = (int) (scaleY * params.height);
                }
                params.leftMargin = (int) (scaleX * params.leftMargin);
                params.rightMargin = (int) (scaleX * params.rightMargin);

                params.topMargin = (int) (scaleY * params.topMargin);
                params.bottomMargin = (int) (scaleY * params.bottomMargin);
            }
            flag = true;
        }

        super.onMeasure(widthMeasureSpec,heightMeasureSpec);

    }

    //获取状态栏的高度
    public int getStatusBarHeight(Context context){
        int resID = context.getResources().getIdentifier("status_bar_height","dimen","android");
        if (resID > 0 ){
            return context.getResources().getDimensionPixelSize(resID);
        }
        return 0;
    }
}

但是,需要注意以下几点:

(1)自定义View适配方案意义何在?

最小宽度适配方案不能完全满足需求时,可以使用看情况使用自定义View适配方案,根据您的需求,继续加工以上代码。

(2)分辨率适配方案、最小宽度适配方案与自定义View适配方案的区别何在?

分辨率适配方案、最小宽度适配方案在加载视图之前就已经屏幕的分辨率或最小宽度,而自定义View适配方案只有在onMeasure(测量)时才会计算有关适配的参数。

(3)DisplayMetrics对象获取的屏幕高度无需考虑屏幕底部虚拟键盘的高度,因为它已经是减去虚拟键盘高度之后的值,即

displayMetrics.heightPixels = 实际高度 - 虚拟键盘的高度

(4)必须减去状态栏高度,即

displayMetricsHeight = displayMetrics.heightPixels - 状态栏高度

当您使用DisplayMetrics对象获取屏幕高度时,已经包括了状态栏高度,这里必须减去状态栏高度。

(5)必须考虑ActionBar的高度,您需要在以上代码的基础上减去ActionBar的高度,本文之所以没有这样做是因为我在主题中默认没有使用ActionBar,如图:

图片.png

当然,ActionBar已被ToolBar替代,现在几乎已经没有人使用ActionBar了,但是需要谨记,如果你的项目中真的使用了ActionBar,那么displayMetricsHeight 必须减去ActionBar的高度。

(6)这种适配方案只适用子视图的宽高具有确定数值的情况,所以需要排除wrap_content的情况,当然,如果是match_parent,是不需要适配的,所以也排除match_parent的情况,局部代码如下:

            //如果横向布局不是wrap_content,并且不是match_parent
            if(widthMode != MeasureSpec.AT_MOST && widthSize != displayMetricsWidth){
                params.width = (int) (scaleX * params.width);
            }
            //如果纵向布局不是wrap_content,并且不是match_parent
            if(heightMode != MeasureSpec.AT_MOST && heightSize != displayMetricsHeight){
                params.height = (int) (scaleY * params.height);
            }

(7)CustomRelativeLayout后面继承的父类是可以更改的,继承RelativeLayout是为了保留RelativeLayout特性。也可以继承LinearLayout保留LinearLayout特性,最后继承其它布局也是一样。

(8)xml中的使用如下:

<com.zyc.screendemo.CustomRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello Worl World"
        android:textSize="26sp"
        android:background="@drawable/chat"
        android:layout_centerInParent="true"/>

</com.zyc.screendemo.CustomRelativeLayout>

各大厂商屏幕适配

只要涉及到厂商适配,必须要查看官方文档才可以。下面直接贴出官方文档。

【一】:vivo屏幕适配指南

  • 高长宽比设备应用适配指导意见

官方文档如下:

https://dev.vivo.com.cn/documentCenter/doc/195

这篇文档主要讲解了满屏应用和非满屏应用的相关处理方案,我们大部分非游戏应用基本都是非满屏应用,游戏应用基本都是满屏应用。

  • 全面屏应用适配指南(异形屏)

官方文档如下:

https://dev.vivo.com.cn/documentCenter/doc/103

  • Android P屏幕适配指南

官方文档如下:

https://dev.vivo.com.cn/documentCenter/doc/145

在这篇文档中找到刘海屏适配方案即可。

【二】:oppo屏幕适配指南

  • 挖孔屏适配指导

官方文档如下:

https://open.oppomobile.com/wiki/doc#id=10667

  • oppo凹形屏适配说明

官方文档如下:

https://open.oppomobile.com/wiki/doc#id=10159

  • Android P适配凹形屏注意点

https://open.oppomobile.com/wiki/doc#id=10293

【三】:小米屏幕适配指南

  • 全面屏及虚拟键适配说明

https://dev.mi.com/console/doc/detail?pId=1160

  • 小米刘海屏水滴屏 Android O 适配

https://dev.mi.com/console/doc/detail?pId=1293

  • 小米刘海屏/水滴屏/挖孔屏 Android P/Q 适配

https://dev.mi.com/console/doc/detail?pId=1341

【四】:华为云给出的屏幕适配指南

  • 详解Android刘海屏适配

https://bbs.huaweicloud.com/blogs/138775

  • Android刘海屏、水滴屏全面屏适配

https://bbs.huaweicloud.com/blogs/113986

最后,贴出Android P有关刘海屏屏幕适配代码

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //取消状态栏
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
    if (android.os.Build.VERSION.SDK_INT >= 28) {
        WindowManager.LayoutParams lp = this.getWindow().getAttributes();
        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
        this.getWindow().setAttributes(lp);
    }
    //沉浸式布局
    //在使用LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES的时候,状态栏会显示为白色,这和主内容区域颜色冲突,
    //所以我们要开启沉浸式布局模式,即真正的全屏模式,以实现状态和主体内容背景一致
    if (android.os.Build.VERSION.SDK_INT >= 28) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        WindowManager.LayoutParams lp = getWindow().getAttributes();
        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
        getWindow().setAttributes(lp);
        View decorView = getWindow().getDecorView();
        int systemUiVisibility = decorView.getSystemUiVisibility();
        int flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_FULLSCREEN;
        systemUiVisibility |= flags;
        getWindow().getDecorView().setSystemUiVisibility(systemUiVisibility);
    }

    setContentView(R.layout.heterotypic);

}

代码仅供参考和演示。

总结

(1)图片适配

当我们新建项目时,在工程目录下已经给我们建立好了图片资源限定符,即以下文件夹

图片.png

想要做图片适配,必须考虑图片的失真情况以及内存浪费情况。当适配图片时,可以参考下面表格:

名称 像素密度范围 大小(px) 大小(dp) 比例
mdpi 0~160dpi 1px 1dp 1
hdpi 160dpi~240dpi 1.5px 1dp 1.5
xhdpi 240dpi~320dpi 2px 1dp 2
xxhdpi 320dpi~480dpi 3px 1dp 3
xxxhdpi 480dpi~640dpi 4px 1dp 4
anydpi-v26 和像素密度无关(Android 8.0以上生效) 和像素无关 48x48dp 不需要适配

使用这种方法,必须将图片限定为固定的大小,单位为dp,比如将ImageView的长宽设置为固定的大小,单位dp,因为只有这样,我们才能准确的计算出图片资源的实际大小,然后将不同分辨率的图片放入不同的资源文件夹。

当然,您还可以使用wrap_content和math_parent来适配图片,但是我感觉意义不大,因为wrap_content会导致程序员不知道无法计算出实际所需图片分辨率,math_parent会导致图片变形,遇到图片变形的情况往往需要裁剪图片。

wrap_content和math_parent一般运用在布局上。

(2)使用wrap_content和math_parent对布局做适配,这个都懂的,就不多说了。
(3)在线性布局中,还可以使用weight(权重)对布局做等比摆放适配,这个您肯定也非常熟悉了,直接跳过;
(4)常见的LinearLayout(线性布局)、RelativeLayout(相对布局)、ConstraintLayout(约束布局)同样可以对布局做适配,这个不多说了;
(5)根据分辨率来适配含有瑕疵,这种适配方案受到宽度和高度的双重影响,如果设备中含有虚拟键盘,那么这种适配方案将不准确,最大的原因是,我们无法确定虚拟键盘的高度;
(6)尺寸限定符适配方案只用于Android 3.2之前,现在这种设备已被淘汰,所以不需要考虑;
(7)最小宽度适配方案被Google官方所推荐,使用ScreenMatch插件轻松实现屏幕适配,这种适配方案只被屏幕宽度所影响,和屏幕高度无关;
(8)布局别名屏幕方向限定符这两种屏幕适配都是针对于布局进行适配,可以运用在特殊的需求;
(9)自动拉伸位图,即使用Nine-Patch类型的图片,图片可以自动拉伸而不会变形;
(10)百分比适配,当年提出的百分比适配已被废弃,被ConstraintLayout替代;
(11)自定义View适配方案,一般情况下,我们使用最小宽度适配方案即可,自定义View适配方案只有在特殊需求的时候使用;
(12)有关全面屏、刘海屏、水滴屏、挖孔屏的屏幕适配直接参考各大厂商的文档即可,需要注意的是,从Android P开始,Google对刘海屏提供了接口支持。

简化版总结

(1)图片适配方案不仅可以保证图片不会失真,还可以保证因为图片浪费不必要的内存;
(2)合理的使用wrap_content、math_parent、weight,可以让布局更加协调;
(3)Google官方推荐的最小宽度限定符配置方案;
(4)布局别名屏幕方向限定符可以解决布局的特殊适配需求;
(5)Nine-Patch类型的图片可以保证图片拉伸而不变形;
(6)一般我们使用Google官方推荐的最小宽度限定符配置方案,当这个适配方案不能完全满足需求时,可以使用自定义View适配方案;
(7)有关全面屏、刘海屏、水滴屏、挖孔屏的屏幕适配直接参考各大厂商的文档即可,需要注意的是,从Android P开始,Google对刘海屏提供了接口支持;

[本章完...]

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

推荐阅读更多精彩内容