Android 今日头条屏幕适配详细使用攻略
1. 屏幕像素
像素
通常所说的像素,就是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里调整字体尺寸以后的效果——窗口大小不变,只有文字大小改变。
2. 适配原理
传统的屏幕适配头如下几种措施:
种像素密度机型,做5套图。比例 1:1.5:2:3:4
多用相对布局
尺寸限定符
点九图
不同图片填充类型ScaleType
但是以往的所有屏幕适配都有各种各样的问题和重大缺陷,直到字节跳动的屏幕适配方案出现。根据其公开的核心源码,网上重大大咖封装了各种屏幕适配框架,其中最成功且本人使用感受最好的是AutoSize框架。
Android AutoSize的核心代码来源于字节跳动的微信文章https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA。网上也有多各个大神进行了代码的封装设计,都是万变不离其中。
1. 核心思想
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的1080 * 1920的手机它的dpi=480,它的density=480/160,则说明1dp=3px。当我们在布局中给如TextView设置layout_width=30dp时,在程序运行时会自动计算其对应px单位长度90px,px=dp*density。
今日头条适配方案的核心就是动态计算程序中的density=appDisplayMetrics.widthPixels / 360,360是原始设计图纸的dp。假设原先的设计图纸1080 * 1920,现在适配5.99寸560dpi的1440*2880手机,则30dp=30 * 560/160=105px,实际上屏幕适配要求的30dp=1440/360 * 30=120px才可以达到适配效果。因为120/1440=90/1080,控件在布局中的占宽比是一样的才能达到宽度适配效果。这就是为什么要动态修改全局或activity的DisplayMetrics#density的目的了。
2. 优缺点
优点:
侵入性非常低,该方案和项目完全解耦,使用的还是Android官方单位
接入无性能损耗,使用的全是Android官方的API。
缺点:
项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样,此时会产生适配误差。解决方案就是取消当前 Activity 的适配效果,改用其他的适配方案
系统修改字体大小后,返回应用系统字体大小还是未改变,需要设置registerComponentCallbacks监听。 Android AutoSize框架已经解决了该问题。
在使用过程中需要进行registerComponentCallbacks监听内容文字的大小改变情况,解决退出应用修改文字大小后,文字大小不改变的情况。
3. 框架配置
依赖配置
implementation 'me.jessyan:autosize:1.2.1'
在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>
4. 自定义初始化
本文中使用的框架是经过大神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); } }
5. 常用方法解析
对于初始化中方法,我们进行一一分析
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中配置的参数不一样,不会被覆盖。
6. 常见接口及类的使用
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接口放弃适配,所有的适配效果都将失效。
7.框架核心
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); } ... }
8. 其它
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