欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-03-24 18:22:40
屏幕自适应1. 原理核心2. 框配置3. 自定义初始化4. 常用方法解析5. 常见接口及类的使用CustomAdapt首先感谢大神JessYan的创神之作《AndroidAutoSize》,大神以今日头条屏幕适配的核心代码为基础进行了扩展封装,产生了《AndroidAutoSize》这个能快速接入使用的屏幕适配方案,这个屏幕适配方案是我遇到的截止2020.9.15为止最强大、简单有效的屏幕适配方案。我已使用该方案有一年,在使用过程未发现有何问题,强烈推荐各位极客们使用学习。以下是大神JessYan的相关...


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

以下是大神JessYan的相关地址:
邮箱:jess.yan.effort@gmail.com
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”的缩写。屏幕像素密度与屏幕尺寸和屏幕分辨率有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。
Android 今日头条屏幕适配详细使用攻略

计算公式: 像素密度 = 像素 / 尺寸 (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达到适配效果。


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

本文地址:https://blog.csdn.net/luo_boke/article/details/108594891