Android 今日头条屏幕适配详细使用攻略


首先感谢大神JessYan的创神之作《AndroidAutoSize》,大神以今日头条屏幕适配的核心代码为基础进行了扩展封装,产生了《AndroidAutoSize》这个能快速接入使用的屏幕适配方案,这个屏幕适配方案是我遇到的截止2020.9.15为止最强大、简单有效的屏幕适配方案。我已使用该方案有一年,在使用过程未发现有何问题,强烈推荐各位极客们使用学习。

以下是大神JessYan的相关地址:
邮箱:[email protected]
github:https://github.com/JessYanCoding/AndroidAutoSize
简书:https://www.jianshu.com/p/4aa23d69d481
原始核心代码:https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA

大神的源码都在github中各位可以自行下载,我写这篇博客的目的就是记录使用心得,并将该框架的重要类和方法使用进行详细说明,大神的文章中对细节问题写的比较少,我对其进行了细微的修改和详细的注解解释。

屏幕适配相关知识点

像素
通常所说的像素,就是CCD/CMOS上光电感应元件的数量,一个感光元件经过感光,光电信号转换,A/D转换等步骤以后,在输出的照片上就形成一个点,我们如果把影像放大数倍,会发现这些连续色调其实是由许多色彩相近的小方点所组成,这些小方点就是构成影像的最小单位“像素”(Pixel)。简而言之,像素就是手机屏幕的最小构成单元。

屏幕尺寸
屏幕尺寸指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米。比如常见的屏幕尺寸有2.4、2.8、3.5、3.7、4.2、5.0、5.5、6.0等

屏幕分辨率
屏幕分辨率是指在横纵向上的像素点数,单位是px,1px=1个像素点。一般以纵向像素横向像素,如19201080

屏幕像素密度(dpi)
屏幕像素密度是指每英寸上的像素点数,单位是dpi,即“dot per inch”的缩写。屏幕像素密度与屏幕尺寸和屏幕分辨率有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。
在这里插入图片描述

计算公式: 像素密度 = 像素 / 尺寸 (dpi = px / in)
标准屏幕像素密度(mdpi): 每英寸长度上还有160个像素点(160dpi),即称为标准屏幕像素密度(mdpi)。

密度无关像素(dp)
含义:density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关
单位:dp,可以保证在不同屏幕像素密度的设备上显示相同的效果,是安卓特有的长度单位。
场景例子:假如同样都是画一条长度是屏幕一半的线,如果使用px作为计量单位,那么在480x800分辨率手机上设置应为240px;在320x480的手机上应设置为160px,二者设置就不同了;如果使用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度。
dp与px的转换:1dp = (dpi / 160 ) * 1px;

密度类型 代表的分辨率(px) 屏幕像素密度(dpi) 换算
低密度(ldpi) 240 x 320 120 1dp = 0.75px
中密度(mdpi) 320 x 480 160 1dp = 1px
高密度(hdpi) 480 x 800 240 1dp = 1.5px
超高密度(xhdpi) 720 x 1280 320 1dp = 2px
超超高密度(xxhdpi) 1080 x 1920 480 1dp = 3px

独立比例像素(sp)
scale-independent pixel,叫sp或sip,字体大小专用单位 ,Android开发时用此单位设置文字大小,可根据字体大小首选项进行缩放。
推荐使用12sp、14sp、18sp、22sp作为字体大小,不推荐使用奇数和小数,容易造成精度丢失,12sp以下字体太小。

sp与dp的区别
dp只跟屏幕的像素密度有关, sp和dp很类似但唯一的区别是,Android系统允许用户自定义文字尺寸大小(小、正常、大、超大等等),当文字尺寸是“正常”时1sp=1dp=0.00625英寸,而当文字尺寸是“大”""或“超大”时,1sp>1dp=0.00625英寸。类似我们在windows里调整字体尺寸以后的效果——窗口大小不变,只有文字大小改变。


1. 适配原理

Android AutoSize的核心代码来源于字节跳动的微信文章https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA。网上也有多各个大神进行了代码的封装设计,都是万变不离其中。

原理核心

        DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
        if (sNoncompatDensity == 0) {
    
    
            sNoncompatDensity = appDisplayMetrics.density;
            sNoncompatDensity = 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() {
    
    

                }
            });
        }
        float targetDensity = appDisplayMetrics.widthPixels / 360;
        float targetScaleDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
        int targetDensityDpi = (int) (160 * targetDensity);
        appDisplayMetrics.density = targetDensity;
        appDisplayMetrics.scaledDensity = targetScaleDensity;
        appDisplayMetrics.densityDpi = targetDensityDpi;

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

原理很简单,例如一个4.59的10801920的手机它的dpi=480,它的density=480/160,则说明1dp=3px。当我们在布局中给如TextView设置
layout_width=30dp时,在程序运行时会自动计算其对应px单位长度90px,px=dp
density。

今日头条适配方案的核心就是动态计算程序中的density=appDisplayMetrics.widthPixels / 360,360是原始设计图纸的dp。假设原先的设计图纸10801920,现在适配5.99寸560dpi的14402880手机,则30dp=30560/160=105px,实际上屏幕适配要求的30dp=1440/36030=120px才可以达到适配效果。因为120/1440=90/1080,控件在布局中的占宽比是一样的才能达到宽度适配效果。这就是为什么要动态修改全局或activity的DisplayMetrics#density的目的了。

优缺点

优点:

  1. 侵入性非常低,该方案和项目完全解耦,使用的还是Android官方单位
  2. 接入无性能损耗,使用的全是Android官方的API。

缺点:

  1. 项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样,此时会产生适配误差。解决方案就是取消当前 Activity 的适配效果,改用其他的适配方案
  2. 系统修改字体大小后,返回应用系统字体大小还是未改变,需要设置registerComponentCallbacks监听。 Android AutoSize框架已经解决了该问题。
  3. 在使用过程中需要进行registerComponentCallbacks监听内容文字的大小改变情况,解决退出应用修改文字大小后,文字大小不改变的情况。

2. 框配置

依赖配置

  1. 远程依赖,截止到2020.9.15,版本为1.2.1
    implementation 'me.jessyan:autosize:1.2.1'
  1. 从github上下载源码进行library库依赖

参数配置
在AndroidManifest.xml中配置参数

<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>

3. 自定义初始化

本文中使用的框架是经过大神JessYan的封装后成为你所看到的框架。它能根据一套给定的设计图尺寸进行布局展示,当安装当不同分辨率尺寸的设备上时,它能自动适配屏幕。

框架的初始化时机是配置在ContentProvider中,在Application#onCreate()方法之前启动。框架一旦初始化完成,其适配效果会在Activity和Fragment、各种View中自动全局适配。程序将默认是以屏幕宽度为基准进行适配的,并且使用的是在AndroidManifest中填写的全局设计图尺寸进行全局适配。

框架支持dp、sp两个主单位,pt、in、mm三个冷门副单位,如果使用副单位,可以规避系统控件或三方库控件使用的不良影响。

ContentProvider初始化第三方库
ContentProvider是一种共享型组件,它通过Binder向其他组件或者其他应用程序提供数据,当ContentProvider所在进程启动时候,ContentProvider会被同时启动并被发布到AMS中。

ContentProvider的onCreate要优先于Application的onCreate,但在attachBaseContext()之后而执行,它的具体详细启动源码在ActivityThread中。很多人会在ContentProvider#onCreate()初始化第三方库。

一般进行了依赖配置参数配置两操作,Android AutoSize就配置完成可以直接使用了,它的框架源码初始化在InitProvider代码中。

在InitProvider 中已进行了初始化设置

public class InitProvider extends ContentProvider {
    
    
    @Override
    public boolean onCreate() {
    
    
        if (getContext() != null) {
    
    
            Context application = getContext().getApplicationContext();
            if (application == null) {
    
    
                application = AutoSizeUtils.getApplicationByReflect();
            }
            AutoSizeConfig.getInstance()
                    .setLog(true)
                    .init((Application) application)
                    .setUseDeviceSize(false);
            return true;
        }
        return false;
    }

但是为了个性化的配置,我们可以在Application中进行一些自定义设置,设置的方法都应写在Application#onCreate()方法中。

public class Application {
    
    
    @Override
    public void onCreate() {
    
    
        super.onCreate();
        ...
        AutoSize.initCompatMultiProcess(this);
        AutoSize.checkAndInit(this);
        AutoSizeConfig.getInstance()
                .setCustomFragment(true)
                .setExcludeFontScale(true)
                .setPrivateFontScale(0.8f)
                .setLog(false)
                .setBaseOnWidth(true)
                .setUseDeviceSize(true)
                //屏幕适配监听器
                .setOnAdaptListener(new OnAdaptListener() {
    
    
                    @Override
                    public void onAdaptBefore(Object target, Activity activity) {
    
    
//                        AutoSizeConfig.getInstance().setScreenWidth(ScreenUtils.getScreenSize(activity)[0]);
//                        AutoSizeConfig.getInstance().setScreenHeight(ScreenUtils.getScreenSize(activity)[1]);
                        AutoSizeLog.d(String.format(Locale.ENGLISH, "%s onAdaptBefore!", target.getClass().getName()));
                    }

                    @Override
                    public void onAdaptAfter(Object target, Activity activity) {
    
    
                        AutoSizeLog.d(String.format(Locale.ENGLISH, "%s onAdaptAfter!", target.getClass().getName()));
                    }
                });
        configUnits();
    }
    
    private void configUnits() {
    
    
        AutoSizeConfig.getInstance()
                .getUnitsManager()
                .setSupportDP(true)
                .setDesignSize(2160, 3840)
                .setSupportSP(true)
                .setSupportSubunits(Subunits.MM);
    }
}

4. 常用方法解析

对于初始化中方法,我们进行一一分析
1. AutoSize.initCompatMultiProcess(Context context)
当 App 中出现多进程,并且您需要适配所有的进程,就需要在 App 初始化时调用。一般的单进程App程序不用设置。

2. AutoSize.checkAndInit(Application application)

     if (!checkInit()) {
    
    
            AutoSizeConfig.getInstance()
                    .setLog(true)
                    .init(application)
                    .setUseDeviceSize(false);
        }

一般来说Android AutoSize会通过InitProvider实例化自动完成初始化,是不需要调用checkAndInit()方法的。

但由于某些 issues 反应, 可能会在某些特殊情况下出现InitProvider未能正常实例化的情况, 导致 AndroidAutoSize 未能完成初始化。所以需要使用该方法确保Android AutoSize 初始化成功。

3. AutoSizeConfig.getInstance().setCustomFragment(boolean customFragment)
设定是否让框架支持自定义Fragment 的适配参数,一般这个需求比较少。默认不支持的

4. AutoSizeConfig.getInstance().setExcludeFontScale(true)
是否屏蔽系统字体大小对AndroidAutoSize 的影响, 如果为 true, App 内的字体的大小将不会跟随系统设置中字体大小的改变, 如果为 false, 则会跟随系统设置中字体大小的改变, 默认为 false

5. AutoSizeConfig.getInstance().setPrivateFontScale(float fontScale)
区别于系统字体大小的放大比例, AndroidAutoSize 允许 APP 内部可以独立于系统字体大小之外,独自拥有全局调节 APP 字体大小的能力。 fontScale取值0~1,设为 0 则取消此功能。同时字体的单位必须是sp做单位。

6. AutoSizeConfig.getInstance().setLog(boolean log)
设置是否打印AutoSize的日志,true为打印

7. AutoSizeConfig.getInstance().setBaseOnWidth(true)
是否全局按照宽度进行等比例适配,true以宽来适配,false以高来适配

8. AutoSizeConfig.getInstance().stop(this)
自动适配方案可以手动调用方法停止,需要注意的是Android AutoSize暂停只是停止了对后续还没有启动的{@link Activity}进行适配的工作,但对已经启动且已经适配的{@link Activity}不会有任何影响

9. AutoSizeConfig.getInstance().restart()
AutoSize可以暂停适配也可以重启适配,但是重启适配只能对后续还没有启动的 {@link Activity} 进行适配的工作,但对已经启动且在stop期间未适配的{@link Activity}不会有任何影响

10. AutoSizeConfig.getInstance().setUseDeviceSize(true)
是否以屏幕的实际尺寸为高度,默认为false,屏幕的适配高度是屏幕总高度减去状态栏高度。

11. UnitsManager.setSupportSP(boolean supportSP)
是否让框架支持sp单位,默认是为true支持,如果为false,则字体大小最好设置为其他单位才能自动适配

12. UnitsManager.setSupportSubunits(Subunits supportSubunits)
自主设置心仪的副单位,可以从pt、in、mm中进行选择,如果使用了Subunits#NONE即代表不支持副单位

13. UnitsManager.setSupportDP(boolean supportDP)
是否支持dp单位,默认是true支持,如果关闭将不对dp单位进行支持

14. UnitsManager.setDesignSize(float designWidth, float designHeight)
设置设计图尺寸,一般专为副单位尺寸设计,它与AndroidManifest.xml中配置的参数不一样,不会被覆盖。

5. 常见接口及类的使用

CustomAdapt

实现CustomAdapt接口即可对activity和fragment进行新的自定义尺寸适配,适配方向可以自主选择是宽度还是高度。实现该接口会取消默认的适配方案和效果

对于fragment的自定义尺寸需要进行AutoSizeConfig.getInstance().setCustomFragment(true)设置,默认是不支持对fragment的自定义尺寸适配的。

在CustomAdapt接口中需要实现者重写两个方法boolean isBaseOnWidth()和float getSizeInDp(),根据使用者需求自定义。
1. boolean isBaseOnWidth()
为了保证在高宽比不同的屏幕上也能正常适配,所以只能在宽度和高度之中选一个作为基准进行适配。 true为按照宽度适配, false 为按照高度适配

2. float getSizeInDp()
getSizeInDp 须配合isBaseOnWidth()使用, 有如下使用规则:
如果 {@link #isBaseOnWidth()} 返回 {@code true}, {@link CustomAdapt #getSizeInDp} 则应该返回设计图的总宽度。
如果 {@link #isBaseOnWidth()} 返回 {@code false}, {@link CustomAdapt #getSizeInDp} 则应该返回设计图的总高度。
如果您不需要自定义设计图上的设计尺寸, 想继续使用在 AndroidManifest 中填写的设计图尺寸,getSizeInDp 则返回 0即可。

CancelAdapt

接口CancelAdapt没有任何成员变量,支持AndroidAutoSize的项目所有模块默认使用适配功能,第三方库的也不例外。
如果某个页面不想使用适配功能, 请让该页面实现CancelAdapt接口放弃适配,所有的适配效果都将失效。

6.框架核心

1.屏幕适配自定义

通过字节跳动的核心源码,只能进行全局适配,但是该框架中进行了Activity和Fragmen的自定义适配和随时取消恢复适配功能。它的原理是注册了ActivityLifecycleCallbacks,进行了Activity的适配时间精准化自我掌控。

通过注册ActivityLifecycleCallbacks,进行Activity的生命周期进行管理, 当onActivityCreated时,也就是OnCreate()的setContentView之前进行了AutoAdaptStrategy#applyAdapt的调用。这种方案类似于 AOP, 面向接口, 侵入性低, 方便统一管理, 扩展性强。

    @Override
    public void onActivityCreated(@androidx.annotation.NonNull Activity activity, Bundle savedInstanceState) {
    
    
        if (AutoSizeConfig.getInstance().isCustomFragment()) {
    
    
            if (mFragmentLifecycleCallbacksToAndroidx != null && activity instanceof androidx.fragment.app.FragmentActivity) {
    
    
                ((androidx.fragment.app.FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(mFragmentLifecycleCallbacksToAndroidx, true);
            }
        }
        //Activity 中的 setContentView(View) 一定要在 super.onCreate(Bundle); 之后执行
        if (mAutoAdaptStrategy != null) {
    
    
            mAutoAdaptStrategy.applyAdapt(activity, activity);
        }
    }

通过注册registerFragmentLifecycleCallbacks,进行Fragment的生命周期管理,当onFragmentCreated时,也就是OnCreate()中进行了AutoAdaptStrategy#applyAdapt的调用
@Override
public void onFragmentCreated(@androidx.annotation.NonNull FragmentManager fm, @androidx.annotation.NonNull Fragment f, Bundle savedInstanceState) {
if (mAutoAdaptStrategy != null) {
mAutoAdaptStrategy.applyAdapt(f, f.getActivity());
}
}

通过全局的进行Activity和Fragment的生命周期监控,在其布局创建之前调用 AutoAdaptStrategy#applyAdapt进行具体的适配操作,它的关键点是动态修改density、scaledDensity、densityDpi三个参数,造成每个Activity或Fragment加载布局时的density、scaledDensity、densityDpi等参数不一样,达到的适配效果则不一样。

2.适配策略的实现

ActivityLifecycleCallbacks的使用能实时监测Activity和Fragment进行适配调用,但是实际操作的代码在策略方案AutoAdaptStrategy的实现子类中,框架中已有默认策略方案,当然自己也可以自定义修改创建。

  • 当target实现CancelAdapt后,将density、scaledDensity、densityDpi恢复到原始状态,不进行匹配
  • 当target实现CustomAdapt后,将density、scaledDensity、densityDpi根据target的配置进行计算后设置
  • 当target未进行任何处理时,将density、scaledDensity、densityDpi根据AndroidManifest.xml中的配置进行计算设置
    @Override
    public void applyAdapt(Object target, Activity activity) {
    
    
    	....
        //如果 target 实现 CancelAdapt 接口表示放弃适配, 所有的适配效果都将失效
        if (target instanceof CancelAdapt) {
    
    
            AutoSizeLog.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
            AutoSize.cancelAdapt(activity);
            return;
        }

        //如果 target 实现 CustomAdapt 接口表示该 target 想自定义一些用于适配的参数, 从而改变最终的适配效果
        if (target instanceof CustomAdapt) {
    
    
            AutoSizeLog.d(String.format(Locale.ENGLISH, "%s implemented by %s!", target.getClass().getName(), CustomAdapt.class.getName()));
            AutoSize.autoConvertDensityOfCustomAdapt(activity, (CustomAdapt) target);
        } else {
    
    
            AutoSizeLog.d(String.format(Locale.ENGLISH, "%s used the global configuration.", target.getClass().getName()));
            AutoSize.autoConvertDensityOfGlobal(activity);
        }
        ...
    }
   

7. 其实事项

1. Fragment横竖屏切换布局问题
由于某些原因, 屏幕旋转后 Fragment 的重建, 会导致框架对 Fragment 的自定义适配参数失去效果。所以如果您的 Fragment 允许屏幕旋转, 则请在 onCreateView 手动调用一次 AutoSize.autoConvertDensity(),如AutoSize.autoConvertDensity(getActivity(), 1080, true)。
如果您的 Fragment 不允许屏幕旋转, 则可以将下面调用 AutoSize.autoConvertDensity() 的代码删除掉

public class CustomFragment1 extends Fragment implements CustomAdapt {
    
    

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    
    
        //由于某些原因, 屏幕旋转后 Fragment 的重建, 会导致框架对 Fragment 的自定义适配参数失去效果
        //所以如果您的 Fragment 允许屏幕旋转, 则请在 onCreateView 手动调用一次 AutoSize.autoConvertDensity()
        //如果您的 Fragment 不允许屏幕旋转, 则可以将下面调用 AutoSize.autoConvertDensity() 的代码删除掉
        AutoSize.autoConvertDensity(getActivity(), 1080, true);
        return createTextView(inflater, "Fragment-1\nView width = 360dp\nTotal width = 1080dp", 0xffff0000);
    }

2. 主副单位的逐步替换
框架中同时支持主单位和副单位,对于对于旧项目中已使用dp或px的项目,可以通过逐步在新页面中使用主单位副单位。通过不断的迭代替换,最终将项目中的主单位如dp全替换为副单位px,或者将副单位px全替换为主单位dp。

当单位都替换完成后,设置UnitsManager.setSupportDP(false)关闭对dp的支持,彻底隔离修改 density 所造成的不良影响。
或者都使用dp,不在支持副单位时设置UnitsManager.setSupportSubunits(Subunits.NONE)关闭对副单位的支持。

3. 主副单位的同时支持
当使用者想将旧项目从主单位过渡到副单位, 或从副单位过渡到主单位时。因为在使用主单位时, 建议在 AndroidManifest 中填写设计图的 dp 尺寸, 比如 360 * 640。

但在 AndroidManifest 中却只能填写一套设计图尺寸, 并且已经填写了主单位的设计图尺寸,所以当项目中同时存在副单位和主单位, 并且副单位的设计图尺寸与主单位的设计图尺寸不同时, 可以通过UnitsManager#setDesignSize() 方法配置。

如果副单位的设计图尺寸与主单位的设计图尺寸相同, 则不需要调用 UnitsManager#setDesignSize(), 框架会自动使用 AndroidManifest 中填写的设计图尺寸。

4. 自定义单位模拟器创建
布局时的实时预览在开发阶段是一个很重要的环节, 很多情况下 Android Studio 提供的默认预览设备并不能完全展示我们的设计图。所以我们就需要自己创建模拟设备, 大神@JessYan已经为我们准备好了dp、pt、in、mm 这四种单位的模拟设备创建方法,请点击查看链接https://github.com/JessYanCoding/AndroidAutoSize/blob/master/README-zh.md#preview

总结

经过我自己修改注释的源码在https://github.com/l424533553/MyAutoSize.git中,大家也可以自行封装框架,适合自己的才是最好的。
自适应的核心就是根据需要在使用之前不断修改density、scaledDensity、densityDpi达到适配效果。


博客书写不易,您的点赞收藏是我前进的动力,觉得不错请点赞、 收藏 ^ _ ^ !
相关链接

猜你喜欢

转载自blog.csdn.net/luo_boke/article/details/108594891