Android全面的屏幕适配方案解析(四)__今日头条适配方案

之前三篇把屏幕适配概念梳理了还讲解了列举的其中四种适配方案,还没有看过的童鞋可以先参考这三篇:

Android全面的屏幕适配方案解析(一)__屏幕适配概念梳理

Android全面的屏幕适配方案解析(二)__宽高限定符屏幕适配

Android全面的屏幕适配方案解析(三)__sw限定符适配方案

下面列举常用的适配方案:

  • dp适配方案
  • 宽高限定符适配方案
  • AndroidAutoLayout适配方案
  • sw限定符适配方案
  • 今日头条适配方案
  • AndroidAutoSize适配方案

这里还是有必要重申一下,有些过时的适配方案这里还讲解啊,只能说每种适配方案都会有各自的优缺点,从最原始的适配方案讲起,才能更好的理解为啥会衍生出各种适配方案,话不多说,下面继续讲解。

今日头条适配方案

今日头条适配方案原理解析

今日头条适配方案原理在于通过公式density = 设备真实宽度(单位px)/设计图总宽度(单位dp),在确保设计图总宽度(单位dp)一定时,通过修改density值,确保所有不同尺寸分辨率设备计算出的真实宽度值正好是屏幕宽度,这样就能达到适配所有设备的目的啦。

举个例子:比如UI设计稿总宽度为360dp,这里有两台不同尺寸分辨率的设备:

设备1:分辨率1080x1920,dpi为480,正常情况下计算density=dpi/160=480/160=3,此时屏幕总宽度dp=px/density=1080/3=360;

设备2:分辨率1440x2560,dpi为560,正常情况下计算desity=dpi/160=560/160=3.5,此时屏幕总宽度dp=px/density=1440/3.5=411;

正常情况下density 在每个设备上都是固定的,那要是我们想确保设计稿总宽度360不变,再来看看density值:

设备1:分辨率1080x1920,dpi为480,计算density = 设备真实宽度(单位px)/设计图总宽度(单位dp) = 1080/360 = 3,此时屏幕总宽度dp=px/density=1080/3=360;

设备2:分辨率1440x2560,dpi为560,计算density = 设备真实宽度(单位px)/设计图总宽度(单位dp) = 1440/360 = 4,此时屏幕总宽度dp=px/density=1440/4=360;

通过对比可以看出,修改density值,确实是能确保不同分辨率设备总宽度值始终是360dp,这样就能保证UI在不同的设备上显示效果是一致的。

今日头条适配方案推导过程

通过上面例子我们知道需要修改density值,那下面我们就来看看怎么修改系统的density值: 首先我们得知道无论在布局文件中填写的是什么样单位的值,比如10dp、10sp、10px等等,最后都会被系统转换成px,这个时候有童鞋就问了,你怎么知道啊?当然是通过布局文件中dp转换源码知道的啦。

通过源码可以知道,dp转换最终都是调用系统工具类Typedvalue类中的applyDimension方法进行转换的:

      /**
     * @param unit 要转换的单位
     * @param value 单位对应的值
     * @param metrics 显示指标
     */
	public static float applyDimension(int unit, float value,DisplayMetrics metrics){
        switch (unit) {
        case COMPLEX_UNIT_PX://单位为px
            return value;
        case COMPLEX_UNIT_DIP://单位为dp
            return value * metrics.density;
        case COMPLEX_UNIT_SP://单位为sp
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT://单位为pt
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN://单位为in
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM://单位为mm
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }
复制代码

系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为px,在applyDimension方法中有个参数DisplayMetrics,我们来看看这个参数的含义:

 @param metrics Current display metrics to use in the conversion -- supplies display density and scaling information.
复制代码

这句话的意思是转换中使用的当前显示指标,提供显示密度和缩放信息。通过applyDimension方法中dp转换:

public static float applyDimension(int unit, float value,DisplayMetrics metrics){
        switch (unit) {
      	......
        case COMPLEX_UNIT_DIP://单位为dp
            return value * metrics.density;
       	......
    }
复制代码

可以发现有我们需要的density值,通过Ctrl键追踪位置发现这个值是系统工具类DisplayMetrics类的成员变量,如图所示:

image.png

而DisplayMetrics实例可以通过系统资源文件Resources类中的getDisplayMetrics方法获得,系统资源文件Resouces也可以通过Activity或者Application的Context获得:

DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
复制代码

然后就可以通过DisplayMetrics实例,修改density值啦,这里需要注意有个scaledDensity变量的影响,这个变量是字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值,防止用户调节了系统字体。

今日头条适配的最终方案:

这里是以设计图总宽度360dp来适配,接下来只需要把我们计算好的 density 在系统中修改下即可,代码实现如下:

  	/**
     * 今日头条适配方案
     *
     * @param activity
     * @param application
     */
    public static void setCustomDensity(Activity activity, final Application application) {
        //通过资源文件getResources类获取DisplayMetrics
        DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
        if (sNoncompatDensity == 0) {
            //保存之前density值
            sNoncompatDensity = appDisplayMetrics.density;
            //保存之前scaledDensity值,scaledDensity为字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值
            sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
            //监听设备系统字体切换
            application.registerComponentCallbacks(new ComponentCallbacks() {

                public void onConfigurationChanged(Configuration newConfig) {
                    if (newConfig != null && newConfig.fontScale > 0) {
                        //调节系统字体大小后改变的值
                        sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                public void onLowMemory() {

                }
            });
        }

        //获取以设计图总宽度360dp下的density值
        float targetDensity = appDisplayMetrics.widthPixels / 360;
        //通过计算之前scaledDensity和density的比获得scaledDensity值
        float targetScaleDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
        //获取以设计图总宽度360dp下的dpi值
        int targetDensityDpi = (int) (160 * targetDensity);
        //设置系统density值
        appDisplayMetrics.density = targetDensity;
        //设置系统scaledDensity值
        appDisplayMetrics.scaledDensity = targetScaleDensity;
        //设置系统densityDpi值
        appDisplayMetrics.densityDpi = targetDensityDpi;

        //获取当前activity的DisplayMetrics
        final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
        //设置当前activity的density值
        activityDisplayMetrics.density = targetDensity;
        //设置当前activity的scaledDensity值
        activityDisplayMetrics.scaledDensity = targetScaleDensity;
        //设置当前activity的densityDpi值
        activityDisplayMetrics.densityDpi = targetDensityDpi;
    }
复制代码

上面代码是今日头条的最终方案,为了方便阅读,我只是基于个人理解做了标注解析,不对的地方请大佬们指正。

今日头条适配步骤:

下面来看验证一下这种适配方案的可行性。

1、布局文件,这里沿用之前的布局,只有一张宽高为150x150(dp)的图片,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">

    <ImageView
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_centerInParent="true"
        android:src="@mipmap/ic_launcher" />

</RelativeLayout>
复制代码

2、这里将今日头条适配方案封装成了工具类,如下所示:

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

    /**
     * 今日头条适配方案
     *
     * @param activity
     * @param application
     */
    public static void setCustomDensity(Activity activity, final Application application) {
        //通过资源文件getResources类获取DisplayMetrics
        DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
        if (sNoncompatDensity == 0) {
            //保存之前density值
            sNoncompatDensity = appDisplayMetrics.density;
            //保存之前scaledDensity值,scaledDensity为字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值
            sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
            //监听设备系统字体切换
            application.registerComponentCallbacks(new ComponentCallbacks() {

                public void onConfigurationChanged(Configuration newConfig) {
                    if (newConfig != null && newConfig.fontScale > 0) {
                        //调节系统字体大小后改变的值
                        sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                public void onLowMemory() {

                }
            });
        }

        //获取以设计图总宽度360dp下的density值
        float targetDensity = appDisplayMetrics.widthPixels / 360;
        //通过计算之前scaledDensity和density的比获得scaledDensity值
        float targetScaleDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
        //获取以设计图总宽度360dp下的dpi值
        int targetDensityDpi = (int) (160 * targetDensity);
        //设置系统density值
        appDisplayMetrics.density = targetDensity;
        //设置系统scaledDensity值
        appDisplayMetrics.scaledDensity = targetScaleDensity;
        //设置系统densityDpi值
        appDisplayMetrics.densityDpi = targetDensityDpi;

        //获取当前activity的DisplayMetrics
        final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
        //设置当前activity的density值
        activityDisplayMetrics.density = targetDensity;
        //设置当前activity的scaledDensity值
        activityDisplayMetrics.scaledDensity = targetScaleDensity;
        //设置当前activity的densityDpi值
        activityDisplayMetrics.densityDpi = targetDensityDpi;
    }
}
复制代码

3、在Activity中的onCreate方法中调用,如下所示:

CustomDensityUtil.setCustomDensity(MainActivity.this, getApplication());
复制代码

到这里就完成了,是不是挺简单的呢,下面来看看在分辨率:480x800、720x1280、1080x1920的测试显示效果图:

image.png

再来看一下没有使用今日头条适配方案之前不同手机的测试对比效果:

image.png

根据适配前后的对比效果还是挺明显的。

今日头条适配方案优点

1、侵入性很低,而且没有涉及私有API,该方案与项目完全解耦,今日头条大厂在使用,稳定性有保证。

2、使用成本非常低,操作简单方便。

3、接入没有任何的性能损耗,使用的都是系统API。

今日头条适配方案缺点

1、只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反映了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的。比如项目中使用了第三方库控件等不是我们项目自身设计的控件,这时就会出现和我们项目自身的设计图尺寸差距非常大的问题。

2、使用过程中需要进行registerComponentCallbacks监听内容文字的大小改变情况,解决退出应用修改文字大小后,会出现文字大小不改变的情况。

AndroidAutoSize适配方案

所谓的AndroidAutoSize适配方案其实就是今日头条适配方案的升级版,是基于今日头条适配方案进行拓展的开源库,该库在很大程度上解决了今日头条适配方案的缺点,使用方式也比较简单,只需要填写设计图尺寸这一步即可接入项目,还支持对Activity、Fragment进行取消适配,灵活性会更强。

下面还是根据实例来讲解集成过程:

1、根目录build.gradle添加:

allprojects {
    repositories {
        ...
        maven { url "https://jitpack.io" }
    }
}
复制代码

2、添加该适配框架依赖

 implementation 'com.github.JessYanCoding:AndroidAutoSize:v1.2.1'
复制代码

3、在项目中的AndroidManifest配置文件注明设计稿的尺寸,这里测试以360x640为例:

<manifest>
    <application>            
        <meta-data
            android:name="design_width_in_dp"
            android:value="360"/>
        <meta-data
            android:name="design_height_in_dp"
            android:value="640"/>           
     </application>           
</manifest>
复制代码

如果只是想使用AndroidAutoSize适配方案的基础功能,AndroidAutoSize框架的使用方法在这里就结束了,只需要上面这一步,即可帮助你以最简单的方式接入AndroidAutoSize适配框架,我这里只做适配方案有效性演示,需要拓展需求的请参考下面Github地址有详细介绍。

4、测试布局文件非常的简单,只设置了图片,为了突出跟今日头条适配方案测试结果不同,这里设置图片宽高为120x120(dp),测试布局如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">

    <ImageView
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_centerInParent="true"
        android:src="@mipmap/ic_launcher" />

</RelativeLayout>
复制代码

我们来看看这个实例在不同手机的测试对比效果图:

image.png

再来看一下没有使用AndroidAutoSize适配方案之前不同手机的测试对比效果:

image.png

根据适配前后的对比效果还是挺明显的,AndroidAutoSize适配方案是根据今日头条屏幕适配方案官方公布的 30 行不到的代码,经过不断的优化和扩展,发展成了现在拥有将近20个类文件,上千行代码的全面性屏幕适配框架,在迭代的过程中完善和优化了很多功能,相比今日头条屏幕适配方案官方公布的原始代码,AndroidAutoSize适配方案更加稳定、更加易用、更加强大,感兴趣的可以去阅读源码,注释非常详细,这里就不做过多的介绍啦,点击进入源码地址

以上六种适配方案就全部讲解完了,适配框架因为原作者已经讲解的很清楚了,我这里就只讲解了基本用法,其它几种都尽可能讲解的详细一点,我这里讲解并没有说哪种方案更好或者更坏,每个项目的需求都不一样,适合的才是最好的。欢迎关注公众号【龙旋】能获取最新更新内容哦。

Guess you like

Origin juejin.im/post/7054388007373111310