主題包---根據(jù)思路代碼實現(xiàn)

kotlin vs java

主題包---源碼解析,思路分析
SkinDemo

根據(jù)上文中所分析的思路,我們來具體實現(xiàn):

  1. 首先铛绰,創(chuàng)建我們的activity逸雹,并且重寫Factory2的方法挽鞠。
  2. 然后,自定義我們的CustomAppcompatInflater繼承AppcompatInflater乡翅。本來我們應(yīng)該接下來重寫他的onCreate()方法的星澳,但是他是final疚顷,所以我們就自定義一個方法,功能和它一致就可以了禁偎,因為最后我們需要的是創(chuàng)建具體的控件的子類腿堤,所以這對我們沒什么影響。
  3. 最后如暖,創(chuàng)建我們具體需要改變顏色的控件CustomButton繼承MaterialButton笆檀,上文也說了,一定注意繼承的是該控件的最終子類盒至,否者不會支持MaterialButton酗洒。
  4. 該創(chuàng)建類就三個,有這三個類我們就可以實現(xiàn)加載布局過程中攔截Button的屬性進行自定義操作枷遂。
  5. 接下來我們仿照系統(tǒng)兼容包處理方法樱衷,在acitivity的重寫Factory2方法中創(chuàng)建自定義的CustomAppcompatInflater對象,然后返回自定義的方法酒唉,創(chuàng)建view矩桂,自定義方法中拿著獲取到的控件nameattributes創(chuàng)建出我們的自定義控件,接下來上代碼:
attrs.xml
 <!-- Button控件繼承TextView痪伦,此處parent語法通過侄榴,但無效果雹锣,不像style.xml -->
    <declare-styleable name="CustomButton">
        <attr name="android:background" />
        <attr name="android:textColor" />
        <!-- 字體屬性 -->
    </declare-styleable>

 @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if (openChangeSkin() && !ignoreView(name)) {
            if (viewInflater == null) {
                viewInflater = new CustomAppCompatViewInflater(context);
            }
            viewInflater.setName(name);
            viewInflater.setAttrs(attrs);
            return viewInflater.autoMatch();
        }
        return super.onCreateView(parent, name, context, attrs);
    }


/**
 * 自定義控件加載器(可以考慮該類不被繼承)
 */
public final class CustomAppCompatViewInflater extends AppCompatViewInflater {

    private String name; // 控件名
    private Context context; // 上下文
    private AttributeSet attrs; // 某控件對應(yīng)所有屬性

    public CustomAppCompatViewInflater(@NonNull Context context) {
        this.context = context;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAttrs(AttributeSet attrs) {
        this.attrs = attrs;
    }

    /**
     * @return 自動匹配控件名,并初始化控件對象
     */
    public View autoMatch() {
        View view = null;
        switch (name) {
            case BUTTON:
                view = new CustomButton(context, attrs);
                this.verifyNotNull(view, name);
                break;
        }

        return view;
    }

    /**
     * 校驗控件不為空(源碼方法癞蚕,由于private修飾蕊爵,只能復(fù)制過來了。為了代碼健壯桦山,可有可無)
     *
     * @param view 被校驗控件攒射,如:AppCompatTextView extends TextView(v7兼容包,兼容是重點:闼4衣ā!)
     * @param name 控件名寇窑,如:"ImageView"
     */
    private void verifyNotNull(View view, String name) {
        if (view == null) {
            throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
        }
    }
}

/**
 * 繼承TextView兼容包,9.0源碼中也是如此
 * 參考:AppCompatViewInflater.java
 * 86行 + 138行 + 206行
 */
public class CustomButton extends MaterialButton implements ViewsMatch {

    private AttrsBean attrsBean;

    public CustomButton(Context context) {
        this(context, null);
    }

    public CustomButton(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.buttonStyle);
    }

    public CustomButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        attrsBean = new AttrsBean();

        // 根據(jù)自定義屬性箩张,匹配控件屬性的類型集合甩骏,如:background + textColor
        TypedArray typedArray = context.obtainStyledAttributes(attrs,
                R.styleable.CustomButton,
                defStyleAttr, 0);
        // 存儲到臨時JavaBean對象
        attrsBean.saveViewResource(typedArray, R.styleable.CustomButton);
        // 這一句回收非常重要!obtainStyledAttributes()有語法提示O瓤丁饮笛!
        typedArray.recycle();
    }

    @Override
    public void skinnableView() {
        // 根據(jù)自定義屬性,獲取styleable中的background屬性
        int key = R.styleable.CustomButton[R.styleable.CustomButton_android_background];
      
        // 根據(jù)自定義屬性论熙,獲取styleable中的textColor屬性
        key = R.styleable.CustomButton[R.styleable.CustomButton_android_textColor];
        int textColorResourceId = attrsBean.getViewResource(key);
        if (textColorResourceId > 0) {
            if (SkinManager.getInstance().isDefaultSkin()) {
                ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
                setTextColor(color);
            } else {
                ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
                setTextColor(color);
            }
        }
    }
}

以上代碼福青,在每次主題切換的時候遍歷控件調(diào)用skinnableView()方法,該方法中判斷是否存在主題資源文件脓诡,根據(jù)資源id獲取宿主或者主題包里面的對應(yīng)顏色值設(shè)置顏色无午。


加載主題包

直接上代碼,這個個人覺得沒太大必要細講祝谚,一看就能懂宪迟,而且代碼都注釋很清楚,值得注意的是里面三個位置的TODO注釋:


public class SkinManager {

    private static SkinManager instance;
    private Application application;
    private Resources appResources; // 用于加載app內(nèi)置資源
    private Resources skinResources; // 用于加載皮膚包資源
    private String skinPackageName = ""; // 皮膚包資源所在包名(注:皮膚包不在app內(nèi)交惯,也不限包名)
    private boolean isDefaultSkin = true; // 應(yīng)用默認皮膚(app內(nèi)置)
    private static final String ADD_ASSET_PATH = "addAssetPath"; // 方法名
    private Map<String, SkinCache> cacheSkin;

    private SkinManager(Application application) {
        this.application = application;
        appResources = application.getResources();
        cacheSkin = new HashMap<>();
    }

    /**
     * 單例方法次泽,目的是初始化app內(nèi)置資源(越早越好,用戶的操作可能是:換膚后的第2次冷啟動)
     */
    public static void init(Application application) {
        if (instance == null) {
            synchronized (SkinManager.class) {
                if (instance == null) {
                    instance = new SkinManager(application);
                }
            }
        }
    }

    public static SkinManager getInstance() {
        return instance;
    }

    /**
     * 加載皮膚包資源
     *
     * @param skinPath 皮膚包路徑席爽,為空則加載app內(nèi)置資源
     */
    public void loaderSkinResources(String skinPath) {
        // 優(yōu)化:如果沒有皮膚包或者沒做換膚動作意荤,方法不執(zhí)行直接返回!
        if (TextUtils.isEmpty(skinPath)) {
            isDefaultSkin = true;
            return;
        }

        // 優(yōu)化:app冷啟動只锻、熱啟動可以取緩存對象
        if (cacheSkin.containsKey(skinPath)) {
            isDefaultSkin = false;
            SkinCache skinCache = cacheSkin.get(skinPath);
            if (null != skinCache) {
                skinResources = skinCache.getSkinResources();
                skinPackageName = skinCache.getSkinPackageName();
                return;
            }
        }

        try {
            // 創(chuàng)建資源管理器(此處不能用:application.getAssets())
            AssetManager assetManager = AssetManager.class.newInstance();
            // 由于AssetManager中的addAssetPath和setApkAssets方法都被@hide玖像,目前只能通過反射去執(zhí)行方法
            Method addAssetPath = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
            // 設(shè)置私有方法可訪問
            addAssetPath.setAccessible(true);
            // 執(zhí)行addAssetPath方法
            addAssetPath.invoke(assetManager, skinPath);
            //==============================================================================
            // 如果還是擔心@hide限制,可以反射addAssetPathInternal()方法炬藤,參考源碼366行 + 387行
            //==============================================================================

            // 創(chuàng)建加載外部的皮膚包(net163.skin)文件Resources(注:依然是本應(yīng)用加載)
            skinResources = new Resources(assetManager,
                    appResources.getDisplayMetrics(), appResources.getConfiguration());

            // 根據(jù)apk文件路徑(皮膚包也是apk文件)御铃,獲取該應(yīng)用的包名碴里。兼容5.0 - 9.0(親測)
            skinPackageName = application.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;

            // 無法獲取皮膚包應(yīng)用的包名,則加載app內(nèi)置資源
            isDefaultSkin = TextUtils.isEmpty(skinPackageName);
            if (!isDefaultSkin) {
                cacheSkin.put(skinPath, new SkinCache(skinResources, skinPackageName));
            }

            Log.e("skinPackageName >>> ", skinPackageName);

        } catch (Exception e) {
            e.printStackTrace();
            // 發(fā)生異常上真,預(yù)判:通過skinPath獲取skinPacakageName失斠б浮!
            isDefaultSkin = true;
        }
    }

    /**
     * 參考:resources.arsc資源映射表
     * 通過ID值獲取資源 Name 和 Type
     *
     * @param resourceId 資源ID值
     * @return 如果沒有皮膚包則加載app內(nèi)置資源ID睡互,反之加載皮膚包指定資源ID
     */
    private int getSkinResourceIds(int resourceId) {
        // 優(yōu)化:如果沒有皮膚包或者沒做換膚動作根竿,直接返回app內(nèi)置資源!
        if (isDefaultSkin) return resourceId;

        // 使用app內(nèi)置資源加載就珠,是因為內(nèi)置資源與皮膚包資源一一對應(yīng)(“netease_bg”, “drawable”)
        String resourceName = appResources.getResourceEntryName(resourceId);
        String resourceType = appResources.getResourceTypeName(resourceId);

        // 動態(tài)獲取皮膚包內(nèi)的指定資源ID
        // getResources().getIdentifier(“netease_bg”, “drawable”, “com.netease.skin.packages”);
        int skinResourceId = skinResources.getIdentifier(resourceName, resourceType, skinPackageName);

        // 源碼1924行:(0 is not a valid resource ID.)
        // TODO: 2020/8/5 此處有問題,當我主題包中沒有該資源的時候會導(dǎo)致isDefault變成true寇壳,這就導(dǎo)致了遍歷view的時候無法改變主題,然而這里skinResourceId == 0是對每一個資源的判斷不能代表整個資源包
//        isDefaultSkin = skinResourceId == 0;
        // TODO: 2020/8/5 此處直接返回資源包獲取的id值妻怎,交給下一步操作判斷當前id應(yīng)該是獲取的宿主的還是資源包的
        return skinResourceId;
    }

    public boolean isDefaultSkin() {
        return isDefaultSkin;
    }

    //==============================================================================================
    // TODO: 2020/8/5 此處根據(jù)獲取的資源包id來判斷是該加載宿主還是資源包
    public int getColor(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return (ids == 0 || ids == resourceId) ? appResources.getColor(resourceId) : skinResources.getColor(ids);
    }

    public ColorStateList getColorStateList(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return (ids == 0 || ids == resourceId) ? appResources.getColorStateList(resourceId) : skinResources.getColorStateList(ids);
    }

    // mipmap和drawable統(tǒng)一用法(待測)
    public Drawable getDrawableOrMipMap(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return (ids == 0 || ids == resourceId) ? appResources.getDrawable(resourceId) : skinResources.getDrawable(ids);
    }

    public String getString(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return (ids == 0 || ids == resourceId) ? appResources.getString(resourceId) : skinResources.getString(ids);
    }

    // 返回值特殊情況:可能是color / drawable / mipmap
    public Object getBackgroundOrSrc(int resourceId) {
        // 需要獲取當前屬性的類型名Resources.getResourceTypeName(resourceId)再判斷
        String resourceTypeName = appResources.getResourceTypeName(resourceId);

        switch (resourceTypeName) {
            case "color":
                return getColor(resourceId);

            case "mipmap": // drawable / mipmap
            case "drawable":
                return getDrawableOrMipMap(resourceId);
        }
        return null;
    }

    // 獲得字體
    public Typeface getTypeface(int resourceId) {
        // 通過資源ID獲取資源path壳炎,參考:resources.arsc資源映射表
        String skinTypefacePath = getString(resourceId);
        // 路徑為空,使用系統(tǒng)默認字體
        if (TextUtils.isEmpty(skinTypefacePath)) return Typeface.DEFAULT;
        return isDefaultSkin ? Typeface.createFromAsset(appResources.getAssets(), skinTypefacePath)
                : Typeface.createFromAsset(skinResources.getAssets(), skinTypefacePath);
    }
}

總結(jié):兩篇文章逼侦,大致分析了整個實現(xiàn)過程匿辩,最后總結(jié)梳理一下,首先是我們實現(xiàn)主體更好的切入點是Factory接口榛丢,整個接口是專門攔截控件的铲球,然后仿造系統(tǒng)兼容包的實現(xiàn)自定義Inflater,創(chuàng)建我們的兼容對象晰赞,通過我們兼容對象來自定義顏色等屬性稼病,這里的顏色獲取就是通過宿主或者主題資源包來獲得,主題資源包我們通過資源加載的方式通過宿主的上下文加載資源包的資源掖鱼,獲取對應(yīng)的屬性值然走,因為我們資源包的資源定義名稱和宿主保持一致。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锨用,一起剝皮案震驚了整個濱河市丰刊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌增拥,老刑警劉巖啄巧,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異掌栅,居然都是意外死亡秩仆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門猾封,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澄耍,“玉大人,你說我怎么就攤上這事∑肓” “怎么了痢站?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長选酗。 經(jīng)常有香客問我阵难,道長,這世上最難降的妖魔是什么芒填? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任呜叫,我火速辦了婚禮,結(jié)果婚禮上殿衰,老公的妹妹穿的比我還像新娘朱庆。我一直安慰自己,他們只是感情好闷祥,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布娱颊。 她就那樣靜靜地躺著,像睡著了一般凯砍。 火紅的嫁衣襯著肌膚如雪维蒙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天果覆,我揣著相機與錄音,去河邊找鬼殖熟。 笑死局待,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的菱属。 我是一名探鬼主播钳榨,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼纽门!你這毒婦竟也來了薛耻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤赏陵,失蹤者是張志新(化名)和其女友劉穎饼齿,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝙搔,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡缕溉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吃型。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片证鸥。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出枉层,到底是詐尸還是另有隱情泉褐,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布鸟蜡,位于F島的核電站膜赃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏矩欠。R本人自食惡果不足惜财剖,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望癌淮。 院中可真熱鬧躺坟,春花似錦、人聲如沸乳蓄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虚倒。三九已至美侦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間魂奥,已是汗流浹背菠剩。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留耻煤,地道東北人具壮。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像哈蝇,于是被迫代替她去往敵國和親棺妓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348