今天記錄的筆記是今日頭條的适配方案原理以及開源庫”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分析
設定設計稿 寬或高
<!-- 如果您項目中的所有頁面都隻需要以高或寬中的一個作為基準進行适配的話, 那就隻需要填寫高或寬中的一個設計圖尺寸即可 -->
<meta-data
android:name="design_width_in_dp"
android:value="360"/>
<meta-data
android:name="design_height_in_dp"
android:value="640"/>
初始化配置,通過啟動一個ContenProvider來啟動預設配置
/**
* ================================================
* 通過聲明 {@link ContentProvider} 自動完成初始化
* Created by JessYan on 2018/8/19 11:55
* <a href="mailto:[email protected]" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >Contact me</a>
* <a href="https://github.com/JessYanCoding" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >Follow me</a>
* ================================================
*/
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}
* <p>
* Created by JessYan on 2018/8/8 14:32
* <a href="mailto:[email protected]" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >Contact me</a>
* <a href="https://github.com/JessYanCoding" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >Follow me</a>
* ================================================
*/
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
* <a href="mailto:[email protected]" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >Contact me</a>
* <a href="https://github.com/JessYanCoding" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >Follow me</a>
* ================================================
*/
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 <a href="https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA" target="_blank" rel="external nofollow" >今日頭條官方适配方案</a>
*/
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:
}
}