近日隨筆
近期疫情日漸嚴峻,大家多多保重,出門記得戴口罩,希望河北,黑龍江能盡早控制住好局面迎來拐點,全國人民過個好年,

為了能夠讓低版本的Android系統能夠運行新特性,AppCompat框架自Support時代就已推出,但隨著AndroidX的一統江湖,AppCompat的相關類則一并遷移到了AndroidX庫里,
Android開發者應該都不陌生,在Android Studio上創建的專案默認采用AppCompatActivity作為Activity的基類,可以說,這個類是整個AppCompat框架里最重要的類,也是我們今天研究AppCompat的起點,
AppCompatActivity
其間接繼承自Activity,之間還繼承了其他Activity特色類,可以使得低版本上運行的Activity也能擁有ToolBar和暗黑主題等新功能,
AppCompatActivity extends FragmentActivity extends ComponentActivity extends ComponentActivity extends Activity*
-
FragmentActivity
采用FragmentController類對AndroidX的Fragment新組件提供支撐,比如提供了咱們常用的getSupportFragmentManager() API, -
androidx.activity.ComponentActivity
實作了ViewModel介面,和Lifecycle框架進行配合以支撐ViewModel框架的運行, -
androidx.core.app.ComponentActivity
實作了Lifecycle介面并通過ReportFragment支撐Lifecycle框架的運行,
先來感受一下AppCompatActivity和Activity在UI上的表現,

從對比圖上看并沒有太大區別,但從UI的樹形圖上看是有些區別的,比如AppCompatActivity的content區域的上方多了一個LinearLayout和ViewStub控制元件,再比如AppCompatActivity下面的是AppCompatTextView而不是TextView,
那這些差異是如何實作的,有什么用意?
談到AppCompatActivity實作的話不得不提幕后的大管家AppCompatDelegate類,其承載了AppCompatActivity幾乎所有的實作作業,比如AppCompatActivity復寫了setContentView()的邏輯,交由大管家AppCompatDelegate去實作其特有的UI結構,
AppCompatDelegate
重點介紹下大管家的頭號作業setContentView(),具體分為如下幾個小任務,
-
ensureSubDecor() 確保ActionBar的特有UI結構創建完畢
-
removeAllViews() 確保ContentView的所有Child全部被移除干凈
-
inflate() 將畫面的內容布局決議并添加到ContentView下
第一步ensureSubDecor()的內容比較多,又分為幾個子任務,包括呼叫createSubDecor()創建ActionBar特有布局,setWindowTitle()將Activity標題反映到ToolBar上以及applyFixedSizeWindow()去調整DecorView尺寸,
核心內容在于createSubDecor()這個子任務,它需要確保ActionBar的特有布局創建出來并和Window的DecorView產生聯系,
-
ensureWindow()
獲取Activity所屬的Window參考并添加window相關回呼 -
getDecorView()
告知Window去創建DecorView,這里要提一下PhoneWindow的generateLayout(),其將依據主題的創建不同的布局結構,比如AppCompatActivity的話將決議screen_simple.xml得到DecorView的基本結構,其包括根布局LinearLayout,用來映射actionmode布局的viewstub以及承載App內容的id為ContentView -
inflate()
獲取ActionBar的布局,主要是abc_screen_toolbar.xml和abc_screen_content_include.xml兩個檔案 -
removeViewAt()和addView()
將ContentView的子View遷移至ActionBar布局下,具體方法是將其所有child移除并add到ActionBar布局下id為action_bar_activity_content的ViewGroup下面,并將原有ContentView的id置空,同時將該目標ViewGroup的id設定為Content,意味著它將成為AppCompatActivity畫面承載內容區域的父布局
公開的API
除了setContentView()在打造布局結構上的差異,AppCompatActivity還提供了些Activity所沒有的API供開發者使用,
-
getSupportActionBar() 用以獲取AppCompat特有的ActionBar組件供開發者定制ActionBar
-
getDelegate() 獲取AppCompatActivity內部實作的大管家AppCompatDelegate的實體(實際上將通過靜態的create()獲取實作類AppCompatDelegateImpl的實體)
-
getDrawerToggleDelegate() 獲取抽屜導航布局DrawerLayout的代理類ActionBarDrawableToggleImpl的實體,用來和ActionBar進行UI的互動
-
onNightModeChanged() 不同于配置了uiMode的外部配置變更后才能收到主題變化的通知,本API可以在暗黑主題的適配模式(比如跟隨系統設定模式和跟隨電量設定模式等)發生變化后得到回呼,可利用這個時機做些補充處理
使用上的注意
AppCompatActivity的注釋上有如下說明,推薦采用Theme.AppCompat主題,
You can add an ActionBar to your activity when running on API level 7 or higher by extending this class for your activity and setting the activity theme to Theme.AppCompat or a similar theme.
經過驗證如果我們使用了別的主題就會得到如下的crash,
You need to use a Theme.AppCompat theme (or descendant) with this activity.
原理在于上面自己的大管家AppCompatDelegate在創建ActionBar布局的時候有意地確保Activity是否采用了AppCompatTheme主題,尤其是如果沒有指定AppCompat定義的windowActionBar的屬性的話,將拋出如上的例外,
// AppCompatThemeImpl.java
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
a.recycle();
throw new IllegalStateException(
"You need to use a Theme.AppCompat theme (or descendant) with this activity.");
}
...
}
至于為什么用例外來確保AppCompatTheme的采用,因為后續的處理跟AppCompatTheme息息相關,如果沒有采用后面的很多處理將失效,
AppCompatDialog
除了使用極高的AppCompatActivity以外,AppCompatDialog的曝光率也不低,其實作原理和AppCompatActivity企劃一致,都是依賴大管家AppCompatDelegate進行實作,一樣是為了在Dialog的基礎上擴展出新ToolBar和暗黑主題的支持,
AppCompatTheme
前面提到的AppCompatTheme主要分為兩個主題,
-
Theme.AppCompat
繼承自Base.V7.Theme.AppCompat主題,指定AppCompatViewInflater為widget等class的決議類,并設定AppCompatTheme所定義的基本屬性,其頂級主題仍舊是老牌的主題Theme.Holo -
Theme.AppCompat.DayNight
能夠自動適配暗黑主題,其繼承自Base.V7.Theme.AppCompat.Light,與Theme.AppCompat的區別主要在于其默認情況下采用了light系的主題,比如colorPrimary采用primary_material_light,而Theme.AppCompat則采用primary_material_dark顏色
App采用了該主題就可以自動適配暗黑模式,這是如何做到的?
Dark Theme 暗黑模式
AppCompatActivity在系結BaseContext的時候會通過AppCompatDelegate的applyDayNight()去決議App設定的暗黑主題模式并做出一些相應的配置作業,
比如常用的跟隨省電模式,其指的是設備的省電模式開啟后將自動進入暗黑主題,降低功耗,反之關閉之后回傳到白天主題,
具體實作是AppCompatDelegate將注冊監聽省電模式變化的廣播(ACTION_POWER_SAVE_MODE_CHANGED),當省電模式開啟/關閉時,廣播接收器將自動回呼updateForNightMode()去更新對應的主題,
private boolean applyDayNight(final boolean allowRecreation) {
...
@NightMode final int nightMode = calculateNightMode();
@ApplyableNightMode final int modeToApply = mapNightMode(nightMode);
final boolean applied = updateForNightMode(modeToApply, allowRecreation);
...
if (nightMode == MODE_NIGHT_AUTO_BATTERY) {
// 注冊監聽省電模式的廣播接收器
getAutoBatteryNightModeManager().setup();
}
...
}
abstract class AutoNightModeManager {
...
void setup() {
...
if (mReceiver == null) {
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 省電模式變化后的回呼
onChange();
}
};
}
mContext.registerReceiver(mReceiver, filter);
}
...
}
private class AutoBatteryNightModeManager extends AutoNightModeManager {
...
@Override
public void onChange() {
// 省電模式變化后回呼主題切換方法更新主題
applyDayNight();
}
@Override
IntentFilter createIntentFilterForBroadcastReceiver() {
if (Build.VERSION.SDK_INT >= 21) {
IntentFilter filter = new IntentFilter();
filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
return filter;
}
return null;
}
}
更新主題的處理則是如下關鍵代碼,
private boolean updateForNightMode(final int mode, final boolean allowRecreation) {
...
// 如果Activity的BaseContext尚未初始化則直接適配新的主題值
if ((sAlwaysOverrideConfiguration || newNightMode != applicationNightMode)
&& !mBaseContextAttached
...) {
...
try {
...
((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
handled = true;
...
}
}
final int currentNightMode = mContext.getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK;
// 如果Activity的BaseContext已經創建,
// 且App沒有宣告要處理暗黑主題變化的話,將重繪Activity
if (!handled
...) {
ActivityCompat.recreate((Activity) mHost);
handled = true;
}
// 假使App宣告了處理暗黑主題變化的話,
// 那么將新的主題值更新到Configuration的uiMode屬性
// 并回呼Activity#onConfigurationChanged(),等待App的自行處理
if (!handled && currentNightMode != newNightMode) {
...
updateResourcesConfigurationForNightMode(newNightMode, activityHandlingUiMode);
handled = true;
}
// 最后檢查是否要通知App暗黑主題模式發生變化
// (注意這里指的是App設定的暗黑主題切換的策略發生變更,
// 比如由跟隨系統設定變更為固定暗黑模式等)
if (handled && mHost instanceof AppCompatActivity) {
((AppCompatActivity) mHost).onNightModeChanged(mode);
}
...
}
細心的開發者可能會注意到我們平常在AppCompatActivity的布局里使用的控制元件,最終得到的類名稱里會多上AppCompat的前綴,比如宣告的是TextView控制元件最后得到的是AppCompatTextView類的實體,這是怎么做到的,為什么這么做?這就離不開AppCompatViewInflater的默默付出,
AppCompatViewInflater
核心功能就是將布局里的控制元件切換為AppCompat版本,在呼叫LayoutInflater決議App布局的階段,大管家AppCompatDelegate將呼叫AppCompatViewInflater將布局中的控制元件逐個替換,
final View createView(View parent, final String name, @NonNull Context context...) {
...
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
...
}
...
return view;
}
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
除了上面提到的AppCompatTextView,AppCompat的widget目錄下有很多為了兼容新特性擴展的控制元件,以AppCompatTextView和另一個常用的AppCompatImageView來一探究竟,
AppCompatTextView
由代碼注釋就可以看出來該控制元件在TextView的基礎上增加了Dynamic Tint和Auto Size兩大特性,
先看下這兩特性大體是什么效果,

可以看到第二個TextView對背景著上了更深的綠色,并對icon著上了白色,使得它內部的icon和文字相較第一個TextView看起來更清楚,這是通過AppCompatTextView提供的backgroundTint和drawableTint屬性實作的,這種給背景和icon動態著色的功能就是Dynamic Tint特性,
另外可以看到最下面TextView的文本內容正好鋪滿整個螢屏沒有在末尾出現省略,而上面那個TextView的字體尺寸較大且在尾部用省略號表示,這種自動適配字體尺寸的效果同樣是依賴AppCompatTextView提供的相關屬性來完成,此為Auto Size特性,
Dynamic Tint
主要依賴AppCompatBackgroundHelper和AppCompatDrawableManager實作,包括反映靜態配置和動態修改的Tint屬性,
主要經歷這幾步:
- loadFromAttributes() 決議布局里配置的Tint屬性,核心處理在于能夠將設定的Tint資源決議成ColorStateList實體,
// ColorStateListInflaterCompat.java
private static ColorStateList inflate(Resources r, XmlPullParser parser) {
...
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
...
final int color = modulateColorAlpha(baseColor, alphaMod);
colorList = GrowingArrayUtils.append(colorList, listSize, color);
stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec);
listSize++;
}
...
return new ColorStateList(stateSpecs, colors);
}
-
setInternalBackgroundTint()和applySupportBackgroundTint() 負責管理和區分Tint顏色的取自靜態配置的屬性還是外部動態配置的引數
-
tintDrawable()負責著色,本質在于呼叫Drawable#setColorFilter()去重繪顏色的繪制
// ResourceManagerInternal.java
static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) {
...
if (tint.mHasTintList || tint.mHasTintMode) {
drawable.setColorFilter(createTintFilter(
tint.mHasTintList ? tint.mTintList : null,
tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
state));
} else {
drawable.clearColorFilter();
}
...
}
Auto Size
需要解決的問題是對Text內容依據最大寬度和當前size計算自適應的最佳字體尺寸,依賴AppCompatTextHelper和AppCompatTextViewAutoSizeHelper實作,
- 決議AutoSize相關屬性的配置并設定是否需要自動適配字體尺寸的Flag,
// AppCompatTextViewAutoSizeHelper.java
void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
...
if (a.hasValue(R.styleable.AppCompatTextView_autoSizeTextType)) {
mAutoSizeTextType = a.getInt(R.styleable.AppCompatTextView_autoSizeTextType,
TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE);
}
...
if (supportsAutoSizeText()) {
if (mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) {
...
setupAutoSizeText();
}
...
}
}
private boolean setupAutoSizeText() {
if (supportsAutoSizeText()
&& mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) {
...
if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) {
...
for (int i = 0; i < autoSizeValuesLength; i++) {
autoSizeTextSizesInPx[i] = Math.round(
mAutoSizeMinTextSizeInPx + (i * mAutoSizeStepGranularityInPx));
}
mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx);
}
mNeedsAutoSizeText = true;
}
...
}
- 在文本內容初始化或變化的時候計算合適的字體尺寸并反映到UI上,
// AppCompatTextView.java
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
...
if (mTextHelper != null && !PLATFORM_SUPPORTS_AUTOSIZE && mTextHelper.isAutoSizeEnabled()) {
mTextHelper.autoSizeText();
}
}
// AppCompatTextHelper.java
void autoSizeText() {
mAutoSizeTextHelper.autoSizeText();
}
// AppCompatTextViewAutoSizeHelper.java
void autoSizeText() {
...
if (mNeedsAutoSizeText) {
...
synchronized (TEMP_RECTF) {
...
// 計算最佳size
final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
// 如果和預設的size不一致的話更新size
if (optimalTextSize != mTextView.getTextSize()) {
setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize);
}
}
}
...
}
AppCompatImageView
和AppCompatTextView一樣擴展了針對background和src的Dynamic Tint功能,

與AppCompatTextView不同的是AppCompatImageView對icon著色采用的屬性不是*attr#drawableTint是attr#tint***,由AppCompatImageHelper和ImageViewCompat類實作,原理大同小異,不再贅述,
輔助類
AppCompat框架的開發人員在實作AppCompat擴展控制元件等特性的時候用到很多輔助類,大家可以自行研究下其細節,學習下一些巧妙的實作思路,
- AppCompatBackgroundHelper
- AppCompatDrawableManager
- AppCompatTextHelper
- AppCompatTextViewAutoSizeHelper
- AppCompatTextClassifierHelper
- AppCompatResources
- AppCompatImageHelper
…
類圖
最后上一下AppCompat框架的簡易類圖,幫助大家有個整體上的認識,

總結
可以看到AppCompat框架整體比較簡單,因此也容易被大家忽視,但作為Jetpack系列里的入口,了解一下很有必要,
原創不易,如果本篇文章引起你的思考和興趣,歡迎點贊,收藏和關注,如果想要了解更多更全資訊,掃碼關注博主公眾號TechMerger,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/249933.html
標籤:其他
