View的繪制(6)-換膚框架實現(xiàn)解析(二)

主目錄見:Android高級進階知識(這是總目錄索引)
?終于迎來我們的換膚框架最終章了梢睛,前面我們也學了support v7的源碼了召娜,那么今天我們這里就輕松很多闸与,廢話不多說洋满,直接開始講解剩晴。想要源碼的直接[點擊下載],這老哥穩(wěn)B嘀洹!赞弥!大家向他學習毅整。

敬禮

一.目標

看過前面幾篇的問題都知道,文章前面會有目標說明绽左,今天不例外悼嫉。
1.復習《Support v7庫》的知識點。
2.說下framework怎么加載資源拼窥。
3.同時說明下Application的registerActivityLifecycleCallbacks方法戏蔑。

二.源碼分析

1.基礎使用

在Application的onCreate中初始化:

 SkinCompatManager.withoutActivity(this)                         // 基礎控件換膚初始化
            .addStrategy(new CustomSDCardLoader())                  // 自定義加載策略,指定SDCard路徑[可選]
            .addHookInflater(new SkinHookAutoLayoutViewInflater())  // hongyangAndroid/AndroidAutoLayout[可選]
            .addInflater(new SkinMaterialViewInflater())            // material design 控件換膚初始化[可選]
            .addInflater(new SkinConstraintViewInflater())          // ConstraintLayout 控件換膚初始化[可選]
            .addInflater(new SkinCardViewInflater())                // CardView v7 控件換膚初始化[可選]
            .addInflater(new SkinCircleImageViewInflater())         // hdodenhof/CircleImageView[可選]
            .addInflater(new SkinFlycoTabLayoutInflater())          // H07000223/FlycoTabLayout[可選]
            .setSkinStatusBarColorEnable(false)                     // 關(guān)閉狀態(tài)欄換膚鲁纠,默認打開[可選]
            .setSkinWindowBackgroundEnable(false)                   // 關(guān)閉windowBackground換膚总棵,默認打開[可選]
            .loadSkin();

加載插件皮膚庫:

// 指定皮膚插件
SkinCompatManager.getInstance().loadSkin("new.skin"[, SkinLoaderListener], int strategy);

// 恢復應用默認皮膚
SkinCompatManager.getInstance().restoreDefaultTheme();

本來以前庫是要繼承SkinCompatActivity的,但是現(xiàn)在用了registerActivityLifecycleCallbacks監(jiān)聽了Activity的生命周期改含,這樣可以燥起來情龄,就不用繼承了,等會會說明捍壤。

2.SkinCompatManager withoutActivity

這個地方遵循看源碼的一貫步驟骤视,我們來看下withoutActivity方法到底是干了啥?方法如下:

  public static SkinCompatManager withoutActivity(Application application) {
        init(application);
        SkinActivityLifecycle.init(application);
        return sInstance;
    }

這個方法總共就兩個方法鹃觉,我們順序來看init方法干了啥:

   public static SkinCompatManager init(Context context) {
        if (sInstance == null) {
            synchronized (SkinCompatManager.class) {
                if (sInstance == null) {
                    sInstance = new SkinCompatManager(context);
                }
            }
        }
        return sInstance;
    }

這個方法其實就是創(chuàng)建一個SkinCompatManager這個單例對象专酗,那我們就看這個構(gòu)造函數(shù)里面做了些啥:

   private SkinCompatManager(Context context) {
        mAppContext = context.getApplicationContext();
        SkinPreference.init(mAppContext);
        SkinCompatResources.init(mAppContext);
        initLoaderStrategy();
    }

這個方法主要是做一些初始化工作,第一個類SkinPreference其實就是對SharePreference的封裝盗扇,SkinCompatResources其實就是存放包名祷肯,皮膚名,加載策略(就是從什么途徑加載)等等疗隶,同時有一些加載資源的方法躬柬。最后initLoaderStrategy()方法是加載默認的皮膚包加載策略。
?看完第一個方法抽减,我們現(xiàn)在看第二個方法SkinActivityLifecycle.init(application)允青,這個方法非常關(guān)鍵,我們先看看init做了啥?

   public static SkinActivityLifecycle init(Application application) {
        if (sInstance == null) {
            synchronized (SkinActivityLifecycle.class) {
                if (sInstance == null) {
                    sInstance = new SkinActivityLifecycle(application);
                }
            }
        }
        return sInstance;
    }

首先也是初始化一個SkinActivityLifecycle單例類颠锉,我們也直接跟進構(gòu)造函數(shù)里面:

 private SkinActivityLifecycle(Application application) {
        application.registerActivityLifecycleCallbacks(this);
    }

這個地方就是一句話法牲,但是非常關(guān)鍵,記得在LeakCanary源碼里面也有用到這個方法琼掠。這個方法是攔截Acitivity的生命周期方法來進行統(tǒng)一處理拒垃,這樣的話我們不用讓我們的Acitivity繼承SkinCompatActivity(所以現(xiàn)在這個類被標記為廢棄),就可以在每個Acitivity創(chuàng)建的時候做點動作了,非常管用瓷蛙。具體的Activity的生命周期里面做了哪些動作悼瓮,我們下面會重點講解。

3.SkinCompatManager loadSkin

這個方法是用來加載皮膚包的艰猬,我們這里先看loadSkin做了些什么東西横堡。

   public AsyncTask loadSkin() {
        String skin = SkinPreference.getInstance().getSkinName();
        int strategy = SkinPreference.getInstance().getSkinStrategy();
        if (TextUtils.isEmpty(skin) || strategy == SKIN_LOADER_STRATEGY_NONE) {
            return null;
        }
        return loadSkin(skin, null, strategy);
    }

我們看到這個地方,首先獲取本地是否有皮膚包名稱和皮膚包策略的記錄冠桃,如果有則取出來進行加載命贴。

 public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
        return new SkinLoadTask(listener, mStrategyMap.get(strategy)).execute(skinName);
    }

這個地方用到了SkinLoadTask來進行加載,SkinLoadTask是一個AsyncTask對象食听,所以我們先看onPreExecute()方法:

 protected void onPreExecute() {
            if (mListener != null) {
                mListener.onStart();
            }
        }

這個方法沒有做啥胸蛛,就是調(diào)用接口的onStart方法,但是我們前面創(chuàng)建來的Listener是null樱报,所以這個地方?jīng)]有調(diào)用葬项。接下來我們看到doInBackground()方法:

        @Override
        protected String doInBackground(String... params) {
            synchronized (mLock) {
                while (mLoading) {
                    try {
                        mLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                mLoading = true;
            }
            try {
                if (params.length == 1) {
                    if (TextUtils.isEmpty(params[0])) {
                        SkinCompatResources.getInstance().reset();
                        return params[0];
                    }
                    if (!TextUtils.isEmpty(
                            mStrategy.loadSkinInBackground(mAppContext, params[0]))) {
                        return params[0];
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            SkinCompatResources.getInstance().reset();
            return null;
        }

這個方法前面加了一個同步代碼塊進行同步操作,然后我們看到調(diào)用了SkinCompatResources.getInstance().reset();將這個對象之前存進去的皮膚包名稱迹蛤,策略等信息重置民珍。接著調(diào)用mStrategy.loadSkinInBackground(mAppContext, params[0])方法,這個地方mStrategy就是加載策略笤受,內(nèi)置了三個策略分別為:SkinAssetsLoader穷缤,SkinBuildInLoader敌蜂,SkinSDCardLoader箩兽,第一個就是從Assets中加載皮膚包,第二個就是從本應用中加載皮膚包章喉,第三個是從SD卡中加載皮膚包汗贫。這個地方我們用從SD卡中加載皮膚包為例子,因為這個場景用到還比較多秸脱。

4.SkinSDCardLoader loadSkinInBackground

這個類是個抽象類落包,為什么設置為抽象類,是因為SD卡的目錄作者希望留給用戶自己設置摊唇。我們直接進入到這個類的loadSkinInBackground方法:

 public String loadSkinInBackground(Context context, String skinName) {
        String skinPkgPath = getSkinPath(context, skinName);
        if (SkinFileUtils.isFileExists(skinPkgPath)) {
            String pkgName = SkinCompatManager.getInstance().getSkinPackageName(skinPkgPath);
            Resources resources = SkinCompatManager.getInstance().getSkinResources(skinPkgPath);
            if (resources != null && !TextUtils.isEmpty(pkgName)) {
                SkinCompatResources.getInstance().setupSkin(
                        resources,
                        pkgName,
                        skinName,
                        this);
                return skinName;
            }
        }
        return null;
    }

我們看到第一句就是獲取皮膚包的路徑咐蝇,這個方法由用戶自己指定,我們可以集成這個類進行重寫巷查。然后程序判斷目錄是否存在有序,如果存在則獲取皮膚包的包名:

 public String getSkinPackageName(String skinPkgPath) {
        PackageManager mPm = mAppContext.getPackageManager();
        PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
        return info.packageName;
    }

然后獲取Resources對象:

  @Nullable
    public Resources getSkinResources(String skinPkgPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, skinPkgPath);

            Resources superRes = mAppContext.getResources();
            return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

這個地方有個知識點:就是系統(tǒng)是怎么加載資源的抹腿。要詳細可以看Android應用程序資源管理器(Asset Manager)的創(chuàng)建過程分析 Android應用程序資源的查找過程分析 。這個地方我們簡單歸納就是會調(diào)用AssetManager里面的final方法addAssetPath()旭寿,所以我們傳進皮膚包的路徑警绩,反射調(diào)用這個方法進行添加即可。這樣我們的外部皮膚包就被加進去了盅称。

5.SkinLoadTask onPostExecute

看完onPreExecute()方法和doInBackground()方法我們就來看onPostExecute方法了:

        protected void onPostExecute(String skinName) {
            SkinLog.e("skinName = " + skinName);
            synchronized (mLock) {
                // skinName 為""時肩祥,恢復默認皮膚
                if (skinName != null) {
                    SkinPreference.getInstance().setSkinName(skinName).setSkinStrategy(mStrategy.getType()).commitEditor();
                    notifyUpdateSkin();
                    if (mListener != null) mListener.onSuccess();
                } else {
                    SkinPreference.getInstance().setSkinName("").setSkinStrategy(SKIN_LOADER_STRATEGY_NONE).commitEditor();
                    if (mListener != null) mListener.onFailed("皮膚資源獲取失敗");
                }
                mLoading = false;
                mLock.notifyAll();
            }
        }

這里首先是將我們的策略類型保存起來,然后調(diào)用notifyUpdateSkin(),這個方法是做什么呢缩膝?因為這邊加載完皮膚會通知Acitivity里面的視圖控件跟著皮膚進行變化混狠,這里用到了觀察者設計模式,在每個Activity onResume的時候會將Activity添加為觀察者逞盆,所以這個地方notifyUpdateSkin就是調(diào)用到

   observer = new SkinObserver() {
                @Override
                public void updateSkin(SkinObservable observable, Object o) {
                    updateStatusBarColor(activity);
                    updateWindowBackground(activity);
                    getSkinDelegate((AppCompatActivity) activity).applySkin();
                }
            };

刷新狀態(tài)欄檀蹋,刷新背景,然后通知每個控件進行重新設置皮膚云芦。跟之前套路不一樣俯逾,我們將要進入主要知識講解了。為了蹭iphone發(fā)布會熱點舅逸,決定輕松一刻:


賣腎

6.SkinActivityLifecycle onActivityCreated

我們知道之前有一句話:

 private SkinActivityLifecycle(Application application) {
        application.registerActivityLifecycleCallbacks(this);
    }

這句話已經(jīng)將SkinActivityLifecycle 設置為生命周期攔截器了桌肴,這樣我們就可以攔截到Acitivity的每個生命周期,我們先來看我們Activity創(chuàng)建的生命周期即onActivityCreated:

@Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        if (activity instanceof AppCompatActivity) {
            LayoutInflater layoutInflater = activity.getLayoutInflater();
            try {
                Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
                field.setAccessible(true);
                field.setBoolean(layoutInflater, false);
                LayoutInflaterCompat.setFactory(activity.getLayoutInflater(),
                        getSkinDelegate((AppCompatActivity) activity));
            } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
            updateStatusBarColor(activity);
            updateWindowBackground(activity);
        }
    }

看到這個方法我們應該有種似曾相識的感覺琉历,沒錯坠七!這里其實跟support V7的源碼是一樣的。重點強調(diào):LayoutInflaterCompat.setFactory(activity.getLayoutInflater(),getSkinDelegate((AppCompatActivity) activity));這個地方就是自定義Factory攔截View的創(chuàng)建過程旗笔。那我們還是那個套路看下getSkinDelegate()這個方法做了啥:

    private SkinCompatDelegate getSkinDelegate(AppCompatActivity activity) {
        if (mSkinDelegateMap == null) {
            mSkinDelegateMap = new WeakHashMap<>();
        }

        SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(activity);
        if (mSkinDelegate == null) {
            mSkinDelegate = SkinCompatDelegate.create(activity);
        }
        mSkinDelegateMap.put(activity, mSkinDelegate);
        return mSkinDelegate;
    }

這個地方還有緩存彪置,作者不錯,首先看HashMap(這里要用WeakHashMap主要是因為持有了Activity的實例蝇恶,為了防止內(nèi)存泄漏所以用了弱引用的HashMap)里面有沒有SkinCompatDelegate對象拳魁,沒有則創(chuàng)建。SkinCompatDelegate是個LayoutInflaterFactory即Factory對象撮弧,所以我們xml里面View創(chuàng)建的時候會調(diào)用Factory里面的onCreateView()方法潘懊。

7.SkinCompatDelegate onCreateView

因為這個方法攔截了view的創(chuàng)建過程,所以我們就可以看到這個方法做了啥贿衍,其實我們已經(jīng)很熟悉這個方法了:

   @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View view = createView(parent, name, context, attrs);

        if (view == null) {
            return null;
        }
        if (view instanceof SkinCompatSupportable) {
//這個主要是添加進mSkinHelpers內(nèi)授舟,到時調(diào)用notifyUpdateSkin的時候會調(diào)用刷新
            mSkinHelpers.add(new WeakReference<SkinCompatSupportable>((SkinCompatSupportable) view));
        }

        return view;
    }

我們知道我們調(diào)用createView方法,那么創(chuàng)建view的任務就是這個方法了:

   public View createView(View parent, final String name, @NonNull Context context,
                           @NonNull AttributeSet attrs) {
        final boolean isPre21 = Build.VERSION.SDK_INT < 21;

        if (mSkinCompatViewInflater == null) {
            mSkinCompatViewInflater = new SkinCompatViewInflater();
        }

        // We only want the View to inherit its context if we're running pre-v21
        final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);

        return mSkinCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }

前面初始化了mSkinCompatViewInflater對象然后調(diào)用它的createView方法贸辈,我們繼續(xù)跟進去:

public final View createView(View parent, final String name, @NonNull Context context,
                                 @NonNull AttributeSet attrs, boolean inheritContext,
                                 boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;

        // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
        // by using the parent's context
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }
        if (readAndroidTheme || readAppTheme) {
            // We then apply the theme on the context, if specified
            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
        }
        if (wrapContext) {
            context = TintContextWrapper.wrap(context);
        }

        View view = createViewFromHackInflater(context, name, attrs);

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        if (view == null) {
            view = createViewFromFV(context, name, attrs);
        }

        if (view == null) {
            view = createViewFromV7(context, name, attrs);
        }

        if (view == null) {
            view = createViewFromInflater(context, name, attrs);
        }

        if (view == null) {
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check it's android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

前面跟我們support v7源碼是一樣的释树,我們直接看到createViewFromHackInflater方法,這個方法我們看到嘗試創(chuàng)建了view對象:

   private View createViewFromHackInflater(Context context, String name, AttributeSet attrs) {
        View view = null;
        for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getHookInflaters()) {
            view = inflater.createView(context, name, attrs);
            if (view == null) {
                continue;
            } else {
                break;
            }
        }
        return view;
    }

我們看到代碼應該很熟悉呀,這個方法其實就是讓用戶可以設置SkinLayoutInflater奢啥,然后來得到優(yōu)先攔截創(chuàng)建view的能力署浩。我們假裝用戶沒有設置,那么返回的view就會為null扫尺,那么就會進入下一個createViewFromFV()方法:

    private View createViewFromFV(Context context, String name, AttributeSet attrs) {
        View view = null;
        if (name.contains(".")) {
            return null;
        }
        switch (name) {
            case "View":
                view = new SkinCompatView(context, attrs);
                break;
            case "LinearLayout":
                view = new SkinCompatLinearLayout(context, attrs);
                break;
            case "RelativeLayout":
                view = new SkinCompatRelativeLayout(context, attrs);
                break;
            case "FrameLayout":
                view = new SkinCompatFrameLayout(context, attrs);
                break;
            case "TextView":
                view = new SkinCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new SkinCompatImageView(context, attrs);
                break;
            case "Button":
                view = new SkinCompatButton(context, attrs);
                break;
            case "EditText":
                view = new SkinCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new SkinCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new SkinCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new SkinCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new SkinCompatRadioButton(context, attrs);
                break;
            case "RadioGroup":
                view = new SkinCompatRadioGroup(context, attrs);
                break;
            case "CheckedTextView":
                view = new SkinCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new SkinCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new SkinCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new SkinCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new SkinCompatSeekBar(context, attrs);
                break;
            case "ProgressBar":
                view = new SkinCompatProgressBar(context, attrs);
                break;
            case "ScrollView":
                view = new SkinCompatScrollView(context, attrs);
                break;
        }
        return view;
    }

我們可以看到這里攔截好多控件的創(chuàng)建過程筋栋。為了不使我們的文章冗長,這邊就挑一個控件來說明正驻,這里我們挑講解support v7時候同樣的控件TextView控件來講解弊攘。

8.SkinCompatTextView

這是個自定義的TextView(如果自己要往這個庫加入什么新的控件,套路也是一樣的)姑曙,又因為到時通知更新要更新控件襟交,所以每個控件必須實現(xiàn)SkinCompatSupportable接口:

public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
}

我們接下來看到構(gòu)造函數(shù):

    public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
        mTextHelper = SkinCompatTextHelper.create(this);
        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
    }

這里主要有兩個類SkinCompatBackgroundHelper和SkinCompatTextHelper,很明顯第一個是用來設置背景的伤靠,第二個類是設置Text相關(guān)外形的捣域。我們先來看SkinCompatBackgroundHelper的loadFromAttributes方法:

 public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
        TypedArray a = mView.getContext().obtainStyledAttributes(attrs, R.styleable.SkinBackgroundHelper, defStyleAttr, 0);
        try {
            if (a.hasValue(R.styleable.SkinBackgroundHelper_android_background)) {
                mBackgroundResId = a.getResourceId(
                        R.styleable.SkinBackgroundHelper_android_background, INVALID_ID);
            }
        } finally {
            a.recycle();
        }
        applySkin();
    }

這里清晰明了,對老司機來說這都不是事宴合,獲取自定義屬性backgroud焕梅,但是這里有個小技巧我們來看下SkinBackgroundHelper_android_background對應的屬性是啥:

  <declare-styleable name="SkinBackgroundHelper">
        <attr name="android:background" />
    </declare-styleable>

看到?jīng)]有!X郧ⅰ贞言!看到?jīng)]有!7У佟该窗!其實他對應的就是系統(tǒng)的background,為什么這么做呢蚤霞?就是為了我們在寫background的時候不需要麻煩再去自定義酗失,只要寫上background即可,是不是處處有干貨昧绣,這干貨很干刊咳。
接下來我們看下SkinCompatTextHelper的loadFromAttributes:

 public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
        final Context context = mView.getContext();

        // First read the TextAppearance style id
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SkinCompatTextHelper, defStyleAttr, 0);
        final int ap = a.getResourceId(R.styleable.SkinCompatTextHelper_android_textAppearance, INVALID_ID);
        SkinLog.d(TAG, "ap = " + ap);

        if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {
            mDrawableLeftResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, INVALID_ID);
        }
        if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {
            mDrawableTopResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, INVALID_ID);
        }
        if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {
            mDrawableRightResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, INVALID_ID);
        }
        if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {
            mDrawableBottomResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, INVALID_ID);
        }
        a.recycle();

        if (ap != INVALID_ID) {
            a = context.obtainStyledAttributes(ap, R.styleable.SkinTextAppearance);
            if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
                mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
                SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
            }
            if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
                mTextColorHintResId = a.getResourceId(
                        R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
                SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
            }
            a.recycle();
        }

        // Now read the style's values
        a = context.obtainStyledAttributes(attrs, R.styleable.SkinTextAppearance, defStyleAttr, 0);
        if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
            mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
            SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
        }
        if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
            mTextColorHintResId = a.getResourceId(
                    R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
            SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
        }
        a.recycle();
        applySkin();
    }

看著這么多代碼不要被嚇到扔涧,其實非常簡單廷雅,我們看下style:

    <declare-styleable name="SkinCompatTextHelper">
        <attr name="android:drawableLeft" />
        <attr name="android:drawableTop" />
        <attr name="android:drawableRight" />
        <attr name="android:drawableBottom" />
        <attr name="android:drawableStart" />
        <attr name="android:drawableEnd" />
        <attr name="android:textAppearance" />
    </declare-styleable>

其實就是獲取這些屬性值壳鹤。我去鉴嗤。斩启。。醉锅。上面代碼白貼了兔簇。最后會調(diào)用applySkin()方法,就是去設置這些屬性:

  public void applySkin() {
        applyTextColorResource();
        applyTextColorHintResource();
        applyCompoundDrawablesResource();
    }

從方法名就可以很清晰地看出來。就不過多糾結(jié)了垄琐,這個方法applySkin是SkinCompatSupportable 接口里面的边酒,在觀察者模式提示更新的時候也會調(diào)用到這個方法,所以我們自定義的換膚控件都必須實現(xiàn)這個接口狸窘,這是個約定墩朦。
到這里我們的講解就完成,這篇真的是干貨和技能MAX翻擒,媽媽再也不用擔心我求干貨了氓涣。

干貨君

總結(jié):這個換膚框架是比較綜合的一個support v7知識應用,同時包含了許多的小技巧陋气,都是自定義控件或者其他地方能用到的劳吠,是一個解決方案。希望大家有所收獲巩趁,謝謝堅持看完痒玩,說明大哥你是閑人中的戰(zhàn)斗機!R槲俊蠢古!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市别凹,隨后出現(xiàn)的幾起案子便瑟,更是在濱河造成了極大的恐慌,老刑警劉巖番川,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件到涂,死亡現(xiàn)場離奇詭異,居然都是意外死亡颁督,警方通過查閱死者的電腦和手機践啄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沉御,“玉大人屿讽,你說我怎么就攤上這事》婉桑” “怎么了伐谈?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長试疙。 經(jīng)常有香客問我诵棵,道長,這世上最難降的妖魔是什么祝旷? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任履澳,我火速辦了婚禮嘶窄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘距贷。我一直安慰自己柄冲,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布忠蝗。 她就那樣靜靜地躺著现横,像睡著了一般。 火紅的嫁衣襯著肌膚如雪阁最。 梳的紋絲不亂的頭發(fā)上长赞,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機與錄音闽撤,去河邊找鬼得哆。 笑死,一個胖子當著我的面吹牛哟旗,可吹牛的內(nèi)容都是我干的贩据。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼闸餐,長吁一口氣:“原來是場噩夢啊……” “哼饱亮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起舍沙,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤近上,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拂铡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壹无,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年感帅,在試婚紗的時候發(fā)現(xiàn)自己被綠了斗锭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡失球,死狀恐怖岖是,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情实苞,我是刑警寧澤豺撑,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站黔牵,受9級特大地震影響聪轿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜荧止,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一屹电、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧跃巡,春花似錦危号、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至兔朦,卻和暖如春偷线,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背沽甥。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工声邦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摆舟。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓亥曹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親恨诱。 傳聞我的和親對象是個殘疾皇子媳瞪,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,735評論 25 707
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,365評論 0 17
  • OK體魄六六六積極那就考慮考慮 計劃 hello
    f8e4ed5b616b閱讀 195評論 0 0
  • 剛剛下載簡書謝謝讓我燃起希望習畫念頭的美女照宝。 很多文藝情懷沉海蛇受,總是不能堅持的半吊子惰性,讓我多有慚愧厕鹃。珍惜和簡書...
    waterfront閱讀 149評論 2 2