Android听说你还在用dp单位做屏幕适配?

前言

屏幕适配一直是Android开发人员躲避不开的话题,更多的同学使用dp单位结合权重去做屏幕适配,但是当设备的物理尺寸存在差异的时候,dp就显得无能为力了。为4.3寸屏幕准备的UI,运行在5.0寸的屏幕上,很可能在右侧和下侧存在大量的空白。而5.0寸的UI运行到4.3寸的设备上,很可能显示不下。也有同学使用GooGle的百分比布局,但是实践过程中需要增加代码量,也没有那么简单高效,有没有一种无脑按照UI设计图设置宽高就能完美适配不同机型的方案?

思路

我们可以按照百分比布局的思想创造出一个全新的单位变量,来衡量屏幕到底是多少份,无论是480 x 800 还是720 x 1280 甚至1080 x 1920分辨率的手机,我们都可以把他的宽分成100份,高分成100份。
\color{red}{以480*800的手机来说}
一份宽就是480/100 = \color{red}{4.8像素}
一份高就是800/100 = \color{red}{8像素}
\color{red}{以720*1280的手机来说:}
一份宽就是720/100 = \color{red}{ 7.2像素}
一份高就是1280/100 = \color{red}{12.8像素}

在布局的时候,比如设计稿上需要一个View宽占屏幕宽度一半,高也占屏幕一半 如下图


image.png

那我们就已经知道宽设置成50份,高也设置成50份,就可以完美适配所有分辨率的屏幕。

 android:layout_width="50份"
 android:layout_height="50份"

\color{red}{对于480 * 800的机型}
宽实际上就是50份x\color{red}{4.8像素} = 240像素
高实际上就是50份x \color{red}{8像素} = 400像素
\color{red}{对于720*1280的机型}
宽实际上就是50份x \color{red}{7.2像素} = 360像素
高实际上就是50份x \color{red}{12.8像素} = 640像素

伪实践

按照UI设计师给我们的蓝湖设计稿为例

蓝湖设计稿.png

UI设计师给我们了360*640设计原稿 那么我们可以把宽分成360份,高分成640份。

\color{red}{以480*800的手机来说}
一份宽就是480/360 = \color{red}{1.33像素}
一份高就是800/640 = \color{red}{1.25像素}
\color{red}{以720*1280的手机来说:}
一份宽就是720/360 = \color{red}{ 2像素}
一份高就是1280/640 = \color{red}{2像素}

蓝湖设计稿2.png

那么我们在写这个View宽高布局的时候如下

  android:layout_width="48份"
  android:layout_height="24份"

工程实践

工程方案1(已经过多个项目实践)

针对你所需要适配的手机屏幕的分辨率各自建立一个value文件

image.png

比如以蓝湖设计稿320*480的分辨率为基准

  • 宽度为320,将任何分辨率的宽度分为320份,取值为x1-x320
  • 高度为480,将任何分辨率的高度分为480份,取值为y1-y480

x1相当于1份宽
y1相当于1份高

例如对于800*480的宽度480:

image.png

可以看到x1 = 480 / 基准 = 480 / 320 = 1.5 以此类推

假设我现在需要在屏幕中心有个按钮,宽度和高度为我们屏幕宽度的1/2,我可以怎么编写布局文件呢?

<FrameLayout>
    <Button
        android:layout_gravity="center"
        android:gravity="center"
        android:text="@string/hello_world"
        android:layout_width="@dimen/x160"
        android:layout_height="@dimen/x160"/>
</FrameLayout>

可以看到我们的宽度和高度定义为x160,其实就是宽度的50%

不同机型的效果图


image.png

好了,有个最主要的问题,就是分辨率这么多,难道我们要自己计算,然后手写?

\color{red}{下载自动生成工具}

jar包内置了常用的分辨率,默认基准为480*320,当然对于特殊需求,通过命令行指定即可

例如基准 1280 * 800 ,额外支持尺寸:1152 * 735;4500 * 3200

  • java -jar xx.jar width height width,height_width,height
20150503173911632.gif

这样拷贝到自己的项目中就可以使用了

缺点

  • 新建了很多value文件,给apk新增了3-4M的体积
  • 默认基准分辨率只能适配99%的机型,如果遇到value里面不存在的分辨率则不能适配,需要动态增加该分辨率下的value文件

工程方案2(头条的适配方案)

大致思路:通过重写Activity的getResources(),重写冷门单位pt作为基准单位,1pt代表前文提到的一份

  • AdaptScreenUtils
public final class AdaptScreenUtils {
    private static List<Field> sMetricsFields;

    private AdaptScreenUtils() {
        throw new UnsupportedOperationException("u can't instantiate me...");
    }

    /**
     * Adapt for the horizontal screen, and call it in {@link android.app.Activity#getResources()}.
     */
    public static Resources adaptWidth(final Resources resources, final int designWidth) {
        float newXdpi = (resources.getDisplayMetrics().widthPixels * 72f) / designWidth;
        applyDisplayMetrics(resources, newXdpi);
        return resources;
    }

    /**
     * Adapt for the vertical screen, and call it in {@link android.app.Activity#getResources()}.
     */
    public static Resources adaptHeight(final Resources resources, final int designHeight) {
        return adaptHeight(resources, designHeight, false);
    }

    /**
     * Adapt for the vertical screen, and call it in {@link android.app.Activity#getResources()}.
     */
    public static Resources adaptHeight(final Resources resources, final int designHeight, final boolean includeNavBar) {
        float screenHeight = (resources.getDisplayMetrics().heightPixels
                + (includeNavBar ? getNavBarHeight(resources) : 0)) * 72f;
        float newXdpi = screenHeight / designHeight;
        applyDisplayMetrics(resources, newXdpi);
        return resources;
    }

    private static int getNavBarHeight(final Resources resources) {
        int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId != 0) {
            return resources.getDimensionPixelSize(resourceId);
        } else {
            return 0;
        }
    }

    /**
     * @param resources The resources.
     * @return the resource
     */
    public static Resources closeAdapt(final Resources resources) {
        float newXdpi = Resources.getSystem().getDisplayMetrics().density * 72f;
        applyDisplayMetrics(resources, newXdpi);
        return resources;
    }

    /**
     * Value of pt to value of px.
     *
     * @param ptValue The value of pt.
     * @return value of px
     */
    public static int pt2Px(final float ptValue) {
        DisplayMetrics metrics = FWAdSDK.sContext.getResources().getDisplayMetrics();
        return (int) (ptValue * metrics.xdpi / 72f + 0.5);
    }

    /**
     * Value of px to value of pt.
     *
     * @param pxValue The value of px.
     * @return value of pt
     */
    public static int px2Pt(final float pxValue) {
        DisplayMetrics metrics = FWAdSDK.sContext.getResources().getDisplayMetrics();
        return (int) (pxValue * 72 / metrics.xdpi + 0.5);
    }

    private static void applyDisplayMetrics(final Resources resources, final float newXdpi) {
        resources.getDisplayMetrics().xdpi = newXdpi;
        FWAdSDK.sContext.getResources().getDisplayMetrics().xdpi = newXdpi;
        applyOtherDisplayMetrics(resources, newXdpi);
    }

    static void preLoad() {
        applyDisplayMetrics(Resources.getSystem(), Resources.getSystem().getDisplayMetrics().xdpi);
    }

    private static void applyOtherDisplayMetrics(final Resources resources, final float newXdpi) {
        if (sMetricsFields == null) {
            sMetricsFields = new ArrayList<>();
            Class resCls = resources.getClass();
            Field[] declaredFields = resCls.getDeclaredFields();
            while (declaredFields != null && declaredFields.length > 0) {
                for (Field field : declaredFields) {
                    if (field.getType().isAssignableFrom(DisplayMetrics.class)) {
                        field.setAccessible(true);
                        DisplayMetrics tmpDm = getMetricsFromField(resources, field);
                        if (tmpDm != null) {
                            sMetricsFields.add(field);
                            tmpDm.xdpi = newXdpi;
                        }
                    }
                }
                resCls = resCls.getSuperclass();
                if (resCls != null) {
                    declaredFields = resCls.getDeclaredFields();
                } else {
                    break;
                }
            }
        } else {
            applyMetricsFields(resources, newXdpi);
        }
    }

    private static void applyMetricsFields(final Resources resources, final float newXdpi) {
        for (Field metricsField : sMetricsFields) {
            try {
                DisplayMetrics dm = (DisplayMetrics) metricsField.get(resources);
                if (dm != null) dm.xdpi = newXdpi;
            } catch (Exception e) {
                Log.e("AdaptScreenUtils", "applyMetricsFields: " + e);
            }
        }
    }

    private static DisplayMetrics getMetricsFromField(final Resources resources, final Field field) {
        try {
            return (DisplayMetrics) field.get(resources);
        } catch (Exception e) {
            Log.e("AdaptScreenUtils", "getMetricsFromField: " + e);
            return null;
        }
    }
}
  • 使用方法

以宽度320为基准

 @Override
    public Resources getResources() {
        return AdaptScreenUtils.adaptWidth(super.getResources(),320);
    }

假设我现在需要在屏幕中心有个按钮,宽度和高度为我们屏幕宽度的1/2,我可以怎么编写布局文件呢?

<FrameLayout>
    <Button
        android:layout_gravity="center"
        android:gravity="center"
        android:text="@string/hello_world"
        android:layout_width="160pt"
        android:layout_height="160pt"/>
</FrameLayout>

优点

1. 无侵入性
用了这个之后你依然可以使用dp包括其他任何单位,对你从前使用的布局不会造成任何影响,在老项目中开发新功能你可以胆大地加入该适配方案,新项目的话更可以毫不犹豫地采用该适配,并且在关闭该关闭后,pt 效果等同于 dp 哦。

2. 灵活性高
如果你想要对某个 View 做到不同分辨率的设备下,使其尺寸在适配维度上所占比例一致的话,那么对它使用 pt 单位即可,如果你不想要这样的效果,而是想要更大尺寸的设备显示更多的内容,那么你可以像从前那样写 dpsp 什么的即可,结合这两点,在界面布局上你就可以游刃有余地做到你想要的效果。

3. 不会影响系统 View 和三方 View 的大小
这点其实在无侵入性中已经表现出来了,由于头条的方案是直接修改 DisplayMetrics#densitydp 适配,这样会导致系统 View 尺寸和原先不一致,比如 DialogToast、 尺寸,同样,三方 View 的大小也会和原先效果不一致,这也就是我选择 pt 适配的原因之一。

4. 不会失效
因为不论头条的适配还是 AndroidAutoSize,都会存在 DisplayMetrics#density 被还原的情况,需要自己重新设置回去,最显著的就是界面中存在 WebView 的话,由于其初始化的时候会还原 DisplayMetrics#density 的值导致适配失效,当然这点已经有解决方案了,但还会有很多其他情况会还原 DisplayMetrics#density 的值导致适配失效。而我这方案就是为了解决这个痛点,不让 DisplayMetrics 中的值被还原导致适配失效。

效果

480 x 800 - mdpi(160dpi)

image

720 x 1280 - xhdpi(320dpi)

image

1080 x 1920 - xxhdpi(480dpi)

image

1440x2560 - 560dpi

image

可以看到效果图中 WebView 对之后的 View 并没有产生适配失效的问题,这是之前适配所不能解决的问题。

如何创建预览?

在 AS 中 Tools -> AVD Manager -> Create Virtual Device...,我们以适配 1080 x 1920px 为例,具体操作如下:

image

创建完设备我们在预览界面选中这个设备即可看到 pt 单位效果。

注:有的设备需要重启电脑才可以选中设备效果

设计师给你的设计图尺寸是多少,那你就建多少尺寸的设备即可,比如是 720 x 1280px 的,那你把上图的尺寸换成 7201280,再计算下屏幕尺寸即可,如果是 360 x 640dp 的话,那就把上图的尺寸换成 360640,再计算下屏幕尺寸即可,不用去 care 单位到底是什么,设计图标注多少那你就写多少即可,无需换算。适配的时候传入这个维度的尺寸值即可,比如 720 x 1280 的宽度适配,那就传入 720 即可。

参考链接

Android 屏幕适配方案

Android 屏幕适配终结者

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。