Android屏幕适配总结

4,528 阅读19分钟

前言

说到Android屏幕适配,是老生常谈的话题,适配的目的无非就是不同设备UI表现结果要和设计图比例一致。实际适配过程中,面对不同的机型,多样的分辨率,你适配对了吗?是否因为图片位置不对导致应用OOM?本文介绍常用屏幕适配方案宽高限定符和sw限定符,着重介绍sw限定符和图片适配。

1.屏幕适配方案

1. 宽高限定符(分辨率限定符)

穷举市面上所有的手机宽高,生成相应的res文件,如values-1920 × 1080,values-1280 × 720,values-1024 × 600等,使用其中一种分辨率(和UI设计一样)作为基准,编写dimens文件,然后其他所有的分辨率根据基准分辨率计算,生成其对应分辨率的dimens文件,使用时,直接按照ui设计图在xml里面使用对应的dimens值,不同分辨率会使用到其对应的dimens下的值。

这一方案有两个问题:

  • 问题一:生成dimens文件时比较麻烦,虽然可以使用工具生成,但需要减去手机的导航栏高度(针对手机有导航栏的情况);否则命中接近的values目录(并非网上说的精准命中才会生效,读者可以自行测试),导致适配效果较差。
  • 问题二:当基准分辨率与某个分辨率差别较大时,同样的x1,y1,在基准分辨率上显示是正方形,另一个分辨率是长方形,误差很大。

宽高限定符values计算:

方式1:

先算出底部导航栏的高度和屏幕实际宽高,然后用屏幕实际高度减去导航栏高度

方式2:

通过下面代码获取:

DisplayMetrics mDisplayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics);
int widthPixels = mDisplayMetrics.widthPixels;
int heightPixels = mDisplayMetrics.heightPixels;

2. sw限定符

又称最小宽度限定符,Android会识别屏幕可用高度和宽度中最小尺寸的dp值(一般就是手机宽的dp值),然后根据识别到的结果在资源文件中使用对应的资源文件,如果没有找到对应的,就会向下找,最后使用默认的。比如手机sw=360dp,会优先使用values-sw360dp,如果没有values-360dp,只有values350dp,会使用values-350dp下的文件,如果没有比350dp和更小的,会使用默认的配置(values下)。 这一方案和宽高限定符相比,更容易命中资源,而且就算没有该资源,也会使用接近的,ui效果差别不会太大,所以这一方案使用比较广泛。

2.屏幕适配目录含义

默认以竖屏情况下进行介绍,如有区分横竖屏适配会特别说明

1. 布局适配(layout目录)

布局适配即根据适配方案之一加载指定布局

layout 默认目录 layout-land 横屏 layout-port 竖屏 正常情况下,默认目录是竖屏,单独增加layout-land适配横屏,反之亦然

1.1宽高限定符

android3.0之前,适配指定分辨率,将layout文件夹做如下命名: layout-1024 × 768 layout-1024 × 600
layout-1280 × 768

android3.0以后,需将高度减去底部导航栏像素的高度,这里假设底部导航栏高度是48px: layout-976 × 768
layout-976 × 600
layout-1232 × 768

如果要区分横竖屏适配(android3.0以后),目录名加上land (横屏)或port(竖屏) 横屏适配:layout-land-1024 × 720 竖屏适配:layout-port-976 × 768

1.2 sw限定符

命名如下: layout-sw360dp layout-sw392dp layout-sw411dp

如果要区分横竖屏适配,目录名加上land (横屏)或port(竖屏)

layout-sw360dp-land layout-sw360dp-port

1.3 w或h限定符

此外还有一种目录layout-w360dp,与layout-sw360dp的区别,举例说明

1.3.1.layout-sw360dp

这里的sw代表smallwidth的意思,当你的屏幕的绝对宽度大于360dp时,屏幕就会自动调用layout-sw360dp文件夹里面的布局。

注意:这里的绝对宽度是指手机的实际宽度,与手机横竖屏无关。sw最小宽度是指屏幕宽高的较小值,每个屏幕都是固定的,不会随着屏幕横向纵向改变而改变。

1.3.2.layout-w360dp

当你的屏幕的相对宽度大于360dp时,屏幕就会自动调用layout-w360dp文件夹里面的布局。

注意:这里的相对宽度是指手机相对放置的宽度;即当手机竖屏时,为较小边的长度;当手机横屏时,为较长边的长度。当屏幕横向纵向切换时,屏幕的宽度是变化的,以变化后的宽度来与原来的宽度相比,看是否使用此资源文件下的资源。

1.3.3.layout-h360dp

与layout-w360dp的使用一样,只是这里指的是相对的高度。但这种方式很少使用,因为屏幕在纵向上通常能够滚动导致长度变化,不像宽度那样基本固定,因为这个方法灵活性不是很好,google 官方文档建议尽量少使用这种方式。

这里的sw、w、h的 dpi 值计算方式如下

DisplayMetrics metrics = getResources().getDisplayMetrics();
int widthDpi = (int) (metrics.widthPixels / metrics.density); 
int heightDpi = (int) (metrics.heightPixels / metrics.density);

sw: 取widthDpi 和heightDpi 的较小值 w: widthDpi h: heightDpi

2.dimen适配(values目录)

values目录可放的资源比较多,比如dimen,color,style,国际化语言。values目录结构可以复杂到 values-port-xhpdi-1280 × 768-4,实际不用这么精确适配

上面说了布局的限定符,正常情况使用长度的限定符多点,流程同上面。

3.sw dimen适配流程

选择其中一种设备作为基准适配,然后以基准设备在等比生成其它设备的sw值

参考单位:

单位描述
px (pixels)像素,就是屏幕上实际的像素点单位
dip或者dp (device independent pixels)设备独立像素, 与设备屏幕有关
sp (scaled pixels — best for text size)类似dp, 主要处理字体的大小
dpi (dots of per inch)屏幕像素密度,每英尺(对角线长度)的点数
ppi(pixels of per inch)每英尺的像素点数,代表着像素和真是大小的关系

注意:dpi不等于ppi(ppi = √(宽^2 + 高^2)/ 屏幕尺寸),dpi是写入系统配置文件中的,可以通过代码获取

int densityDpi = getResources().getDisplayMetrics().densityDpi;

参考计算公式:

px = density * dp;

density = dpi / 160;

px = dp * (dpi / 160);

而sw-xxxx-dp的计算公式是 :

sw *160/dpi

sw为宽高的较小边

3.1.dimen适配

  1. 选定基准设备分辨率1080 × 1920,dpi=360,密度density=3,图片目录xxhdpi,则values-sw360dp1dp=1dp
  2. 需要适配其它设备(设备1~设备5,均为竖屏),等比处理如下:
  • 设备1:分辨率1920 × 1080,dpi=420,密度density= 2.625,values-sw411dp1dp=411/360*1dp=1.14dp
  • 设备2:分辨率1920 × 1080,dpi=360,密度density=2.25,values-sw480dp1dp=480/360*1dp=1.33dp
  • 设备3:分辨率1280 × 720,dpi=240,密度density=1.5,values-sw480dp1dp=480/360*1dp=1.33dp
  • 设备4:分辨率840 × 480,dpi=240,密度density=1.5,values-sw320dp1dp=320/360*1dp=0.88dp
  • 设备5:分辨率840 × 480,dpi=160,密度density=1,values-sw480dp1dp=480/360*1dp=1.33dp

备注:设备参数来源模拟器

  1. 最后整理生成:

|values目录| dp值| -------- | ------------- | ------------- |values(默认目录和基准一样) | 1dp=1dp | values-sw320dp | 1dp=0.88dp | values-sw360dp(基准sw) | 1dp=1dp |values-sw411dp | 1dp=1.14dp | values-sw480dp | 1dp=1.33dp

  1. 然后调整蓝湖分辨率设计图宽度**360dp **

5.最后设计图控件标注多少dp,就选多少dp。

6.测试验证: 当你标注控件是宽高250dp×250dp,运行在不同设备表现的宽度占比是接近的

  • 在基准设备sw360dp上是250dp->250dp× 1dp=250dp
  • 在设备sw320dp上是250dp->250dp× 0.88dp=220dp
  • 在设备sw411dp上是250dp->250dp× 1.14dp=285dp
  • 在设备sw480dp上是250dp->250dp× 1.33dp=332.5dp

由于设备dp值最终要换算成px值(根据公式px= dp * density),故上面基准设备和5种设备250dp最终值为

  • 基准设备:values-sw360dp(密度density=3),250dp* 3=750px,与宽度占比:750/1080=0.6944
  • 设备1:分辨率1920 × 1080,dpi=420,密度density= 2.625,values-sw411dp,285dp* 2.625=748.125px,与宽度占比:748.125/1080=0.6927
  • 设备2:分辨率1920 × 1080,dpi=360,密度density=2.25,values-sw480dp,332.5dp*2.25=748.125px,与宽度占比:748.125/1080=0.6927
  • 设备3:分辨率1280 × 720,dpi=240,密度density=1.5,values-sw480dp,332.5dp*1.5=498.75px,与宽度占比:498.75px/720=0.6927
  • 设备4:分辨率840 × 480,dpi=240,密度density=1.5,values-sw320dp,220dp*1.5=330px,与宽度占比:330px/480=0.6875
  • 设备5:分辨率840 × 480,dpi=160,密度density=1,values-sw480dp,332.5dp*1=332.5px,与宽度占比:332.5px/480=0.6927

通过上面发现,到最后得出的比例都是在 0.6927 左右,所以屏幕适配完成了等比例缩放,误差范围内是可以接受的,如果差别很大,说明你适配有问题。上面0.6927和0.6875不同是因为计算swdp时,舍去了小数部分,比如设备1本来是411.4285714285714,取值411dp,是因为小于或等于 411.4285714285714 dp 的 values-swdp会被系统匹配匹配到,大于的无法匹配。

补充Sw代码中计算方式: Sw计算方式1:

/**
	 * 获取SmallestWidthDP
	 * @param context
	 * @return
	 */
	public static float getSmallestWidthDP(Context context) {
	    DisplayMetrics dm = context.getResources().getDisplayMetrics();
	    int heightPixels =getScreenHeight(context);
	    int widthPixels = getScreenWidth(context);
	    float density = dm.density;
	    float heightDP = heightPixels / density;
	    float widthDP = widthPixels / density;
	    float smallestWidthDP;
	    if (widthDP < heightDP) {
	        smallestWidthDP = widthDP;
	    } else {
	        smallestWidthDP = heightDP;
	    }
	    return smallestWidthDP;
	}
	/**
	 * 获取屏幕宽度
	 *
	 * @param context Context
	 * @return 屏幕宽度(px)
	 */
	public static int getScreenWidth(Context context) {
	    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
	    Point point = new Point();
	    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
	        wm.getDefaultDisplay().getRealSize(point);
	    } else {
	        wm.getDefaultDisplay().getSize(point);
	    }
	    return point.x;
	}
	
	/**
	 * 获取屏幕高度
	 *
	 * @param context Context
	 * @return 屏幕高度(px)
	 */
	public static int getScreenHeight(Context context) {
	    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
	    Point point = new Point();
	    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
	        wm.getDefaultDisplay().getRealSize(point);
	    } else {
	        wm.getDefaultDisplay().getSize(point);
	    }
	    return point.y;
	}

Sw计算方式2:

int  smallestWidthDP2 = (Math.min(heightPixels,widthPixels)) * 160 / dm.densityDpi;

Sw计算方式3:

int smallestScreenWidthDp3 =context.getResources().getConfiguration().smallestScreenWidthDp;

批量生成values-sw目录

上面说了怎么计算sw值,实际适配要生成不同sw值下的不dimen长度,有两种方法:

  1. 使用插件ScreenMatch可以批量生成values-sw目录
  2. 使用下面的工具类:
package com.sjl.lib.screenadaptation;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * sw生成工具类
 *
 * @author Kelly
 * @version 1.0.0
 * @filename MakeDpXml
 * @time 2020/11/30 14:33
 * @copyright(C) 2020 song
 */
public class MakeDpXml {
    /**
     * 基值,单位dp
     */
    private static int baseSw = 1080;
    private static List<Integer> sw = Arrays.asList(360,392,411,1080,1440,2160);
    private static int decimals = 4;
    private static int scale = 1;
    private static String filename = "dimens.xml";

    public static void main(String[] args) {
        //自定义参数
        baseSw = 1080;
        decimals = 4;
        scale = 1;
        //模板文件,文件存储F:/layout下,也可以自己定义
        filename = "dimens.xml";
        create();
        System.out.println("MakeDp完毕");
    }

    public static void create() {
        //以此文件夹下的dimens.xml文件内容为初始值参照
        File file = new File("F:/layout",filename);
        List<String> lines = new ArrayList<>();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(file));
            String tempString;
            while ((tempString = reader.readLine()) != null) {
                lines.add(tempString);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception ex) {

                }
            }
        }
        int lineSize = lines.size();
        if (lineSize == 0) {
            return;
        }

        Map<Integer, String> resultContent = new HashMap<>();
        for (int j = 0; j < sw.size(); j++) {
            Integer tempSw = sw.get(j);
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < lineSize; i++) {
                String xmlStrLine = lines.get(i);
                if (xmlStrLine.contains("</dimen>")) {
                    String start = xmlStrLine.substring(0, xmlStrLine.indexOf(">") + 1);
                    String end = xmlStrLine.substring(xmlStrLine.lastIndexOf("<") - 2);
                    //截取<dimen></dimen>标签内的内容,从>右括号开始,到左括号减2,取得配置的数字
                    String text = xmlStrLine.substring(xmlStrLine.indexOf(">") + 1,
                            xmlStrLine.indexOf("</dimen>") - 2);
                    if (text.startsWith("@dimen")) {
                        sb.append(xmlStrLine).append("\r\n");
                        continue;
                    }
                    Float num = Float.parseFloat(text);
                    //根据不同的尺寸,计算新的值,拼接新的字符串,并且结尾处换行。
                    float value = (float) tempSw / baseSw * num;
                    if (tempSw != baseSw){
                        value = value / scale;
                    }
                    sb.append(start).append(String.format("%."+decimals+"f", value)).append(end).append("\r\n");
                } else {
                    sb.append(xmlStrLine).append("\r\n");
                }
            }
            resultContent.put(tempSw, sb.toString());
        }

        //写入文件
        for (int i = 0; i < sw.size(); i++) {
            Integer tempSw = sw.get(i);
            String s = resultContent.get(tempSw);
            File dir = new File("F:/layout/values-sw" + tempSw + "dp",filename);
            if (!dir.getParentFile().exists()) {
                dir.getParentFile().mkdirs();
            }
            MakeXmlUtils.writeFile(dir.getAbsolutePath(), s);
        }


    }



}

MakeXmlUtils:

/**
     * 写入文件
     */

    public static void writeFile(String file, String text) {
        PrintWriter out = null;
        try {
            out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
            out.println(text);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }

3.2.图片适配

上面说sw长度适配,如果控件是图片,根据设计图指定了控件宽高,如何选择哪个密度的图片呢?

操作步骤:

  1. 计算设备密度dpi
DisplayMetrics   dm = getResources().getDisplayMetrics();
int densityDpi = dm.densityDpi;
  1. 然后参考下图即可
目录对应密度设备密度logo建议尺寸密度区间
ldpi120dpi0.7536*360-120
mdpi160dpi148*48120dpi~160dpi
hdpi240dpi1.572*72160-240
xhdpi320dpi296*96240-320
xxhdpi480dpi3144*144320-480
xxxhdpi640dpi4192*192480-640
  1. 根据设备实际dpi选择图片下载存放即可,一般选择一种基准密度,如360dpi,如xxhdpi,然后运行在不同的设备,控件显示长度最终计算出的宽度占一致,上面已经说明。

  2. 但是,运行到不同dpi的设备时,占用内存是不一样,与控件显示尺寸无关,别妄想修改控件显示尺寸降低应用内存。当且仅当设备密度区间是320dp~480dpi的时候,匹配到xxhdpi时最佳(指清晰度最好,内存占用适当),其它设备密度图片内存减小或增多,图片出现不同程度模糊。

上面说了dimen适配长度是宽度占比一样的,但图片占用内存不一样,图片占用内存大小与图片本身的分辨率、像素存储格式、图片所在drawable 文件夹和设备dpi有关。

像素占用内存受Bitmap色彩的存储模式影响:

  • ARGB_8888:A->8bit->一个字节,R->8bit->一个字节,G->8bit->一个字节,B->8bit->一个字节,即8888,一个像素总共占四个字节,8+8+8+8=32bit=4byte
  • ARGB_4444:A->4bit->半个字节,R->4bit->半个字节,G->4bit->半个字节,B->4bit->半个字节,即4444,一个像素总共占两个字节,4+4+4+4=16bit=2byte
  • RGB_565:R->5bit->半个字节,G->6bit->半个字节,B->5bit->半个字节,即565,一个像素总共占两个字节,5+6+5=16bit=2byte ALPHA_8:A->8bit->一个字节,即8,一个像素总共占一个字节,8=8bit=1byte

举例子之前,先了解图片实际宽高计算公式

新图的宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )
新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )

根据上面所得的宽高(不是显示的尺寸android:layout_width,android:layout_height)所知,图片占用内存大小计算公式:

图片占用内存大小=宽x高 *一个像素所占的内存字节大小

实际并不是直接取原图宽高计算,严格来说是以图片以bitmap的形式存在内存中的实际尺寸。所以上面

图片占用内存大小=bitmap宽xbitmap高 *一个像素所占的内存字节大小

而
bitmap 宽 = 原图的宽 * (设备的 dpi / 目录对应的 dpi)
bitmap 高 = 原图的高 * (设备的 dpi / 目录对应的 dpi)

或者

图片占用内存大小 = 原图宽 * (inTargetDensity / inDensity) * 原图高 * (inTargetDensity / inDensity) * 每个像素占用的字节数

inDensity:表示目标图片的dpi(放在哪个资源文件夹下),即目录对应的 dpi inTargetDensity:表示目标屏幕的dpi,即设备的 dpi 所以你可以发现inDensity和inTargetDensity会对Bitmap的宽高进行拉伸,进而改变Bitmap占用内存的大小

举例子:

假设你设备的基准设备dpi是360dp,密度3,对应图片目录是xxhdpi,分辨率为1080 × 1920,sw为sw360,有这么一个图片的宽高为250px ×250px,假设你放错了图片目录(不是mdpi,正常放在xxhdpi目录),图片所占用的内存大小是不一样的。

  1. 图片放错目录导致图片宽高变化,从而导致图片的内存变化情况分析。下面我们计算下图片在不同目录内存变化情况
存放目录宽度计算内存变化
ldpi360/120*250变大
mdpi360/160*250变大
xxdpi360/360*250正常
xxdpi360/640*250变小

同理高度亦然。 通过上面可知,** 同一设备的同一张图片不同图片目录下,占用内存不一样**。

  1. 假设你图片正常放在xxhdpi,当你运行在其他不同dpi设备时内存变化情况:

计算参考:

新图的宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )
  • 设备1:分辨率1080 × 1920,dpi=420,密度density= 2.625,values-sw411dp,420/360*250,变大
  • 设备2:分辨率1080 × 1920,dpi=360,密度density=2.25,values-sw480dp,360/360*250,正常
  • 设备3:分辨率720 × 1280,dpi=240,密度density=1.5,values-sw480dp,240/360*250,变小
  • 设备4:分辨率480 × 840,dpi=240,密度density=1.5,values-sw320dp,240/360*250,变小
  • 设备5:分辨率480 × 840,dpi=160,密度density=1,values-sw480dp,160/360*250,变小

通过上面可知**,不同设备的同一张图片在同一个目录下,设备dpi不同占用内存不一样**。

总之,平时我们都是一套图片适配,但是如果一套图片适配,出现细微差异,需要针对不同设备dpi单独适配图片。需要注意图片目录存储位置不要差异过大,否则容易出现oom,图片模糊等问题。

4.sw适配基准设备示例

下面数据是每种设备的sw值,可以选择其中一个设备作为基准值适配,然后以基准设备在等比生成其它设备的sw值,适配过程同上面sw dimen适配流程 终端设备样例:

尺寸屏幕方向分辨率密度对应目录 dp和px关系sw蓝湖预览分辨率自定义
8竖屏1280 × 8001mdpi1:1sw800800*1280
10.1横屏1280 × 8001mdpi1:1sw8001280*800
55竖屏1920 × 10801mdpi1:1sw10801080*1920
21竖屏1920× 10801mdpi1:1sw10801080*1920
17横屏1280 × 10241mdpi1:1sw10241280*1024

手机:

尺寸屏幕方向分辨率密度对应目录 dp和px关系sw蓝湖预览分辨率自定义描述
5.2竖屏1920 × 10803xxhdpi1:3sw360360*640小米5
6.4竖屏2340 × 10803xxhdpi1:3sw360360*780Oppo k5水滴全面屏

屏幕方向判断:

If (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
    //竖屏
    System.out.println("竖屏");
} else {
    //横屏
    System.out.println("横屏");
}

sw适配的缺点

当以不同的基准sw进行适配时,在不同sw的设备上运行,可能存在长度差异,从而导致某些情况下最终显示差异

情况1:sw360dp基准(密度是3),如下图:

在这里插入图片描述

以sw360dp基准去适配sw1080dp设备(密度是1),发现在sw1080dp设备,最终px是整数,159*1=159,不存在差异问题

情况2:sw1080dp基准(密度是1),如下图:

在这里插入图片描述 以sw1080dp基准去适配sw360dp设备(密度是3),发现在sw360dp设备,最终px是小数,17.6667*3=53.0001,存在差异问题

当采用sw适配时,代码中禁止使用下面这种转换方法获取dp或px,因为这脱离了sw适配体现,会导致误差,建议使用getDimension、getDimensionPixelOffset 、getDimensionPixelSize获取

  /**
     * dip转px
     * @param context
     * @param dipValue
     * @return
     */
    public static int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    /**
     * px转dip
     * @param context
     * @param pxValue
     * @return
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

5.代码中获取dimen

采用sw限定符适配,难免有时候在Java或Kotlin代码中引用dimen.xml的的dp值 主要有以下三种方法:

  1. getDimension 返回float值
  2. getDimensionPixelOffset 将float强转为int值返回
  3. getDimensionPixelSize 将float值四舍五入成int值返回

上面三种方法返回值,以sw xml文件的长度单位为准:

  1. 当给定值是px,则直接返回给定的值
  2. 当给定值是dp,则需乘以屏幕密度再返回
  3. 当给定值是sp,则需乘以缩放因子再返回

根据上面可知,当你使用dp值作为字体大小设置时需要注意放大问题,即xml设置字体大小和代码中表现结果是不一样的:

xml设置字体20dp

 <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textSize="@dimen/dp_20"
        />

Java代码中设置20dp

		float size = context.getResources().getDimension(R.dimen.dp_20);
		TextView textView = new TextView(context);
		textView.setTextSize(size);

上面字体结果值会比xml的大(设备密度非160dpi,即不是1px不等于1dp的情况),导致显示不达预期。原因是上面已经说明,当给定值是dp,则需乘以屏幕密度再返回(转成px值),而 setTextSize的默认单位是sp,使用sp单位时,会乘多缩放因子scaledDensity,故字体放大了

备注:无论是sp还是dp最终都转成px,getResources().getDisplayMetrics().scaledDensitygetResources().getDisplayMetrics().density一般情况下是相同的

为了兼容其它设备,正确获取dp值如下:

   float size = context.getResources().getDimension(R.dimen.dp_20)/ context.getResources().getDisplayMetrics().density;
	TextView textView = new TextView(context);
	textView.setTextSize(size);
	
	//或者
    setTextSize(TypedValue.COMPLEX_UNIT_PX,getResources().getDimensionPixelSize(R.dimen.dp_20));

这样设置才是是正确的值,显示效果达到预期。

6.扩展(今日头条适配)

今日头条适配方案,通过修改系统density达到适配目的。Android中的尺寸,不管使用的是什么单位,系统最终都会转换成px使用,而

px = dp * density

而系统使用的density值,是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces通过Activity或者ApplicationContext获得。也就是说所有的dp和px的转换都是通过 DisplayMetrics 中相关的值来计算的,所以只需要修改DisplayMetrics中的density值,就可以完成dp适配。sp的适配也类似,只是多了一个缩放因子,让用户可以改变字体大小

public class ScreenAdpt {
    /** 设计图宽度dp */
    private static final float width = 360;


    private static float textDensity = 0;
    private static float textScaledDensity = 0;


  

    /**
     * @param activity
     */
    public static void setCustomDensity(@NonNull final Activity activity) {
        final Application application = activity.getApplication();
        final DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
        if (textDensity == 0) {
            textDensity = displayMetrics.density;
            textScaledDensity = displayMetrics.scaledDensity;
            application.registerComponentCallbacks(new ComponentCallbacks() {
                @Override
                public void onConfigurationChanged(Configuration configuration) {
                    if (configuration != null && configuration.fontScale > 0) {
                        textScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                @Override
                public void onLowMemory() {

                }
            });
        }

        final float targetDensity = displayMetrics.widthPixels / width;
        
        final float targetScaledDensity = targetDensity * (textScaledDensity / textDensity);
        final int targetDpi = (int) (160 * targetDensity);

        displayMetrics.density = targetDensity;
        displayMetrics.scaledDensity = targetScaledDensity;
        displayMetrics.densityDpi = targetDpi;

        final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
        activityDisplayMetrics.density = targetDensity;
        activityDisplayMetrics.scaledDensity = targetScaledDensity;
        activityDisplayMetrics.densityDpi = targetDpi;
    }
}

7.小结

总之,smallestWidth 限定符屏幕适配方案的情况下,不同设备 View 与屏幕宽度的比例和设计图中的比例一致。适配需要注意是选择其中一种设备作为基准适配,然后以基准设备(最小宽度基准值 )在等比生成其它设备的sw值,不然容易出问题,UI设计师一般没有这种意识,多数基于某种分辨率出图,当你选择sw限定符适配时,多数需要自定义分辨率预览(指蓝湖、慕客等工具),不然容易出现选错长度和图片大小,导致一系列问题。上面如有错误,请指正。