深度解讀Jetpack框架的基石-AppCompat

爲了可以讓低版本的Android系統可以運行新特性,AppCompat框架自Support時代就已推出。但隨着AndroidX的一統江湖,AppCompat的相關類則一併遷移到了AndroidX庫裏。java

Android開發者應該都不陌生,在Android Studio上建立的項目默認採用AppCompatActivity做爲Activity的基類。能夠說,這個類是整個AppCompat框架裏最重要的類,也是咱們今天研究AppCompat的起點。android

1. AppCompatActivity

其間接繼承自Activity,之間還繼承了其餘Activity特點類,可使得低版本上運行的Activity也能擁有ToolBar和暗黑主題等新功能。markdown

AppCompatActivity extends FragmentActivity extends ComponentActivity extends ComponentActivity extends Activity*app

做用
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。ide

那這些差別是如何實現的,有什麼用意?佈局

談到AppCompatActivity實現的話不得不提幕後的大管家AppCompatDelegate類,其承載了AppCompatActivity幾乎全部的實現工做。學習

好比AppCompatActivity複寫了setContentView()的邏輯,交由大管家AppCompatDelegate去實現其特有的UI結構。字體

1.1 AppCompatDelegate

重點介紹下大管家的頭號工做:setContentView(),具體分爲以下幾個小任務。ui

  • ensureSubDecor()確保ActionBar的特有UI結構建立完畢

  • removeAllViews()確保ContentView的全部Child所有被移除乾淨

  • inflate()將畫面的內容佈局解析並添加到ContentView下

第一步ensureSubDecor()的內容比較多,又分爲幾個子任務,包括調用createSubDecor()建立ActionBar特有佈局,setWindowTitle()將Activity標題反映到ToolBar上以及applyFixedSizeWindow()去調整DecorView尺寸。

核心內容在於createSubDecor()這個子任務。它須要確保ActionBar的特有佈局建立出來並和Window的DecorView產生聯繫。

  1. ensureWindow()

獲取Activity所屬的Window引用並添加window相關回調

  1. getDecorView()

告知Window去建立DecorView,這裏要提一下PhoneWindow的generateLayout(),其將依據主題的建立不一樣的佈局結構,好比AppCompatActivity的話將解析screen_simple.xml獲得DecorView的基本結構,其包括根佈局LinearLayout,用來映射actionmode佈局的viewstub以及承載App內容的id爲ContentView

  1. inflate()

獲取ActionBar的佈局,主要是abc_screen_toolbar.xml和abc_screen_content_include.xml兩個文件

  1. removeViewAt()和addView()

ContentView的子View遷移至ActionBar佈局下。具體方法是將其全部child移除並add到ActionBar佈局下id爲action_bar_activity_content的ViewGroup下面,並將原有ContentView的id置空,同時將該目標ViewGroup的id設置爲Content。意味着它將成爲AppCompatActivity畫面承載內容區域的父佈局

1.2 公開的API

除了setContentView()在打造佈局結構上的差別,AppCompatActivity還提供了些Activity所沒有的API供開發者使用。

  • getSupportActionBar() 用以獲取AppCompat特有的ActionBar組件供開發者定製ActionBar

  • getDelegate() 獲取AppCompatActivity內部實現的大管家AppCompatDelegate的實例(實際上將經過靜態的create()獲取實現類AppCompatDelegateImpl的實例)

  • getDrawerToggleDelegate() 獲取抽屜導航佈局DrawerLayout的代理類ActionBarDrawableToggleImpl的實例,用來和ActionBar進行UI的交互

  • onNightModeChanged() 不一樣於配置了uiMode的外部配置變動後才能收到主題變化的通知,本API能夠在暗黑主題的適配模式(好比跟隨系統設置模式和跟隨電量設置模式等)發生變化後獲得回調,可利用這個時機作些補充處理

1.3 使用上的注意

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息息相關,若是沒有采用後面的不少處理將失效。

2. AppCompatDialog

除了使用極高的AppCompatActivity之外,AppCompatDialog的曝光率也不低。其實現原理和AppCompatActivity企劃一致,都是依賴大管家AppCompatDelegate進行實現。同樣是爲了在Dialog的基礎上擴展出新ToolBar和暗黑主題的支持。

3. 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採用了該主題就能夠自動適配暗黑模式,這是如何作到的?

4. 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類的實例。這是怎麼作到的,爲何這麼作?這就離不開ppCompatViewInflater的默默付出。

5. 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來一探究竟。

6. AppCompatTextView

由代碼註釋就能夠看出來該控件在TextView的基礎上增長了Dynamic TintAuto Size兩大特性。

先看下這兩特性大致是什麼效果。

DynamicTint & AutoSize

能夠看到第二個TextView對背景着上了更深的綠色,並對icon着上了白色,使得它內部的icon和文字相較第一個TextView看起來更清楚。這是經過AppCompatTextView提供的backgroundTint和drawableTint屬性實現的,這種給背景和icon動態着色的功能就是Dynamic Tint特性。

另外能夠看到最下面TextView的文本內容正好鋪滿整個屏幕沒有在末尾出現省略,而上面那個TextView的字體尺寸較大且在尾部用省略號表示。這種自動適配字體尺寸的效果一樣是依賴AppCompatTextView提供的相關屬性來完成。此爲Auto Size特性。

6.1 Dynamic Tint

主要依賴AppCompatBackgroundHelperAppCompatDrawableManager實現,包括反映靜態配置和動態修改的Tint屬性。

主要經歷這幾步:

  1. 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);
    }
複製代碼
  1. setInternalBackgroundTint()和applySupportBackgroundTint() 負責管理和區分Tint顏色的取自靜態配置的屬性仍是外部動態配置的參數

  2. 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();
        }
        ...
    }
複製代碼

6.2 Auto Size

須要解決的問題是對Text內容依據最大寬度和當前size計算自適應的最佳字體尺寸,依賴AppCompatTextHelperAppCompatTextViewAutoSizeHelper實現。

  1. 解析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;
        }
		...
    }
複製代碼
  1. 在文本內容初始化或變化的時候計算合適的字體尺寸並反映到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);
                }
            }
        }
        ...
    }
複製代碼

7. AppCompatImageView

AppCompatTextView同樣擴展了針對background和src的Dynamic Tint功能。

DynamicTint & AutoSize

AppCompatTextView不一樣的是AppCompatImageView對icon着色採用的屬性不是attr#drawableTintattr#tint。由AppCompatImageHelperImageViewCompat類實現,原理大同小異,再也不贅述。

8. 輔助類

AppCompat框架的開發人員在實現AppCompat擴展控件等特性的時候用到不少輔助類,你們能夠自行研究下其細節,學習下一些巧妙的實現思路。

  • AppCompatBackgroundHelper
  • AppCompatDrawableManager
  • AppCompatTextHelper
  • AppCompatTextViewAutoSizeHelper
  • AppCompatTextClassifierHelper
  • AppCompatResources
  • AppCompatImageHelper

...

9. 類圖

最後上一下AppCompat框架的簡易類圖,幫助你們有個總體上的認識。

在這裏插入圖片描述

10. 結語

能夠看到AppCompat框架總體比較簡單,所以也容易被你們忽視。但做爲Jetpack系列裏的基石,瞭解一下頗有必要。

相關文章
相關標籤/搜索