Android_動(dòng)態(tài)換皮膚功能

效果:

可修改字體類型,字體顏色,背景顏色,背景圖案.等等
可配合服務(wù)端,提供在線下載皮膚功能,下載完成即時(shí)生效替換資源.

效果圖.gif

實(shí)現(xiàn)思路:

1.采樣: 找到需要替換的所有view控件,記錄保存起來
2.替換皮膚資源: 利用AssetManager.加載皮膚資源,生成Resources,在給view設(shè)置資源屬性的時(shí)候,使用皮膚資源Resources來設(shè)置

實(shí)現(xiàn)原理:

皮膚包其實(shí)是一個(gè)apk,在更換皮膚的時(shí)候,其實(shí)是使用皮膚包里面的資源,來替換本地app的資源文件.

1.AssetManager加載皮膚包資源

AssetManager里面有一個(gè)hide的方法addAssetPath,通過反射調(diào)用這個(gè)方法可以給AssetManager設(shè)置我們皮膚資源的path,來加載皮膚資源

/**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
        return  addAssetPathInternal(path, false);
    }
AssetManager assetManager = AssetManager.class.newInstance();
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.setAccessible(true);
//調(diào)用addAssetPath方法,傳入皮膚資源路徑
method.invoke(assetManager,path);
//得到本app的application的resources
Resources resources = application.getResources();
//根據(jù)本app的resources的配置創(chuàng)建皮膚Resources
Resources skinResource = new Resources(assetManager, resources.getDisplayMetrics(),
                        resources.getConfiguration());
//獲取外部Apk(皮膚包) 包信息
PackageManager mPm = application.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
String packageName = info.packageName;
2.利用LayoutInflater采樣,找到需要換膚的view

查看系統(tǒng)setContentView(int resId)源碼發(fā)現(xiàn),view的創(chuàng)建是通過LayoutInflater來創(chuàng)建的,而LayoutInflater在創(chuàng)建view的過程中,我們可以通過給LayoutInflater.setFactory2(),來設(shè)置我們自己的Factory2,然后拿到需要替換皮膚的View

mLayoutInflater.inflate(layoutResID, mContentParent);
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        //得到xml解析器
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
//...省略
//調(diào)用createViewFromTag來創(chuàng)建xml對(duì)應(yīng)的View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {

        ......
        try {
            View view;
            //如果有mFactory2 ,就調(diào)用該工廠來創(chuàng)建view
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            //省略部分代碼...
            return view;
}

LayoutInflater.setFactory2(),回調(diào)到Factory的onCreateView()方法中,模仿系統(tǒng)源碼,實(shí)現(xiàn)創(chuàng)建view,并且拿到這個(gè)view,來給它設(shè)置資源,實(shí)現(xiàn)換皮膚效果

仿寫android系統(tǒng)源碼,利用classloader來創(chuàng)建view

//name 傳入view的全路徑,如果是android SDK提供的view,需要我們拼接路徑處理
//如果是自定義控件,或者控件在xml使用的時(shí)候帶 '.'的,就直接傳入該完整路徑
private View createView(String name, Context context, AttributeSet attrs) {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if(constructor == null) {
            try {
                //加載類的全路徑,得到class
                Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
                //得到構(gòu)造方法,參數(shù):mConstructorSignature是方法的參數(shù)類型.class
                constructor = aClass.getConstructor(mConstructorSignature);
                sConstructorMap.put(name, constructor);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if(null != constructor){
            try {
                return constructor.newInstance(context,attrs);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
3.過濾需要換皮膚的view,并且設(shè)置屬性

拿到上面創(chuàng)建的view,按照我們定義的條件,找到符合條件,需要替換皮膚的控件,并且設(shè)置屬性值

 private static final List<String> mAttributes = new ArrayList<>();
    static {
        //過濾的條件,有以下屬性的view才考慮換膚
        mAttributes.add("background");
        mAttributes.add("src");
        mAttributes.add("textColor");
        mAttributes.add("tabTextColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

switch (skinPair.attributeName) {
    //設(shè)置background
    case "background": 
        Object background = SkinResources.getInstance().getBackground(skinPair.resId);
        //Color
        if (background instanceof Integer) {
             view.setBackgroundColor((Integer) background);
        } else {
             ViewCompat.setBackground(view, (Drawable) background);
        }
        break;
    //設(shè)置textColor
    case "textColor":
        ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
                                (skinPair.resId));
        break;
    case "drawableLeft":
        left = SkinResources.getInstance().getDrawable(skinPair.resId);
        break;
    case "drawableTop":
        top = SkinResources.getInstance().getDrawable(skinPair.resId);
    ......
    //按照我們需要設(shè)置的屬性,并且賦值..
    default:
        break;
  }
if (null != left || null != right || null != top || null != bottom) {
    ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
                            bottom);
}

4.還原

使用APP默認(rèn)的Resources來加載資源,并且給換皮膚的view設(shè)置回APP默認(rèn)的Resources下的資源即可

注意:

1.皮膚包其實(shí)是一個(gè)只有資源文件的空殼apk
2.app的資源文件的名字,必須和apk的資源文件的名字一樣

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末藻雌,一起剝皮案震驚了整個(gè)濱河市砂蔽,隨后出現(xiàn)的幾起案子恨统,更是在濱河造成了極大的恐慌,老刑警劉巖撇叁,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡牵辣,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門躺枕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來服猪,“玉大人,你說我怎么就攤上這事拐云“罩恚” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵叉瘩,是天一觀的道長膳帕。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么危彩? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任攒磨,我火速辦了婚禮,結(jié)果婚禮上汤徽,老公的妹妹穿的比我還像新娘娩缰。我一直安慰自己,他們只是感情好谒府,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布拼坎。 她就那樣靜靜地躺著,像睡著了一般完疫。 火紅的嫁衣襯著肌膚如雪泰鸡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天壳鹤,我揣著相機(jī)與錄音盛龄,去河邊找鬼。 笑死芳誓,一個(gè)胖子當(dāng)著我的面吹牛余舶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播兆沙,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼欧芽,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了葛圃?” 一聲冷哼從身側(cè)響起千扔,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎库正,沒想到半個(gè)月后曲楚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡褥符,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年龙誊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喷楣。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡趟大,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铣焊,到底是詐尸還是另有隱情逊朽,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布曲伊,位于F島的核電站叽讳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜岛蚤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一邑狸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧涤妒,春花似錦单雾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至犁苏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扩所,已是汗流浹背围详。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祖屏,地道東北人助赞。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像袁勺,于是被迫代替她去往敵國和親雹食。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

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