Android 第三方库系列文章
博客创建时间:2020.09.20
博客更新时间:2021.06.27
以Android studio build=4.2.1,gradle=6.7.1,SdkVersion 30来分析讲解。如图文和网上其他资料不一致,可能是别的资料版本较低而已
首先感谢大神JessYan的创神之作《AndroidAutoSize》,大神以今日头条屏幕适配的核心代码为基础进行了扩展封装,产生了《AndroidAutoSize》这个能快速接入使用的屏幕适配方案,这个屏幕适配方案是我遇到的截止2020.9.15为止最强大、简单有效的屏幕适配方案。我已使用该方案有一年,在使用过程未发现有何问题,强烈推荐各位极客们使用学习。
以下是大神JessYan的相关地址:
大神的源码都在github中各位可以自行下载,我写这篇博客的目的就是记录使用心得,并将该框架的重要类和方法使用进行详细说明,大神的文章中对细节问题写的比较少,我对其进行了细微的修改和详细的注解解释。我自己修改注释过得框架源码请前往github下载https://github.com/l424533553/MyAutoSize。
像素
通常所说的像素,就是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里调整字体尺寸以后的效果——窗口大小不变,只有文字大小改变。
传统的屏幕适配头如下几种措施:
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. 优缺点
依赖配置
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>
本文中使用的框架是经过大神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);
}
}
对于初始化中方法,我们进行一一分析
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中配置的参数不一样,不会被覆盖。
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接口放弃适配,所有的适配效果都将失效。
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的实现子类中,框架中已有默认策略方案,当然自己也可以自定义修改创建。
@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);
}
...
}
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中,大家也可以自行封装框架,适合自己的才是最好的。
屏幕自适应的核心就是根据需要在使用之前不断修改density、scaledDensity、densityDpi达到适配效果。
相关链接:
扩展链接:
博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !