Android主題更換換膚

Android主題更換換膚
文章目錄
知識總覽
認識setFactory
獲取任意一個apk壓縮文件的Resource對象
1、如何創建自定義的Resource實例
2、如何知道當前屬性值在所在Resource中的id
參考文章
知識總覽
android主題換膚通常藉助LayoutInflater#setFactory實現換膚。

換膚步驟:

通過解析外部的apk壓縮文件,創建自定義的Resource對象去訪問apk壓縮文件的資源。
藉助LayoutInfater#setFactoy,將步驟(1)中的資源應用到View的創建過程當中。
認識setFactory
平常設置或者獲取一個View時,用的較多的是setContentView或LayoutInflater#inflate,setContentView內部也是通過調用LayoutInflater#inflate實現(具體調用在AppCompatViewInflater#setContentView(ind resId)中)。

通過LayoutInflater#inflate可以將xml佈局文件解析爲所需要的View,通過分析LayoutInflate#inflate源碼,可以看到.xml佈局文件在解析的過程中會調用LayoutInflater#rInflate,隨後會通過調用LayoutInflater#createViewFromTag來創建View。這裏推薦《遇見LayoutInflater&Factory》
下面一起看看View的創建過程LayoutInflate#createViewFormTag:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,

        boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    // Apply a theme wrapper, if allowed and one is specified.
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }

    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    try {
        View view;
        if (mFactory2 != null) {
            //根據attrs信息,通過mFactory2創建View
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            //根據attrs信息,通過mFactory創建View
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                    //創建Android原生的View(android.view包下面的view)
                    view = onCreateView(parent, name, attrs);
                } else {
                    //創建自定義View或者依賴包中的View(xml中聲明的是全路徑)
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    } catch (InflateException e) {
        throw e;

    } catch (ClassNotFoundException e) {
        final InflateException ie = new InflateException(attrs.getPositionDescription()
                + ": Error inflating class " + name, e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;

    } catch (Exception e) {
        final InflateException ie = new InflateException(attrs.getPositionDescription()
                + ": Error inflating class " + name, e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;
    }
}

從上述源碼中可以看出View的創建過程中,會首先找Factory2#onCreateView和Factory#onCreateView進行創建,然後走默認的創建流程。所以,我們可以在此處創建自定義的Factory2或Factory,並將自定義的Factory2或Factory對象添加到LayoutInflater對象當中,來對View的創建進行干預,LayoutInflate也提供了相關的API供我們添加自己的ViewFactory。

例如:下面我們通過設置LayoutInflater的Factory來,將視圖中的Button轉換爲TextView

@Override

protected void onCreate(@Nullable Bundle savedInstanceState) {
    LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() {
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            for (int i = 0; i < attrs.getAttributeCount(); i ++){
                String attrName = attrs.getAttributeName(i);
                String attrValue = attrs.getAttributeValue(i);
                Log.i(TAG, String.format("name = %s, attrName = %s, attrValue= %s", name, attrName, attrValue));
            }
            TextView textView = null;
            if (name.equals("Button")){
                 textView = new TextView(context, attrs);
            }

            return textView;
        }
    });
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_theme_change);
}

讓後啓動Activity後,視圖中的Button都轉化成了TextView,並且能看到輸出:

name = Button, attrName = id, attrValue= @2131230758
name = Button, attrName = background, attrValue= @2131034152
name = Button, attrName = layout_width, attrValue= -2
name = Button, attrName = layout_height, attrValue= -2
name = Button, attrName = id, attrValue= @2131230757
name = Button, attrName = background, attrValue= @2131034150
name = Button, attrName = layout_width, attrValue= -2
name = Button, attrName = layout_height, attrValue= -2

獲取任意一個apk壓縮文件的Resource對象
上述過程已經提供了更改View類型以及屬性的方式,下面我們見介紹如何獲取一個apk壓縮文件中的res資源。

我們通常通過Context#getSource()獲取res目錄下的資源,Context#getAssets()(想當於Context#getSource().getAssets())獲取asset目錄下的資源。所以要獲取一個apk壓縮文件的資源文件,創建對應該壓縮文件的Resource實例,然後通過這個實例獲取壓縮文件中的資源信息。
比如,新創建的的Resource實例爲mResource,則可以使用mResource.getColor(colorId),來獲取實例內colorId所對應的顏色。

那麼接下來的問題分爲兩步:

1、如何創建自定義的Resource實例
由Resource的構造函數Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)瞭解到,需要獲取app外部apk文件資源的Resource對象,首先需要創建對應的AssetManager對象。

public final class AssetManager implements AutoCloseable {

/**
 * Create a new AssetManager containing only the basic system assets.
 * Applications will not generally use this method, instead retrieving the
 * appropriate asset manager with {@link Resources#getAssets}.    Not for
 * use by applications.
 * {@hide}
 */
public AssetManager() {
    synchronized (this) {
        if (DEBUG_REFS) {
            mNumRefs = 0;
            incRefsLocked(this.hashCode());
        }
        init(false);
        if (localLOGV) Log.v(TAG, "New asset manager: " + this);
        ensureSystemAssets();
    }
}
 /**
 * 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}
 */
 //添加額外的asset路徑
public final int addAssetPath(String path) {
    synchronized (this) {
        int res = addAssetPathNative(path);
        if (mStringBlocks != null) {
            makeStringBlocks(mStringBlocks);
        }
        return res;
    }
}

所以通過反射可以創建對應的AssertManager,進而創建出對應的Resource實例,代碼如下:

private final static Resources loadTheme(String skinPackageName, Context context){

    String skinPackagePath = Environment.getExternalStorageDirectory() + "/" + skinPackageName;
    File file = new File(skinPackagePath);
    Resources skinResource = null;
    if (!file.exists()) {
        return skinResource;
    }
    try {
        //創建AssetManager實例
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, skinPackagePath);
        //構建皮膚資源Resource實例
        Resources superRes = context.getResources();
        skinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());

    } catch (Exception e) {
        skinResource = null;
    }
    return skinResource;
}

2、如何知道當前屬性值在所在Resource中的id
在Resource的源碼中,可以發現

public class Resources {

/**
 * 通過給的資源名稱,類型和包名返回一個資源的標識id。
 * @param name 資源的描述名稱
 * @param defType 資源的類型名稱
 * @param defPackage 包名
 * 
 * @return 返回資源id,0標識未找到該資源
 */
public int getIdentifier(String name, String defType, String defPackage) {
    if (name == null) {
        throw new NullPointerException("name is null");
    }
    try {
        return Integer.parseInt(name);
    } catch (Exception e) {
        // Ignore
    }
    return mAssets.getResourceIdentifier(name, defType, defPackage);
}

}

也就是說在任意的apk文件中,只需要知道包名(manifest.xml中指定的包名,用於尋找資源和Java類)、資源類型名稱、資源描述名稱。
比如:在包A中有一個defType爲"color",name爲color_red_1的屬性,通過Resource#getIdentifier則可以獲取包B中該名稱的顏色資源。

//將skina重View的背景色設置爲com.example.skinb中所對應的顏色
if (attrValue.startsWith("@") && attrName.contains("background")){

int resId = Integer.parseInt(attrValue.substring(1));
int originColor = mContext.getResources().getColor(resId);
    if (mResource == null){
        return originColor;
    }
    String resName = mContext.getResources().getResourceEntryName(resId);
    int skinRealResId = mResource.getIdentifier(resName, "color", "com.example.skinb");
    int skinColor = 0;
    try{
        skinColor = mResource.getColor(skinRealResId);
    }catch (Exception e){
        Log.e(TAG, "", e);
        skinColor = originColor;
    }
    view.setBackgroundColor(skinColor);

}

上述方法也是換膚框架Android-Skin-Loader的基本思路。

參考文章
遇見LayoutInflater&Factory
Android 探究 LayoutInflater setFactory
Android換膚原理和Android-Skin-Loader框架解析

Android中插件開發篇之----應用換膚原理解析

作者:d袋鼠b
來源:CSDN
原文:https://blog.csdn.net/weixin_36570478/article/details/91464020
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章