Android 第三方庫系列文章
- Android 今日頭條螢幕适配詳細使用攻略
- Lottie動畫 輕松使用
今日頭條螢幕适配
- 前言
- 1. 螢幕像素
- 2. 适配原理
- 3. 架構配置
- 4. 自定義初始化
- 5. 常用方法解析
- 6. 常見接口及類的使用
- 7.架構核心
- 8. 其它
- **總結**
部落格建立時間: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的相關位址:
- 郵箱:[email protected]
- github:https://github.com/JessYanCoding/AndroidAutoSize
- 簡書:https://www.jianshu.com/p/4aa23d69d481
- 原始核心代碼:https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA
大神的源碼都在github中各位可以自行下載下傳,我寫這篇部落格的目的就是記錄使用心得,并将該架構的重要類和方法使用進行詳細說明,大神的文章中對細節問題寫的比較少,我對其進行了細微的修改和詳細的注解解釋。我自己修改注釋過得架構源碼請前往github下載下傳https://github.com/l424533553/MyAutoSize。
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的10801920的手機它的dpi=480,它的density=480/160,則說明1dp=3px。當我們在布局中給如TextView設定layout_width=30dp時,在程式運作時會自動計算其對應px機關長度90px,px=dpdensity。
今日頭條适配方案的核心就是動态計算程式中的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的目的了。
2. 優缺點
- 優點:
- 侵入性非常低,該方案和項目完全解耦,使用的還是Android官方機關
- 接入無性能損耗,使用的全是Android官方的API。
- 缺點:
- 項目中的系統控件、三方庫控件、等非我們項目自身設計的控件,它們的設計圖尺寸并不會和我們項目自身的設計圖尺寸一樣,此時會産生适配誤差。解決方案就是取消目前 Activity 的适配效果,改用其他的适配方案
- 系統修改字型大小後,傳回應用系統字型大小還是未改變,需要設定registerComponentCallbacks監聽。 Android AutoSize架構已經解決了該問題。
- 在使用過程中需要進行registerComponentCallbacks監聽内容文字的大小改變情況,解決退出應用修改文字大小後,文字大小不改變的情況。
3. 架構配置
依賴配置
- 遠端依賴,截止到2020.9.15,版本為1.2.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>
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
總結
經過我自己修改注釋的源碼在https://github.com/l424533553/MyAutoSize中,大家也可以自行封裝架構,适合自己的才是最好的。
螢幕自适應的核心就是根據需要在使用之前不斷修改density、scaledDensity、densityDpi達到适配效果。
相關連結:
- Android 今日頭條螢幕适配詳細使用攻略
- Lottie動畫 輕松使用
擴充連結:
- Android CameraX 使用入門
- Android studio 最全必用插件
- Android 史上最新最全的ADB及指令百科,沒有之一
部落格書寫不易,您的點贊收藏是我前進的動力,千萬别忘記點贊、 收藏 ^ _ ^ !