前言

首先是今日说法很早就提到的适配方案一种极低成本的Android屏幕适配方式
原理是修改手机屏幕的density和dpi实现所有屏幕的宽度都被强制设置成和设计图上面的宽度一致。这个方案是和在开发中xml布局查看时切换不同的屏幕时效果。

下面可以看下xml布局查看的效果

Android 适配头条 android 今日头条屏幕适配_屏幕适配

Nexus4

Android 适配头条 android 今日头条屏幕适配_屏幕适配_02

Nexus5

Android 适配头条 android 今日头条屏幕适配_android_03

Pixel 2

Android 适配头条 android 今日头条屏幕适配_屏幕适配_04

可以看到屏幕变了之后,确实只有宽度随着变动了,高度没有变过。


原理分析

一切以转换公式 px = dp * density 为主。

首先我们先设置设计图宽度为360dp
在density为3的屏幕中,实际px为1080 对应上面的Nexus5(1080)
在density为2的屏幕中,实际px为720 对应上面的Nexus4(768)
在density为2.625的屏幕中,实际px为945 对应上面的Pixel 2(1080)
这就导致了如果我们是定值的dp单位会在不同density的屏幕下出现不同的宽度。

那么按照公式,我们如何让一个设计图上面360dp扩展到全部的屏幕呢?

我们将density = px / dp计算出来,得到如下

  1. 1080 / 360 = 3
  2. 768 / 360 = 2.1333
  3. 1080 / 360 = 3

我们可以看到,明明1和3应该要属于同一个density然而实际一个是3一个是2.625,那是因为dpi和屏幕的尺寸有关。

Android 适配头条 android 今日头条屏幕适配_ide_05

如果我们将所有的屏幕的dpi都设置成符合dp转px的dpi不就可以适配全部的宽度了!

即我们将Nexus4的density 由2改为2.1333,Pixel 2的density 由2.625改为3


代码分析

首先我们要确定到底是系统的dpi是不是DisplayMetrics的值

在源码TypedValue.applyDimension中可以看到sp px 等值都是调用了DisplayMetrics的值

/**
     * Converts an unpacked complex data value holding a dimension to its final floating 
     * point value. The two parameters <var>unit</var> and <var>value</var>
     * are as in {@link #TYPE_DIMENSION}.
     *  
     * @param unit The unit to convert from.
     * @param value The value to apply the unit to.
     * @param metrics Current display metrics to use in the conversion -- 
     *                supplies display density and scaling information.
     * 
     * @return The complex floating point value multiplied by the appropriate 
     * metrics depending on its unit. 
     */
    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;
    }

那么按照上面的原理分析,我们就可以开始为了适配宽度进行代码编写了。
我先将我写好的工具类AutoScreenDpUtils贴下

package com.gjn.autoscreenlibrary;

import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import android.util.Log;

/**
 * @author gjn
 * @time 2018/8/22 18:26
 */

public class AutoScreenDpUtils {
    private static final String TAG = "AutoScreenDpUtils";

    private final static String WIDTH_DP = "set_width_dp";

    public static boolean isDebug = false;

    private static Application mApplication;

    private static float w = 0;

    private static float oldDensity = -1;
    private static int oldDensityDpi = -1;
    private static float oldScaledDensity = -1;

    private static float newDensity = -1;
    private static int newDensityDpi = -1;
    private static float newScaledDensity = -1;

    public static void init(@NonNull Application application) {
        init(application, 0);
    }

    public static void init(@NonNull Application application, boolean debug) {
        init(application, 0, debug);
    }

    public static void init(@NonNull Application application, float widthDp) {
        init(application, widthDp, false);
    }

    public static void init(@NonNull Application application, float widthDp, boolean debug) {
        isDebug = debug;
        if (widthDp != 0) {
            w = widthDp;
        } else {
            try {
                ApplicationInfo info = application.getPackageManager().getApplicationInfo(application.getPackageName(), PackageManager.GET_META_DATA);
                w = info.metaData.getInt(WIDTH_DP, 0);
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        }
        if (w == 0) {
            Log.e(TAG, "w is null.");
            return;
        }
        if (mApplication == null) {
            mApplication = application;
        }
        log("-----------------------------------------------");
        log("默认AutoScreenDpUtils.isDebug为false");
        initApplication();
        log("-----------------------------------------------");
    }

    private static void initApplication() {
        final DisplayMetrics metrics = mApplication.getResources().getDisplayMetrics();
        if (oldDensity == -1) {
            oldDensity = metrics.density;
            newDensity = metrics.widthPixels / w;
        }
        if (oldDensityDpi == -1) {
            oldDensityDpi = metrics.densityDpi;
            newDensityDpi = (int) (newDensity * 160);
        }
        if (oldScaledDensity == -1) {
            oldScaledDensity = metrics.scaledDensity;
            newScaledDensity = newDensity * (oldScaledDensity / oldDensity);
        }
        mApplication.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(Configuration newConfig) {
                if (newConfig != null && newConfig.fontScale > 0) {
                    log("fontScale is change.");
                    newScaledDensity = newDensity * newConfig.fontScale;
                    oldScaledDensity = oldDensity * newConfig.fontScale;
                }
            }

            @Override
            public void onLowMemory() {
            }
        });
        log("oldDensity = " + oldDensity);
        log("oldDensityDpi = " + oldDensityDpi);
        log("oldScaledDensity = " + oldScaledDensity);
        log("newDensity => " + metrics.widthPixels + " / " + w + " = " + newDensity);
        log("newDensityDpi => " + newDensity + " * 160 = " + newDensityDpi);
        log("newScaledDensity => " + newDensity + " * (" + oldScaledDensity + " / " + oldDensity + ") = " + newScaledDensity);
    }

    public static void setCustomDensity(@NonNull final Activity activity) {
        if (mApplication == null) {
            Log.e(TAG, "application is null.");
            return;
        }
        changeDensity(activity);
    }

    private static void changeDensity(@NonNull Activity activity) {
        float density;
        int densityDpi;
        float scaledDensity;
        final DisplayMetrics activityMetrics = activity.getResources().getDisplayMetrics();
        if (activity instanceof IAutoCancel) {
            density = oldDensity;
            densityDpi = oldDensityDpi;
            scaledDensity = oldScaledDensity;
            log("default Density");
        }else if (activity instanceof IAutoChange) {
            density = activityMetrics.widthPixels / ((IAutoChange) activity).newWidth();
            densityDpi = (int) (density * 160);
            scaledDensity = densityDpi * (oldScaledDensity / oldDensity);
            log("new dp width = " + ((IAutoChange) activity).newWidth());
            log("new Density = " + density);
        }else {
            density = newDensity;
            densityDpi = newDensityDpi;
            scaledDensity = newScaledDensity;
            log("new dp width = " + w);
            log("new Density = " + density);
        }
        activityMetrics.density = density;
        activityMetrics.densityDpi = densityDpi;
        activityMetrics.scaledDensity = scaledDensity;
    }

    private static void log(String msg) {
        if (isDebug) {
            Log.d(TAG, msg);
        }
    }

}

下面是流程整理

  • 保存旧参数

我们先将旧的densitydensityDpiscaledDensity保存下来,随时为了还原适配。
既代码中的

oldDensity = metrics.density;
oldDensityDpi = metrics.densityDpi;
oldScaledDensity = metrics.scaledDensity;
  • 生成新参数

由当前的widthPixels除去设计图的dp宽度生成新的density
然后由新的density生成新的densityDpi和新的scaledDensity
既代码中的

newDensity = metrics.widthPixels / w;
newDensityDpi = (int) (newDensity * 160);
newScaledDensity = newDensity * (oldScaledDensity / oldDensity);

这边说下为何要用旧的计算一下,因为正常scaledDensity是等于density但是如果用户修改过系统的字体大小。那么他们之间是有差值的,需要先用旧的计算出差值在设置成新的。

  • 监听系统修改变化

这边主要是为了监听修改字体后的变化。
既代码中的

mApplication.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(Configuration newConfig) {
                if (newConfig != null && newConfig.fontScale > 0) {
                    log("fontScale is change.");
                    newScaledDensity = newDensity * newConfig.fontScale;
                    oldScaledDensity = oldDensity * newConfig.fontScale;
                }
            }

            @Override
            public void onLowMemory() {
            }
        });
  • 取消适配和单独适配说明

这边写了两个interface用来区别是否适配和单独修改适配。
IAutoCancel代表取消适配
IAutoChange代表修改新的适配宽度

  • 使用

按照上面的流程,我们有了新的参数和旧的参数。只要在需要调用适配的地方加入AutoScreenDpUtils.setCustomDensity(@NonNull final Activity activity)就可以实现适配了。
注: 这句话需要写在setContentView之前


工具类使用

  • 依赖
allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}


dependencies {
    implementation 'com.github.Gaojianan2016:AutoScreenDpUtils:1.0.2'
}
  • 自定义Application
public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //第二个参数为设计图上面的宽度以dp为单位 第三个参数为显示log
        AutoScreenDpUtils.init(this, 384, true);
    }
}

或者直接

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        AutoScreenDpUtils.init(this, true);
    }
}

在清单文件中加入

<application
  .....>
  <meta-data android:name="set_width_dp" android:value="384" />
</application>

在Activity设置布局之前加入AutoScreenDpUtils.setCustomDensity(this);


效果

在小米max中

Android 适配头条 android 今日头条屏幕适配_屏幕适配_06

第一个Activity取消了适配

Android 适配头条 android 今日头条屏幕适配_屏幕适配_07

第二个Activity进行适配

Android 适配头条 android 今日头条屏幕适配_ide_08

以上就是工具类里面的demo使用而已。


资料

骚年你的屏幕适配方式该升级了!-今日头条适配方案
一种极低成本的Android屏适配方式