今天记录的笔记是今日头条的适配方案原理以及开源库”AndroidAutoSize“的实现剖析!(备注:此文仅供自己参考,全文全是文字,如果读者读起来比较生涩难懂,建议阅读引入文章)
此篇文章是通过阅读《骚年你的屏幕适配方式该升级了!-今日头条适配方案》理解的整理。感谢作者JessYan
适配的前提是你需要知道安卓适配的基础知识和基本方法 《Android屏幕适配-第一篇(基础知识)》、《Android屏幕适配-第二篇(你要知道的适配基本操作)》
你需要知道的关键字:density、dip、px、dp(dpi)
今日头条适配原理:density=(当前屏幕分辨率宽或高(单位px))/(ui设计稿宽或高(单位dp))
这个表示的是1dp在当前分辨率下的所对于的像素
真正的view大小: realWidth=(ui设计稿上的宽度)*density
如何使用,例子:如设置一个view大小为100dpx100dp,通过上面的公式可得到宽为100*density高为100*density
假设当前分辨率为720x1280,设计搞的尺寸为360x640,当前density=2
假设当前分辨率为480x800,设计搞的尺寸为360x640,当前density=1.333
分辨率为720x1280下 view宽度=100*2 px,占整体宽度的比例为0.278(约等于)
分辨率为480x800下 view宽度=100*1.33 px,占整体宽度的比例为0.278(约等于)
比例近乎相同
AndroidAutoSize分析
设置设计稿 宽或高
初始化配置,通过启动一个ContenProvider来启动默认配置
/**
* ================================================
* 通过声明 {@link ContentProvider} 自动完成初始化
* Created by JessYan on 2018/8/19 11:55
* Contact me
* Follow me
* ================================================
*/
public class InitProvider extends ContentProvider {
@Override
public boolean onCreate() {
AutoSizeConfig.getInstance()
.setLog(true)
.init((Application) getContext().getApplicationContext())
.setUseDeviceSize(false);
return true;
}
//其他代码忽略...
}
然后会调用此方法
AutoSizeConfig init(final Application application, boolean isBaseOnWidth, AutoAdaptStrategy strategy) {
Preconditions.checkArgument(mInitDensity == -1, "AutoSizeConfig#init() can only be called once");
Preconditions.checkNotNull(application, "application == null");
this.mApplication = application;
this.isBaseOnWidth = isBaseOnWidth;
final DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
getMetaData(application);
isVertical = application.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
int[] screenSize = ScreenUtils.getScreenSize(application);
mScreenWidth = screenSize[0];
mScreenHeight = screenSize[1];
mStatusBarHeight = ScreenUtils.getStatusBarHeight();
LogUtils.d("designWidthInDp = " + mDesignWidthInDp + ", designHeightInDp = " + mDesignHeightInDp + ", screenWidth = " + mScreenWidth + ", screenHeight = " + mScreenHeight);
mInitDensity = displayMetrics.density;
mInitDensityDpi = displayMetrics.densityDpi;
mInitScaledDensity = displayMetrics.scaledDensity;
mInitXdpi = displayMetrics.xdpi;
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null) {
if (newConfig.fontScale > 0) {
mInitScaledDensity =
Resources.getSystem().getDisplayMetrics().scaledDensity;
LogUtils.d("initScaledDensity = " + mInitScaledDensity + " on ConfigurationChanged");
}
isVertical = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT;
int[] screenSize = ScreenUtils.getScreenSize(application);
mScreenWidth = screenSize[0];
mScreenHeight = screenSize[1];
}
}
@Override
public void onLowMemory() {
}
});
....
//注意看这句代码是
mActivityLifecycleCallbacks = new ActivityLifecycleCallbacksImpl(strategy == null ? new WrapperAutoAdaptStrategy(new DefaultAutoAdaptStrategy()) : strategy);
application.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
....
return this;
}
ActivityLifecycleCallbacksImpl这里面有很多核心代码
/**
* ================================================
* {@link ActivityLifecycleCallbacksImpl} 可用来代替在 BaseActivity 中加入适配代码的传统方式
* {@link ActivityLifecycleCallbacksImpl} 这种方案类似于 AOP, 面向接口, 侵入性低, 方便统一管理, 扩展性强, 并且也支持适配三方库的 {@link Activity}
*
* Created by JessYan on 2018/8/8 14:32
* Contact me
* Follow me
* ================================================
*/
public class ActivityLifecycleCallbacksImpl implements Application.ActivityLifecycleCallbacks {
/**
* 屏幕适配逻辑策略类
*/
private AutoAdaptStrategy mAutoAdaptStrategy;
/**
* 让 {@link Fragment} 支持自定义适配参数
*/
private FragmentLifecycleCallbacksImpl mFragmentLifecycleCallbacks;
public ActivityLifecycleCallbacksImpl(AutoAdaptStrategy autoAdaptStrategy) {
mFragmentLifecycleCallbacks = new FragmentLifecycleCallbacksImpl(autoAdaptStrategy);
mAutoAdaptStrategy = autoAdaptStrategy;
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (AutoSizeConfig.getInstance().isCustomFragment()) {
if (activity instanceof FragmentActivity) {
((FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(mFragmentLifecycleCallbacks, true);
}
}
//Activity 中的 setContentView(View) 一定要在 super.onCreate(Bundle); 之后执行
if (mAutoAdaptStrategy != null) {
mAutoAdaptStrategy.applyAdapt(activity, activity);
}
}
@Override
public void onActivityStarted(Activity activity) {
if (mAutoAdaptStrategy != null) {
mAutoAdaptStrategy.applyAdapt(activity, activity);
}
}
...
/**
* 设置屏幕适配逻辑策略类
*
* @param autoAdaptStrategy {@link AutoAdaptStrategy}
*/
public void setAutoAdaptStrategy(AutoAdaptStrategy autoAdaptStrategy) {
mAutoAdaptStrategy = autoAdaptStrategy;
mFragmentLifecycleCallbacks.setAutoAdaptStrategy(autoAdaptStrategy);
}
}
DefaultAutoAdaptStrategy里面干了什么事情
/**
* ================================================
* 屏幕适配逻辑策略默认实现类, 可通过 {@link AutoSizeConfig#init(Application, boolean, AutoAdaptStrategy)}
* 和 {@link AutoSizeConfig#setAutoAdaptStrategy(AutoAdaptStrategy)} 切换策略
*
* @see AutoAdaptStrategy
* Created by JessYan on 2018/8/9 15:57
* Contact me
* Follow me
* ================================================
*/
public class DefaultAutoAdaptStrategy implements AutoAdaptStrategy {
@Override
public void applyAdapt(Object target, Activity activity) {
//检查是否开启了外部三方库的适配模式, 只要不主动调用 ExternalAdaptManager 的方法, 下面的代码就不会执行
if (AutoSizeConfig.getInstance().getExternalAdaptManager().isRun()) {
if (AutoSizeConfig.getInstance().getExternalAdaptManager().isCancelAdapt(target.getClass())) {
LogUtils.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
AutoSize.cancelAdapt(activity);
return;
} else {
ExternalAdaptInfo info = AutoSizeConfig.getInstance().getExternalAdaptManager()
.getExternalAdaptInfoOfActivity(target.getClass());
if (info != null) {
LogUtils.d(String.format(Locale.ENGLISH, "%s used %s for adaptation!", target.getClass().getName(), ExternalAdaptInfo.class.getName()));
AutoSize.autoConvertDensityOfExternalAdaptInfo(activity, info);
return;
}
}
}
//如果 target 实现 CancelAdapt 接口表示放弃适配, 所有的适配效果都将失效
if (target instanceof CancelAdapt) {
LogUtils.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
AutoSize.cancelAdapt(activity);
return;
}
//如果 target 实现 CustomAdapt 接口表示该 target 想自定义一些用于适配的参数, 从而改变最终的适配效果
if (target instanceof CustomAdapt) {
LogUtils.d(String.format(Locale.ENGLISH, "%s implemented by %s!", target.getClass().getName(), CustomAdapt.class.getName()));
AutoSize.autoConvertDensityOfCustomAdapt(activity, (CustomAdapt) target);
} else {
LogUtils.d(String.format(Locale.ENGLISH, "%s used the global configuration.", target.getClass().getName()));
AutoSize.autoConvertDensityOfGlobal(activity);
}
}
}
AutoSize
public static void autoConvertDensityOfCustomAdapt(Activity activity, CustomAdapt customAdapt) {
Preconditions.checkNotNull(customAdapt, "customAdapt == null");
float sizeInDp = customAdapt.getSizeInDp();
//如果 CustomAdapt#getSizeInDp() 返回 0, 则使用在 AndroidManifest 上填写的设计图尺寸
if (sizeInDp <= 0) {
if (customAdapt.isBaseOnWidth()) {
sizeInDp = AutoSizeConfig.getInstance().getDesignWidthInDp();
} else {
sizeInDp = AutoSizeConfig.getInstance().getDesignHeightInDp();
}
}
autoConvertDensity(activity, sizeInDp, customAdapt.isBaseOnWidth());
}
/**
* 这里是今日头条适配方案的核心代码, 核心在于根据当前设备的实际情况做自动计算并转换 {@link DisplayMetrics#density}、
* {@link DisplayMetrics#scaledDensity}、{@link DisplayMetrics#densityDpi} 这三个值, 额外增加 {@link DisplayMetrics#xdpi}
* 以支持单位 {@code pt}、{@code in}、{@code mm}
*
* @param activity {@link Activity}
* @param sizeInDp 设计图上的设计尺寸, 单位 dp, 如果 {@param isBaseOnWidth} 设置为 {@code true},
* {@param sizeInDp} 则应该填写设计图的总宽度, 如果 {@param isBaseOnWidth} 设置为 {@code false},
* {@param sizeInDp} 则应该填写设计图的总高度
* @param isBaseOnWidth 是否按照宽度进行等比例适配, {@code true} 为以宽度进行等比例适配, {@code false} 为以高度进行等比例适配
* @see 今日头条官方适配方案
*/
public static void autoConvertDensity(Activity activity, float sizeInDp, boolean isBaseOnWidth) {
Preconditions.checkNotNull(activity, "activity == null");
float subunitsDesignSize = isBaseOnWidth ? AutoSizeConfig.getInstance().getUnitsManager().getDesignWidth()
: AutoSizeConfig.getInstance().getUnitsManager().getDesignHeight();
subunitsDesignSize = subunitsDesignSize > 0 ? subunitsDesignSize : sizeInDp;
int screenSize = isBaseOnWidth ? AutoSizeConfig.getInstance().getScreenWidth()
: AutoSizeConfig.getInstance().getScreenHeight();
String key = sizeInDp + "|" + subunitsDesignSize + "|" + isBaseOnWidth + "|"
+ AutoSizeConfig.getInstance().isUseDeviceSize() + "|"
+ AutoSizeConfig.getInstance().getInitScaledDensity() + "|"
+ screenSize;
DisplayMetricsInfo displayMetricsInfo = mCache.get(key);
float targetDensity = 0;
int targetDensityDpi = 0;
float targetScaledDensity = 0;
float targetXdpi = 0;
if (displayMetricsInfo == null) {
if (isBaseOnWidth) {
targetDensity = AutoSizeConfig.getInstance().getScreenWidth() * 1.0f / sizeInDp;
} else {
targetDensity = AutoSizeConfig.getInstance().getScreenHeight() * 1.0f / sizeInDp;
}
float scale = AutoSizeConfig.getInstance().isExcludeFontScale() ? 1 : AutoSizeConfig.getInstance().
getInitScaledDensity() * 1.0f / AutoSizeConfig.getInstance().getInitDensity();
targetScaledDensity = targetDensity * scale;
targetDensityDpi = (int) (targetDensity * 160);
if (isBaseOnWidth) {
targetXdpi = AutoSizeConfig.getInstance().getScreenWidth() * 1.0f / subunitsDesignSize;
} else {
targetXdpi = AutoSizeConfig.getInstance().getScreenHeight() * 1.0f / subunitsDesignSize;
}
mCache.put(key, new DisplayMetricsInfo(targetDensity, targetDensityDpi, targetScaledDensity, targetXdpi));
} else {
targetDensity = displayMetricsInfo.getDensity();
targetDensityDpi = displayMetricsInfo.getDensityDpi();
targetScaledDensity = displayMetricsInfo.getScaledDensity();
targetXdpi = displayMetricsInfo.getXdpi();
}
setDensity(activity, targetDensity, targetDensityDpi, targetScaledDensity, targetXdpi);
LogUtils.d(String.format(Locale.ENGLISH, "The %s has been adapted! \n%s Info: isBaseOnWidth = %s, %s = %f, %s = %f, targetDensity = %f, targetScaledDensity = %f, targetDensityDpi = %d, targetXdpi = %f"
, activity.getClass().getName(), activity.getClass().getSimpleName(), isBaseOnWidth, isBaseOnWidth ? "designWidthInDp"
: "designHeightInDp", sizeInDp, isBaseOnWidth ? "designWidthInSubunits" : "designHeightInSubunits", subunitsDesignSize
, targetDensity, targetScaledDensity, targetDensityDpi, targetXdpi));
}
由此可见autoConvertDensity()方法是核心代码
这里还匹配了pt in mm 为单位的适配
private static void setDensity(DisplayMetrics displayMetrics, float density, int densityDpi, float scaledDensity, float xdpi) {
if (AutoSizeConfig.getInstance().getUnitsManager().isSupportDP()) {
displayMetrics.density = density;
displayMetrics.densityDpi = densityDpi;
}
if (AutoSizeConfig.getInstance().getUnitsManager().isSupportSP()) {
displayMetrics.scaledDensity = scaledDensity;
}
switch (AutoSizeConfig.getInstance().getUnitsManager().getSupportSubunits()) {
case NONE:
break;
case PT:
displayMetrics.xdpi = xdpi * 72f;
break;
case IN:
displayMetrics.xdpi = xdpi;
break;
case MM:
displayMetrics.xdpi = xdpi * 25.4f;
break;
default:
}
}