Android主題更換換膚
文章目錄
知識總覽
認識setFactory
擷取任意一個apk壓縮檔案的
Resource
對象
1、如何建立自定義的Resource執行個體
2、如何知道目前屬性值在所在Resource中的id
參考文章
android主題換膚通常借助LayoutInflater#setFactory實作換膚。
換膚步驟:
通過解析外部的apk壓縮檔案,建立自定義的Resource對象去通路apk壓縮檔案的資源。
借助LayoutInfater#setFactoy,将步驟(1)中的資源應用到View的建立過程當中。
平常設定或者擷取一個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
擷取任意一個apk壓縮檔案的Resource對象
上述過程已經提供了更改View類型以及屬性的方式,下面我們見介紹如何擷取一個apk壓縮檔案中的res資源。
我們通常通過Context#getSource()擷取res目錄下的資源,Context#getAssets()(想當于Context#getSource().getAssets())擷取asset目錄下的資源。是以要擷取一個apk壓縮檔案的資源檔案,建立對應該壓縮檔案的Resource執行個體,然後通過這個執行個體擷取壓縮檔案中的資源資訊。
比如,新建立的的Resource執行個體為mResource,則可以使用mResource.getColor(colorId),來擷取執行個體内colorId所對應的顔色。
那麼接下來的問題分為兩步:
由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;
}
在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版權聲明:本文為部落客原創文章,轉載請附上博文連結!