Android - 屏幕适配

一、概念

DPI名称(范围值)

density密度(1dp显示多少px)

常见分辨率

ldpi ≤ 120dpi

0.75px = 120dpi / 160

QVGA = 240*320

mdpi ≤ 160dpi(基准)

1px = 160dpi / 160

HVGA = 320*480

hdpi ≤ 240dpi

1.5px = 240dpi / 160

WVGA = 480*800

FWVGA = 480*854

xhdpi ≤ 320dpi

2px = 320dpi / 160

720P = 720*1280

xxhdpi ≤ 480dpi

3px = 480dpi / 160

1080P = 1080*1920

xxxhdpi ≤ 640dpi

4px = 640dpi / 160

4K = 2460*3840

1.1 屏幕像素密度 PPI、DPI

        PPI(Pixels Per Inch)屏幕每英寸容纳多少个像素点,DPI(Dots Per Inch)这个“点”是根据屏幕物理概念产生的一个软件概念,在不同行业有不同理解,印刷行业每英寸打印多少个墨点,鼠标移动一英寸光标移动多少像素点,在Android中被用来表示屏幕每英寸显示多少个像素点,概念上可以当作没有区别。

        由于不同设备的屏幕尺寸和分辨率各不相同且毫无规律,导致 DPI 的值非常混乱,从而影响 dp 适配。

1.2 密度无关像素 dp

density-independent pixel,是 Android 特有的单位,只与DPI有关,保证了在不同屏幕像素密度的设备上显示相同的效果。以DPI=160为基准1dp=1px,用设备实际DPI值/160=density密度,即1dp=多少px。用屏幕宽或高的px/density=屏幕宽或高最大dp长度。
  • dp = px / density

  • density = dpi / 160

  • dpi = √(宽² + 高²) / 屏幕尺寸

1.3 独立比例像素 sp

scale-independent pixel,是 Android 特有的单位,用于设置字体大小。

二、AndroidStudio目录说明

Android会根据手机屏幕分辨率选择对应文件夹中的资源,如果没有对应的文件夹,则从最高分辨率的文件夹依次往低处寻找,找到高分辨率的就压缩后显示,找到低分辨率的就放大显示。
mipmap只用来放桌面应用图标(Manifest中application标签配置的icon项),drawable用来存放图片资源,values用来针对不同分辨力编写对应资源文件。

三、品牌机型分辨率

分辨率

小米

红米

Pixel

三星

OPPO

VIVO

荣耀

一加

IQOO

720P

720*1280

720*1520

7/8

720*1600

9A

720*1650

12C

1080P

1080*1920

5/6

1/2

1080*2160

Mix2

3

1080*2220

4

1080*2340

9/10、Mix3

9、K20P、Note9

5/6/7

S22/23

X60

7

1080*2376

X50/70

1080*2388

Z7

1080*2400

12/13、Mix4、CV1/2

10、K30P/40P、Note10/11/12

S21

X5、Reno8

X80、S9/10/12/15/16

60/70/80

1080*2412

Reno9

Ace1

1.5K

1220*2712

K50至尊版/K60至尊版

X90

Ace2

1240*2772

X6

1260*2800

2K

1440*2560

1440*2960

S8/9

1440*3040

S10

1440*3200

11/13P/14P

K50P/K60/K60P

S20

11

1440*3216

9P/10P/11

四、屏幕适配

市面上的机型,不同分辨率*不同屏幕尺寸=无限多种DPI组合。当美工给的图以px为单位使用4.1的dimen适配、以dp为单位使用4.2的字节跳动density适配、更推荐的是smallestWidth限定符适配。

dimen

宽高适配

通过穷举市面上所有 Android 手机的屏幕像素尺寸来实现适配,通过比例换算来为不同分辨率的屏幕分别准备一套 dimens 文件,应用在运行时再去引用和当前设备完全匹配的 dimens 文件,以此来实现屏幕适配。优点:?
缺点:缺点是容错率低,只有精准匹配分辨率才能适配,否则只能引用默认dimens文件夹,显示效果就会有很大出入。

smallestWidth

最小宽度限定

根据屏幕最短的那个边(不考虑屏幕方向)适配。适配原理和宽高限定一样,也是通过比例换算来为不同尺寸屏幕分别准备一套 dimens 文件,应用在运行时再去引用和当前设备最匹配的那个。优点:容错率高,没有找到对应dimens文件会向下寻找接近的。在 320 ~ 460 dp 之间每 10 dp 就提供一套 dimens 文件就足够使用了,想要囊括更多设备的话也可以再缩短步长,基本不用担心最终效果会与设计稿偏差太多,且不会影响到三方库。
缺点:需要生成多套 dimens 文件,增大了 apk 体积。

density

字节跳动密度适配

基于系统将 dp 转换为 px 的公式 px = dp * density 来实现适配,通过在运行时动态修改 density 值的大小,使得修改后计算出的屏幕宽度就等于设计稿的宽度,从而使得在不同屏幕尺寸下我们都可以直接使用设计稿给出的 dp 值,且无需准备多套 dimens 文件。优点:可以直接使用设计稿中的 dp 值,无需准备多套 dimens 文件进行映射,因此不会增大 apk 体积,且在三种方案中 UI 还原度最高,其它两种方案都需要精准命中屏幕尺寸后才能达到此方案的还原度。
缺点:由于此方案会影响到应用全局,对于已经迭代了很久的项目来说,中途引入此方案大概率会影响到现有的适配方案;即使是新项目,又需要考虑到此方案对于三方库的影响,不能由于主项目的变动导致三方库自身界面变形。

4.1 dimen宽高适配


public class DimenUtils {
    private final static String rootPath = "C:/Users/Administrator/Desktop/layoutroot/values-{0}x{1}/"; //注意将Administrator替换为实际用户名
    private final static float dw = 300f;   //屏幕横向分为多少等份
    private final static float dh = 500f;   //屏幕纵向分为多少等份
    private final static String WTemplate = "<dimen name=\"x{0}\">{1}px</dimen>\n"; //xml中横向条目的内容:<dimen name="x1">2.4px</dimen>
    private final static String HTemplate = "<dimen name=\"y{0}\">{1}px</dimen>\n"; //xml中纵向条目的内容:<dimen name="y1">2.56px</dimen>

    public static void main(String[] args) {    //AndroidStudio运行失败的话选择 Run 'DimenUtils.main()' with Coverage
        //720P
        makeString(720, 1280);
//        makeString(720, 1520);
//        makeString(720, 1600);
//        makeString(720, 1650);
        //1080P
//        makeString(1080, 1920);
//        makeString(1080, 2160);
//        makeString(1080, 2220);
        makeString(1080, 2340);
//        makeString(1080, 2376);
        makeString(1080, 2400);
//        makeString(1080, 2412);
        //1.5K
//        makeString(1220, 2712);
//        makeString(1240, 2772);
//        makeString(1260, 2800);
        //2K
//        makeString(1440, 2560);
//        makeString(1440, 2960);
//        makeString(1440, 3040);
        makeString(1440, 3200);
    }

    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 < dw; i++) {
            sb.append(WTemplate.replace("{0}", i + "").replace("{1}",
                    change(cellw * i) + ""));
        }
        sb.append(WTemplate.replace("{0}", "300").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 < dh; i++) {
            sb2.append(HTemplate.replace("{0}", i + "").replace("{1}",
                    change(cellh * i) + ""));
        }
        sb2.append(HTemplate.replace("{0}", "500").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;
    }
}
//将生成的文件夹直接拖进 AndroidStudio 中的 res 中,使用的时候就 @dimen/ 后跟方向和份数。
<TextView
    android:layout_width="@dimen/x100"
    android:layout_height="@dimen/y250"/>

4.2 smallestWidth 限定符适配

点击跳转

大部分手机的宽度dp值集中在320-450之间,大部分1080P的手机应该都是360dp,390dp,411dp。可以在这个基础上,参考Android studio中的Virtual Device Configuration。

public class JavaTest {

    private static final int DESIGN_WIDTH = 375;    //设计稿宽度(将自己设计师的设计稿的宽度填入)
    private static final int DESIGN_HEIGHT = 667;   //设计稿高度(将自己设计师的设计稿的高度填入)

    public static void main(String[] args) {
        int smallest = DESIGN_WIDTH > DESIGN_HEIGHT ? DESIGN_HEIGHT : DESIGN_WIDTH;  //     求得最小宽度
        DimenTypes[] values = DimenTypes.values();
        for (DimenTypes value : values) {
            File file = new File("");
            MakeUtils.makeAll(smallest, value, file.getAbsolutePath());
        }
    }

    private enum DimenTypes {
        //适配Android3.2+,大部分手机的sw值集中在300~460,想生成多少自己以此类推
        DP_sw__300(300),  // values-sw300
        DP_sw__310(310),
        DP_sw__320(320),
        DP_sw__330(330),
        DP_sw__340(340),
        DP_sw__350(350),
        DP_sw__360(360),
        DP_sw__370(370),
        DP_sw__380(380),
        DP_sw__390(390),
        DP_sw__400(400),
        DP_sw__410(410),
        DP_sw__420(420),
        DP_sw__430(430),
        DP_sw__440(440),
        DP_sw__450(450),
        DP_sw__460(460),
        DP_sw__470(470),
        DP_sw__480(480),
        DP_sw__490(490);

        private int swWidthDp;

        DimenTypes(int swWidthDp) {
            this.swWidthDp = swWidthDp;
        }

        public int getSwWidthDp() {
            return swWidthDp;
        }

        public void setSwWidthDp(int swWidthDp) {
            this.swWidthDp = swWidthDp;
        }
    }

    private static class MakeUtils {
        private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
        private static final String XML_RESOURCE_START = "<resources>\r\n";
        private static final String XML_RESOURCE_END = "</resources>\r\n";
        private static final String XML_DIMEN_TEMPLETE = "<dimen name=\"qb_%1$spx_%2$d\">%3$.2fdp</dimen>\r\n";
        private static final String XML_BASE_DPI = "<dimen name=\"base_dpi\">%ddp</dimen>\r\n";
        private static final int MAX_SIZE = 720;
        private static final String XML_NAME = "dimens.xml";    //生成的文件名


        public static float px2dip(float pxValue, int sw, int designWidth) {
            float dpValue = (pxValue / (float) designWidth) * sw;
            BigDecimal bigDecimal = new BigDecimal(dpValue);
            float finDp = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).floatValue();
            return finDp;
        }

        /**
         * 生成所有的尺寸数据
         */
        private static String makeAllDimens(DimenTypes type, int designWidth) {
            float dpValue;
            String temp;
            StringBuilder sb = new StringBuilder();
            try {
                sb.append(XML_HEADER);
                sb.append(XML_RESOURCE_START);
                //备份生成的相关信息
                temp = String.format(XML_BASE_DPI, type.getSwWidthDp());
                sb.append(temp);
                for (int i = 0; i <= MAX_SIZE; i++) {

                    dpValue = px2dip((float) i, type.getSwWidthDp(), designWidth);
                    temp = String.format(XML_DIMEN_TEMPLETE, "", i, dpValue);
                    sb.append(temp);
                }
                sb.append(XML_RESOURCE_END);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return sb.toString();
        }

        /**
         * 生成的目标文件夹,只需传宽进来就行
         * @param type     枚举类型
         * @param buildDir 生成的目标文件夹
         */
        public static void makeAll(int designWidth, DimenTypes type, String buildDir) {
            try {
                //生成规则
                final String folderName;
                if (type.getSwWidthDp() > 0) {
                    //适配Android 3.2+
                    folderName = "values-sw" + type.getSwWidthDp() + "dp";
                } else {
                    return;
                }
                //生成目标目录
                File file = new File(buildDir + File.separator + folderName);
                if (!file.exists()) {
                    file.mkdirs();
                }
                //生成values文件
                FileOutputStream fos = new FileOutputStream(file.getAbsolutePath() + File.separator + XML_NAME);
                fos.write(makeAllDimens(type, designWidth).getBytes());
                fos.flush();
                fos.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4.3 density 字节跳动密度适配

点击跳转

4.3.1 问题原因

由于不同设备的屏幕尺寸和分辨率各不相同且毫无规律,导致 DPI 的值非常混乱从而影响 dp 适配。例如 UI 图按照屏幕宽度为 360dp 设计的,那么在一个 5 寸 1080*1920 的设备上屏幕的宽度为 392.7dp 是比设计图要宽的,这种情况下使用 dp 也无法统一显示效果。

4.3.2 解决思路

        通常情况下只需要以宽或高一个维度去适配,如界面是上下滑动的只需要保证所有设备宽度显示一致,又如界面不支持滑动则需要保证所有设备高度显示一致(避免有的设备显示不全)。考虑现在基本都是以 dp 为单位,从公式 dp = px / density 中可以看出,如果 UI 图宽度为 360dp 想要保证所有设备计算得出的 px 正好是屏幕宽度上的像素点的话,只能通过修改 density 的值。

        源码中得知 dp 和 px 的转换都是通过 DisplayMetric 来计算的,density 是 DisplayMetric 中的成员变量,该类的实例通过 Resource.getDisplayMetrics() 获得,而 Resource 又是通过 Context 获得的。其它成员变量 densityDpi 对应 dpi,scaledDensity 对应字体缩放因子(正常情况下等于 density,调节系统字体大小后会改变该值)。

4.3.3 代码(Kotlin)

object DensityUtils {
    private var noncompatDensity = 0F
    private var noncompatScaledDensity = 0F

    private fun setCustomDensity(activity: Activity, application: Application) {
        val appDisplayMetrics = application.resources.displayMetrics
        if (noncompatDensity == 0F) {
            noncompatDensity = appDisplayMetrics.density
            noncompatScaledDensity = appDisplayMetrics.scaledDensity
            application.registerComponentCallbacks(object : ComponentCallbacks {
                override fun onConfigurationChanged(newConfig: Configuration) {
                    if (newConfig.fontScale > 0 ) {
                        noncompatScaledDensity = application.resources.displayMetrics.scaledDensity
                    }
                }
                override fun onLowMemory() {}
            })
        }
        val targetDensity = appDisplayMetrics.widthPixels / 360F
        val targetScaledDensity = targetDensity * (noncompatScaledDensity / noncompatDensity)
        val targetDensityDpi = (targetDensity * 160).toInt()

        appDisplayMetrics.density = targetDensity
        appDisplayMetrics.scaledDensity = targetScaledDensity
        appDisplayMetrics.densityDpi = targetDensityDpi

        val activityDisplayMetrics = activity.resources.displayMetrics
        activityDisplayMetrics.density = targetDensity
        activityDisplayMetrics.scaledDensity = targetScaledDensity
        activityDisplayMetrics.densityDpi = targetDensityDpi
    }
}

4.3.4 代码(Java)

public class DensityUtils {
    private static float sNoncompatDensity;
    private static float sNoncompatScaledDensity;

    private static void setCustomDensity(@NonNull Activity activity, @NonNull Application application) {
        final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

        if (sNoncompatDensity == 0) {
            sNoncompatDensity = appDisplayMetrics.density;
            sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
            application.registerComponentCallbacks(new ComponentCallbacks() {
                @Override
                public void onConfigurationChanged(@NonNull Configuration newConfig) {
                    if (newConfig != null && newConfig.fontScale > 0) {
                        sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }
                @Override
                public void onLowMemory() {}
            });
        }
        final float targetDensity = appDisplayMetrics.widthPixels / 360F;
        final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
        final int targetDensityDpi = (int)(targetDensity * 160);

        appDisplayMetrics.density = targetDensity;
        appDisplayMetrics.scaledDensity = targetScaledDensity;
        appDisplayMetrics.densityDpi = targetDensityDpi;

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值