Android?換膚實(shí)現(xiàn)指南demo及案例解析
一、換膚方案
目前,市面上Android的換膚方案主要有Resource方案和AssetManager替換方案兩種方案。
其中,Resource方案是用戶提前自定義一些主題,然后將指定主題對(duì)應(yīng)的 id 設(shè)置成默認(rèn)的主題即可。而AssetManager替換方案,使用的是Hook系統(tǒng)AssetMananger對(duì)象,然后再編譯期靜態(tài)對(duì)齊資源文件對(duì)應(yīng)的id數(shù)值。
1.1 Resource方案
Resource方案的原理大概如下:
1、創(chuàng)建新的Resrouce對(duì)象(代理的Resource)
2、替換系統(tǒng)Resource對(duì)象
3、運(yùn)行時(shí)動(dòng)態(tài)映射(原理相同資源在不同的資源表中的Type和Name一樣)
4、xml布局解析攔截(xml布局中的資源不能通過(guò)代理Resource加載,LayoutInflater)
此方案的優(yōu)勢(shì)是支持String/Layout的替換,不過(guò)缺點(diǎn)也很明顯:
- 資源獲取效率有影響
- 不支持style、asset目錄
Resource多出替換,Resource包裝類代碼量大
1.2 AssetManager方案
使用的是Hook系統(tǒng)AssetMananger對(duì)象,然后再編譯期靜態(tài)對(duì)齊資源文件對(duì)應(yīng)的id數(shù)值,達(dá)到替換資源的目的。此種方案,最常見(jiàn)的就是Hook LayoutInflater進(jìn)行換膚。
二、Resource換膚
此種方式采用的方案是:用戶提前自定義一些主題,然后當(dāng)設(shè)置主題的時(shí)候?qū)⒅付ㄖ黝}對(duì)應(yīng)的 id 記錄到本地文件中,當(dāng) Activity RESUME 的時(shí)候,判斷 Activity 當(dāng)前的主題是否和之前設(shè)置的主題一致,不一致的話就調(diào)用當(dāng)前 Activity 的recreate()方法進(jìn)行重建。
比如,在這種方案中,我們可以通過(guò)如下的方式預(yù)定義一些屬性:
<?xml version="1.0" encoding="utf-8"?> <resources> ? ? <attr name="themed_divider_color" format="color"/> ? ? <attr name="themed_foreground" format="color"/> ? ? <!-- .... --> </resources>
然后,在自定義主題中使用為這些預(yù)定義屬性賦值。
<style name="Base.AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> ? ? <item name="themed_foreground">@color/warm_theme_foreground</item> ? ? <item name="themed_background">@color/warm_theme_background</item> ? ? <!-- ... --> </style>
最后,在布局文件中通過(guò)如下的方式引用這些自定義屬性。
<androidx.appcompat.widget.AppCompatTextView ? ? android:id="@+id/tv" ? ? android:textColor="?attr/themed_text_color_secondary" ? ? ... /> <View android:background="?attr/themed_divider_color" ? ? android:layout_width="match_parent" ? ? android:layout_height="1px"/>
三、Hook LayoutInflater方案
3.1 工作原理
通過(guò) Hook LayoutInflater 進(jìn)行換膚的方案是眾多開(kāi)源方案中比較常見(jiàn)的一種。在分析這種方案之前,我們最好先了解下 LayoutInflater 的工作原理。通常,當(dāng)我們想要自定義 Layout 的 Factory 的時(shí)候可以調(diào)用下面兩個(gè)方法將我們的 Factory 設(shè)置到系統(tǒng)的 LayoutInflater 中。
public abstract class LayoutInflater { ? ? public void setFactory(Factory factory) { ? ? ? ? if (mFactorySet) throw new IllegalStateException("A factory has already been set on this LayoutInflater"); ? ? ? ? if (factory == null) throw new NullPointerException("Given factory can not be null"); ? ? ? ? mFactorySet = true; ? ? ? ? if (mFactory == null) { ? ? ? ? ? ? mFactory = factory; ? ? ? ? } else { ? ? ? ? ? ? mFactory = new FactoryMerger(factory, null, mFactory, mFactory2); ? ? ? ? } ? ? } ? ? public void setFactory2(Factory2 factory) { ? ? ? ? if (mFactorySet) throw new IllegalStateException("A factory has already been set on this LayoutInflater"); ? ? ? ? if (factory == null) throw new NullPointerException("Given factory can not be null"); ? ? ? ? mFactorySet = true; ? ? ? ? if (mFactory == null) { ? ? ? ? ? ? mFactory = mFactory2 = factory; ? ? ? ? } else { ? ? ? ? ? ? mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2); ? ? ? ? } ? ? } }
當(dāng)我們調(diào)用 inflator()方法從 xml 中加載布局的時(shí)候,將會(huì)走到如下代碼真正執(zhí)行加載操作。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { ? ? synchronized (mConstructorArgs) { ? ? ? ? // .... ? ? ? ? final Context inflaterContext = mContext; ? ? ? ? final AttributeSet attrs = Xml.asAttributeSet(parser); ? ? ? ? Context lastContext = (Context) mConstructorArgs[0]; ? ? ? ? mConstructorArgs[0] = inflaterContext; ? ? ? ? View result = root; ? ? ? ? try { ? ? ? ? ? ? advanceToRootNode(parser); ? ? ? ? ? ? final String name = parser.getName(); ? ? ? ? ? ? // 處理 merge 標(biāo)簽 ? ? ? ? ? ? if (TAG_MERGE.equals(name)) { ? ? ? ? ? ? ? ? rInflate(parser, root, inflaterContext, attrs, false); ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? // 從 xml 中加載布局控件 ? ? ? ? ? ? ? ? final View temp = createViewFromTag(root, name, inflaterContext, attrs); ? ? ? ? ? ? ? ? // 生成布局參數(shù) LayoutParams ? ? ? ? ? ? ? ? ViewGroup.LayoutParams params = null; ? ? ? ? ? ? ? ? if (root != null) { ? ? ? ? ? ? ? ? ? ? params = root.generateLayoutParams(attrs); ? ? ? ? ? ? ? ? ? ? if (!attachToRoot) { ? ? ? ? ? ? ? ? ? ? ? ? temp.setLayoutParams(params); ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? // 加載子控件 ? ? ? ? ? ? ? ? rInflateChildren(parser, temp, attrs, true); ? ? ? ? ? ? ? ? // 添加到根控件 ? ? ? ? ? ? ? ? if (root != null && attachToRoot) { ? ? ? ? ? ? ? ? ? ? root.addView(temp, params); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? if (root == null || !attachToRoot) { ? ? ? ? ? ? ? ? ? ? result = temp; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } catch (XmlPullParserException e) {/*...*/} ? ? ? ? return result; ? ? } }
接下來(lái),我們看一下createViewFromTag()方法。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ? ? // 老的布局方式 ? ? if (name.equals("view")) { ? ? ? ? name = attrs.getAttributeValue(null, "class"); ? ? } ? ? // 處理 theme ? ? 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(); ? ? } ? ? try { ? ? ? ? View view = tryCreateView(parent, name, context, attrs); ? ? ? ? if (view == null) { ? ? ? ? ? ? final Object lastContext = mConstructorArgs[0]; ? ? ? ? ? ? mConstructorArgs[0] = context; ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? if (-1 == name.indexOf('.')) { ? ? ? ? ? ? ? ? ? ? view = onCreateView(context, parent, name, attrs); ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? view = createView(context, name, null, attrs); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } finally { ? ? ? ? ? ? ? ? mConstructorArgs[0] = lastContext; ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? return view; ? ? } catch (InflateException e) { ? ? ? ? // ... ? ? } } public final View tryCreateView(View parent, String name, Context context, AttributeSet attrs) { ? ? if (name.equals(TAG_1995)) { ? ? ? ? return new BlinkLayout(context, attrs); ? ? } ? ? // 優(yōu)先使用 mFactory2 創(chuàng)建 view,mFactory2 為空則使用 mFactory,否則使用 mPrivateFactory ? ? View view; ? ? if (mFactory2 != null) { ? ? ? ? view = mFactory2.onCreateView(parent, name, context, attrs); ? ? } else if (mFactory != null) { ? ? ? ? view = mFactory.onCreateView(name, context, attrs); ? ? } else { ? ? ? ? view = null; ? ? } ? ? if (view == null && mPrivateFactory != null) { ? ? ? ? view = mPrivateFactory.onCreateView(parent, name, context, attrs); ? ? } ? ? return view; }
可以看出,這里優(yōu)先使用 mFactory2 創(chuàng)建 view,mFactory2 為空則使用 mFactory,否則使用 mPrivateFactory 加載 view。所以,如果我們想要對(duì) view 創(chuàng)建過(guò)程進(jìn)行 hook,就應(yīng)該 hook 這里的 mFactory2,因?yàn)樗膬?yōu)先級(jí)最高。
注意到這里的 方法中并沒(méi)有循環(huán),所以,第一次的時(shí)候只能加載根布局。那么根布局內(nèi)的子控件是如何加載的呢?這就用到了inflaterInflateChildren()方法。
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, ? ? ? ? boolean finishInflate) throws XmlPullParserException, IOException { ? ? rInflate(parser, parent, parent.getContext(), attrs, finishInflate); } void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { ? ? final int depth = parser.getDepth(); ? ? int type; ? ? boolean pendingRequestFocus = false; ? ? while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { ? ? ? ? if (type != XmlPullParser.START_TAG) continue; ? ? ? ? final String name = parser.getName(); ? ? ? ? if (TAG_REQUEST_FOCUS.equals(name)) { ? ? ? ? ? ? // 處理 requestFocus 標(biāo)簽 ? ? ? ? ? ? pendingRequestFocus = true; ? ? ? ? ? ? consumeChildElements(parser); ? ? ? ? } else if (TAG_TAG.equals(name)) { ? ? ? ? ? ? // 處理 tag 標(biāo)簽 ? ? ? ? ? ? parseViewTag(parser, parent, attrs); ? ? ? ? } else if (TAG_INCLUDE.equals(name)) { ? ? ? ? ? ? // 處理 include 標(biāo)簽 ? ? ? ? ? ? if (parser.getDepth() == 0) { ? ? ? ? ? ? ? ? throw new InflateException("<include /> cannot be the root element"); ? ? ? ? ? ? } ? ? ? ? ? ? parseInclude(parser, context, parent, attrs); ? ? ? ? } else if (TAG_MERGE.equals(name)) { ? ? ? ? ? ? // 處理 merge 標(biāo)簽 ? ? ? ? ? ? throw new InflateException("<merge /> must be the root element"); ? ? ? ? } else { ? ? ? ? ? ? // 這里處理的是普通的 view 標(biāo)簽 ? ? ? ? ? ? final View view = createViewFromTag(parent, name, context, attrs); ? ? ? ? ? ? final ViewGroup viewGroup = (ViewGroup) parent; ? ? ? ? ? ? final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); ? ? ? ? ? ? // 繼續(xù)處理子控件 ? ? ? ? ? ? rInflateChildren(parser, view, attrs, true); ? ? ? ? ? ? viewGroup.addView(view, params); ? ? ? ? } ? ? } ? ? if (pendingRequestFocus) { ? ? ? ? parent.restoreDefaultFocus(); ? ? } ? ? if (finishInflate) { ? ? ? ? parent.onFinishInflate(); ? ? } }
注意到該方法內(nèi)部又調(diào)用了createViewFromTag和rInflateChildren方法,也就是說(shuō),這里通過(guò)遞歸的方式實(shí)現(xiàn)對(duì)整個(gè) view 樹的遍歷,從而將整個(gè) xml 加載為 view 樹。以上是安卓的 LayoutInflater 從 xml 中加載控件的邏輯,可以看出我們可以通過(guò) hook 實(shí)現(xiàn)對(duì)創(chuàng)建 view 的過(guò)程的“監(jiān)聽(tīng)”。
上面我們說(shuō)了下?lián)Q膚的原理,下面我們介紹幾種Android換膚的技術(shù)框架:Android-Skin-Loader、ThemeSkinning和Android-skin-support。
3.2 Android-Skin-Loader
3.2.1 使用流程
學(xué)習(xí)了 Hook LayoutInflator 的底層原理之后,我們來(lái)看幾個(gè)基于這種原理實(shí)現(xiàn)的換膚方案。首先是 Android-Skin-Loader 這個(gè)庫(kù),這個(gè)庫(kù)需要你覆寫Activity,然后再替換皮膚,Activity部分代碼如下。
public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{ ? ? private SkinInflaterFactory mSkinInflaterFactory; ? ? @Override ? ? protected void onCreate(Bundle savedInstanceState) { ? ? ? ? super.onCreate(savedInstanceState); ? ? ? ? mSkinInflaterFactory = new SkinInflaterFactory(); ? ? ? ? getLayoutInflater().setFactory(mSkinInflaterFactory); ? ? } ? ? // ... }
可以看出,這里將自定義的 Factory 設(shè)置給了LayoutInflator,SkinInflaterFactory的實(shí)現(xiàn)如下:
public class SkinInflaterFactory implements Factory { ? ? private static final boolean DEBUG = true; ? ? private List<SkinItem> mSkinItems = new ArrayList<SkinItem>(); ? ? @Override ? ? public View onCreateView(String name, Context context, AttributeSet attrs) { ? ? ? ? // 讀取自定義屬性 enable,這里用了自定義的 namespace ? ? ? ? boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false); ? ? ? ? if (!isSkinEnable){ ? ? ? ? ? ? return null; ? ? ? ? } ? ? ? ? // 創(chuàng)建 view ? ? ? ? View view = createView(context, name, attrs); ? ? ? ? if (view == null){ ? ? ? ? ? ? return null; ? ? ? ? } ? ? ? ? parseSkinAttr(context, attrs, view); ? ? ? ? return view; ? ? } ? ? private View createView(Context context, String name, AttributeSet attrs) { ? ? ? ? View view = null; ? ? ? ? try { ? ? ? ? ? ? // 兼容低版本創(chuàng)建 view 的邏輯(低版本是沒(méi)有完整包名) ? ? ? ? ? ? if (-1 == name.indexOf('.')){ ? ? ? ? ? ? ? ? if ("View".equals(name)) { ? ? ? ? ? ? ? ? ? ? view = LayoutInflater.from(context).createView(name, "android.view.", attrs); ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? if (view == null) { ? ? ? ? ? ? ? ? ? ? view = LayoutInflater.from(context).createView(name, "android.widget.", attrs); ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? if (view == null) { ? ? ? ? ? ? ? ? ? ? view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs); ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? // 新的創(chuàng)建 view 的邏輯 ? ? ? ? ? ? ? ? view = LayoutInflater.from(context).createView(name, null, attrs); ? ? ? ? ? ? } ? ? ? ? } catch (Exception e) {? ? ? ? ? ? ? view = null; ? ? ? ? } ? ? ? ? return view; ? ? } ? ? private void parseSkinAttr(Context context, AttributeSet attrs, View view) { ? ? ? ? List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>(); ? ? ? ? // 對(duì) xml 中控件的屬性進(jìn)行解析 ? ? ? ? for (int i = 0; i < attrs.getAttributeCount(); i++){ ? ? ? ? ? ? String attrName = attrs.getAttributeName(i); ? ? ? ? ? ? String attrValue = attrs.getAttributeValue(i); ? ? ? ? ? ? // 判斷屬性是否支持,屬性是預(yù)定義的 ? ? ? ? ? ? if(!AttrFactory.isSupportedAttr(attrName)){ ? ? ? ? ? ? ? ? continue; ? ? ? ? ? ? } ? ? ? ? ? ? // 如果是引用類型的屬性值 ? ? ? ? ? ? if(attrValue.startsWith("@")){ ? ? ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? ? ? int id = Integer.parseInt(attrValue.substring(1)); ? ? ? ? ? ? ? ? ? ? String entryName = context.getResources().getResourceEntryName(id); ? ? ? ? ? ? ? ? ? ? String typeName = context.getResources().getResourceTypeName(id); ? ? ? ? ? ? ? ? ? ? // 加入屬性列表 ? ? ? ? ? ? ? ? ? ? SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName); ? ? ? ? ? ? ? ? ? ? if (mSkinAttr != null) { ? ? ? ? ? ? ? ? ? ? ? ? viewAttrs.add(mSkinAttr); ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? } catch (NumberFormatException e) {/*...*/} ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? if(!ListUtils.isEmpty(viewAttrs)){ ? ? ? ? ? ? // 構(gòu)建該控件的屬性關(guān)系 ? ? ? ? ? ? SkinItem skinItem = new SkinItem(); ? ? ? ? ? ? skinItem.view = view; ? ? ? ? ? ? skinItem.attrs = viewAttrs; ? ? ? ? ? ? mSkinItems.add(skinItem); ? ? ? ? ? ? if(SkinManager.getInstance().isExternalSkin()){ ? ? ? ? ? ? ? ? skinItem.apply(); ? ? ? ? ? ? } ? ? ? ? } ? ? } }
這里自定義了一個(gè) xml 屬性,用來(lái)指定是否啟用換膚配置。然后在創(chuàng)建 view 的過(guò)程中解析 xml 中定義的 view 的屬性信息,比如,background 和 textColor 等屬性。并將其對(duì)應(yīng)的屬性、屬性值和控件以映射的形式記錄到緩存中。當(dāng)發(fā)生換膚的時(shí)候根據(jù)這里的映射關(guān)系在代碼中更新控件的屬性信息。
public class BackgroundAttr extends SkinAttr { ? ? @Override ? ? public void apply(View view) { ? ? ? ? if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){ ? ? ? ? ? ? // 注意這里獲取屬性值的時(shí)候是通過(guò) SkinManager 的方法獲取的 ? ? ? ? ? ? view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId)); ? ? ? ? }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){ ? ? ? ? ? ? Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId); ? ? ? ? ? ? view.setBackground(bg); ? ? ? ? } ? ? } }
如果是動(dòng)態(tài)添加的 view,比如在 java 代碼中,該庫(kù)提供了 等方法來(lái)動(dòng)態(tài)添加映射關(guān)系到緩存中。在 activity 的生命周期方法中注冊(cè)監(jiān)聽(tīng)換膚事件:
public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{ ? ? @Override ? ? protected void onResume() { ? ? ? ? super.onResume(); ? ? ? ? SkinManager.getInstance().attach(this); ? ? } ? ? @Override ? ? protected void onDestroy() { ? ? ? ? super.onDestroy(); ? ? ? ? SkinManager.getInstance().detach(this); ? ? ? ? // 清理緩存數(shù)據(jù) ? ? ? ? mSkinInflaterFactory.clean(); ? ? } ? ? @Override ? ? public void onThemeUpdate() { ? ? ? ? if(!isResponseOnSkinChanging){ ? ? ? ? ? ? return; ? ? ? ? } ? ? ? ? mSkinInflaterFactory.applySkin(); ? ? } ? ? // ...? }
當(dāng)換膚的時(shí)候會(huì)通知到 Activity 并觸發(fā)onThemeUpdate方法,接著調(diào)用 SkinInflaterFactory 的 apply 方法。SkinInflaterFactory 的 apply 方法中對(duì)緩存的屬性信息遍歷更新實(shí)現(xiàn)換膚。
3.2.2 皮膚包加載邏輯
接下來(lái),我們看一下皮膚包的加載邏輯,即通過(guò)自定義的 AssetManager 實(shí)現(xiàn),類似于插件化。
public void load(String skinPackagePath, final ILoaderListener callback) { ? ? new AsyncTask<String, Void, Resources>() { ? ? ? ? protected void onPreExecute() { ? ? ? ? ? ? if (callback != null) { ? ? ? ? ? ? ? ? callback.onStart(); ? ? ? ? ? ? } ? ? ? ? }; ? ? ? ? @Override ? ? ? ? protected Resources doInBackground(String... params) { ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? if (params.length == 1) { ? ? ? ? ? ? ? ? ? ? String skinPkgPath = params[0]; ? ? ? ? ? ? ? ? ? ? File file = new File(skinPkgPath);? ? ? ? ? ? ? ? ? ? ? if(file == null || !file.exists()){ ? ? ? ? ? ? ? ? ? ? ? ? return null; ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? PackageManager mPm = context.getPackageManager(); ? ? ? ? ? ? ? ? ? ? PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES); ? ? ? ? ? ? ? ? ? ? skinPackageName = mInfo.packageName; ? ? ? ? ? ? ? ? ? ? AssetManager assetManager = AssetManager.class.newInstance(); ? ? ? ? ? ? ? ? ? ? Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); ? ? ? ? ? ? ? ? ? ? addAssetPath.invoke(assetManager, skinPkgPath); ? ? ? ? ? ? ? ? ? ? Resources superRes = context.getResources(); ? ? ? ? ? ? ? ? ? ? Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration()); ? ? ? ? ? ? ? ? ? ? SkinConfig.saveSkinPath(context, skinPkgPath); ? ? ? ? ? ? ? ? ? ? skinPath = skinPkgPath; ? ? ? ? ? ? ? ? ? ? isDefaultSkin = false; ? ? ? ? ? ? ? ? ? ? return skinResource; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? return null; ? ? ? ? ? ? } catch (Exception e) { /*...*/ } ? ? ? ? }; ? ? ? ? protected void onPostExecute(Resources result) { ? ? ? ? ? ? mResources = result; ? ? ? ? ? ? if (mResources != null) { ? ? ? ? ? ? ? ? if (callback != null) callback.onSuccess(); ? ? ? ? ? ? ? ? notifySkinUpdate(); ? ? ? ? ? ? }else{ ? ? ? ? ? ? ? ? isDefaultSkin = true; ? ? ? ? ? ? ? ? if (callback != null) callback.onFailed(); ? ? ? ? ? ? } ? ? ? ? }; ? ? }.execute(skinPackagePath); }
然后,在獲取值的時(shí)候使用下面的方法:
public int getColor(int resId){ ? ? int originColor = context.getResources().getColor(resId); ? ? if(mResources == null || isDefaultSkin){ ? ? ? ? return originColor; ? ? } ? ? String resName = context.getResources().getResourceEntryName(resId); ? ? int trueResId = mResources.getIdentifier(resName, "color", skinPackageName); ? ? int trueColor = 0; ? ? try{ ? ? ? ? trueColor = mResources.getColor(trueResId); ? ? }catch(NotFoundException e){ ? ? ? ? e.printStackTrace(); ? ? ? ? trueColor = originColor; ? ? } ? ? return trueColor; }
3.2.3 方案特點(diǎn)
此種方案換膚,有如下的一些特點(diǎn):
- 換膚需要繼承自定義 activity
- 皮膚包和 APK 如果使用了資源混淆加載的時(shí)候就會(huì)出現(xiàn)問(wèn)題
- 沒(méi)處理屬性值通過(guò) 的形式引用的情況?attr
- 每個(gè)換膚的屬性需要自己注冊(cè)并實(shí)現(xiàn)
- 有些控件的一些屬性可能沒(méi)有提供對(duì)應(yīng)的 java 方法,因此在代碼中換膚就行不通
- 沒(méi)有處理使用 style 的情況
- 基于 實(shí)現(xiàn),版本太老android.app.Activity
在 inflator 創(chuàng)建 view 的時(shí)候,其實(shí)只做了對(duì)屬性的攔截處理操作,可以通過(guò)代理系統(tǒng)的 Factory 實(shí)現(xiàn)創(chuàng)建 view 的操作
3.3 ThemeSkinning
這個(gè)庫(kù)是基于上面的 Android-Skin-Loader 開(kāi)發(fā)的,在其基礎(chǔ)之上做了許多的調(diào)整,其地址是 ThemeSkinning。主要調(diào)整的內(nèi)容如下:
3.3.1 AppCompactActivity調(diào)整
該庫(kù)基于 AppCompactActivity 和LayoutInflaterCompat.setFactory開(kāi)發(fā),改動(dòng)的內(nèi)容如下:
public class SkinBaseActivity extends AppCompatActivity implements ISkinUpdate, IDynamicNewView { ? ? private SkinInflaterFactory mSkinInflaterFactory; ? ? private final static String TAG = "SkinBaseActivity"; ? ? @Override ? ? protected void onCreate(Bundle savedInstanceState) { ? ? ? ? mSkinInflaterFactory = new SkinInflaterFactory(this); ? ? ? ? LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinInflaterFactory); ? ? ? ? super.onCreate(savedInstanceState); ? ? ? ? changeStatusColor(); ? ? } ? ? // ... }
同時(shí),該庫(kù)也提供了修改狀態(tài)欄的方法,雖然能力比較有限。
3.3.2 SkinInflaterFactory調(diào)整
SkinInflaterFactory對(duì)創(chuàng)建View做了一些調(diào)整,代碼如下:
public class SkinInflaterFactory implements LayoutInflater.Factory2 { ? ? private Map<View, SkinItem> mSkinItemMap = new HashMap<>(); ? ? private AppCompatActivity mAppCompatActivity; ? ? public SkinInflaterFactory(AppCompatActivity appCompatActivity) { ? ? ? ? this.mAppCompatActivity = appCompatActivity; ? ? } ? ? @Override ? ? public View onCreateView(String s, Context context, AttributeSet attributeSet) { ? ? ? ? return null; ? ? } ? ? @Override ? ? public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { ? ? ? ? // 沿用之前的一些邏輯 ? ? ? ? boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false); ? ? ? ? AppCompatDelegate delegate = mAppCompatActivity.getDelegate(); ? ? ? ? View view = delegate.createView(parent, name, context, attrs); ? ? ? ? // 對(duì)字體兼容做了支持,這里是通過(guò)靜態(tài)方式將其緩存到內(nèi)存,動(dòng)態(tài)新增和移除,加載字體之后調(diào)用 textview 的 settypeface 方法替換 ? ? ? ? if (view instanceof TextView && SkinConfig.isCanChangeFont()) { ? ? ? ? ? ? TextViewRepository.add(mAppCompatActivity, (TextView) view); ? ? ? ? } ? ? ? ? if (isSkinEnable || SkinConfig.isGlobalSkinApply()) { ? ? ? ? ? ? if (view == null) { ? ? ? ? ? ? ? ? // 創(chuàng)建 view 的邏輯做了調(diào)整 ? ? ? ? ? ? ? ? view = ViewProducer.createViewFromTag(context, name, attrs); ? ? ? ? ? ? } ? ? ? ? ? ? if (view == null) { ? ? ? ? ? ? ? ? return null; ? ? ? ? ? ? } ? ? ? ? ? ? parseSkinAttr(context, attrs, view); ? ? ? ? } ? ? ? ? return view; ? ? } ? ? // ... }
以下是View的創(chuàng)建邏輯的相關(guān)代碼:
class ViewProducer { ? ? private static final Object[] mConstructorArgs = new Object[2]; ? ? private static final Map<String, Constructor<? extends View>> sConstructorMap = new ArrayMap<>(); ? ? private static final Class<?>[] sConstructorSignature = new Class[]{Context.class, AttributeSet.class}; ? ? private static final String[] sClassPrefixList = {"android.widget.", "android.view.", "android.webkit."}; ? ? static View createViewFromTag(Context context, String name, AttributeSet attrs) { ? ? ? ? if (name.equals("view")) { ? ? ? ? ? ? name = attrs.getAttributeValue(null, "class"); ? ? ? ? } ? ? ? ? try { ? ? ? ? ? ? // 構(gòu)造參數(shù),緩存,復(fù)用 ? ? ? ? ? ? mConstructorArgs[0] = context; ? ? ? ? ? ? mConstructorArgs[1] = attrs; ? ? ? ? ? ? if (-1 == name.indexOf('.')) { ? ? ? ? ? ? ? ? for (int i = 0; i < sClassPrefixList.length; i++) { ? ? ? ? ? ? ? ? ? ? final View view = createView(context, name, sClassPrefixList[i]); ? ? ? ? ? ? ? ? ? ? if (view != null) { ? ? ? ? ? ? ? ? ? ? ? ? return view; ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? return null; ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? // 通過(guò)構(gòu)造方法創(chuàng)建 view ? ? ? ? ? ? ? ? return createView(context, name, null); ? ? ? ? ? ? } ? ? ? ? } catch (Exception e) { ? ? ? ? ? ? return null; ? ? ? ? } finally { ? ? ? ? ? ? mConstructorArgs[0] = null; ? ? ? ? ? ? mConstructorArgs[1] = null; ? ? ? ? } ? ? } ? ? // ... }
3.3.3 對(duì)style的兼容處理
private void parseSkinAttr(Context context, AttributeSet attrs, View view) { ? ? List<SkinAttr> viewAttrs = new ArrayList<>(); ? ? for (int i = 0; i < attrs.getAttributeCount(); i++) { ? ? ? ? String attrName = attrs.getAttributeName(i); ? ? ? ? String attrValue = attrs.getAttributeValue(i); ? ? ? ? if ("style".equals(attrName)) { ? ? ? ? ? ? // 對(duì) style 的處理,從 theme 中獲取 TypedArray 然后獲取 resource id,再獲取對(duì)應(yīng)的信息 ? ? ? ? ? ? int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background}; ? ? ? ? ? ? TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0); ? ? ? ? ? ? int textColorId = a.getResourceId(0, -1); ? ? ? ? ? ? int backgroundId = a.getResourceId(1, -1); ? ? ? ? ? ? if (textColorId != -1) { ? ? ? ? ? ? ? ? String entryName = context.getResources().getResourceEntryName(textColorId); ? ? ? ? ? ? ? ? String typeName = context.getResources().getResourceTypeName(textColorId); ? ? ? ? ? ? ? ? SkinAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName); ? ? ? ? ? ? ? ? if (skinAttr != null) { ? ? ? ? ? ? ? ? ? ? viewAttrs.add(skinAttr); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? ? ? if (backgroundId != -1) { ? ? ? ? ? ? ? ? String entryName = context.getResources().getResourceEntryName(backgroundId); ? ? ? ? ? ? ? ? String typeName = context.getResources().getResourceTypeName(backgroundId); ? ? ? ? ? ? ? ? SkinAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName); ? ? ? ? ? ? ? ? if (skinAttr != null) { ? ? ? ? ? ? ? ? ? ? viewAttrs.add(skinAttr); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? ? ? a.recycle(); ? ? ? ? ? ? continue; ? ? ? ? } ? ? ? ? if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) { ? ? ? ? ? ? // 老邏輯 ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? //resource id ? ? ? ? ? ? ? ? int id = Integer.parseInt(attrValue.substring(1)); ? ? ? ? ? ? ? ? if (id == 0) continue; ? ? ? ? ? ? ? ? String entryName = context.getResources().getResourceEntryName(id); ? ? ? ? ? ? ? ? String typeName = context.getResources().getResourceTypeName(id); ? ? ? ? ? ? ? ? SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName); ? ? ? ? ? ? ? ? if (mSkinAttr != null) { ? ? ? ? ? ? ? ? ? ? viewAttrs.add(mSkinAttr); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } catch (NumberFormatException e) { /*...*/ } ? ? ? ? } ? ? } ? ? if (!SkinListUtils.isEmpty(viewAttrs)) { ? ? ? ? SkinItem skinItem = new SkinItem(); ? ? ? ? skinItem.view = view; ? ? ? ? skinItem.attrs = viewAttrs; ? ? ? ? mSkinItemMap.put(skinItem.view, skinItem); ? ? ? ? if (SkinManager.getInstance().isExternalSkin() || ? ? ? ? ? ? ? ? SkinManager.getInstance().isNightMode()) {//如果當(dāng)前皮膚來(lái)自于外部或者是處于夜間模式 ? ? ? ? ? ? skinItem.apply(); ? ? ? ? } ? ? } }
3.3.4 fragment 調(diào)整
在 Fragment 的生命周期方法結(jié)束的時(shí)候從緩存當(dāng)中移除指定的 View。
@Override public void onDestroyView() { ? ? removeAllView(getView()); ? ? super.onDestroyView(); } protected void removeAllView(View v) { ? ? if (v instanceof ViewGroup) { ? ? ? ? ViewGroup viewGroup = (ViewGroup) v; ? ? ? ? for (int i = 0; i < viewGroup.getChildCount(); i++) { ? ? ? ? ? ? removeAllView(viewGroup.getChildAt(i)); ? ? ? ? } ? ? ? ? removeViewInSkinInflaterFactory(v); ? ? } else { ? ? ? ? removeViewInSkinInflaterFactory(v); ? ? } }
這種方案相對(duì)第一個(gè)框架改進(jìn)了很多,但是此庫(kù)已經(jīng)有4,5年沒(méi)有維護(hù)了,組件和代碼都比較老。
3.4 Android-skin-support
接下來(lái),我們?cè)倏匆幌翧ndroid-skin-support 。主要修改的部分如下:
3.4.1 自動(dòng)注冊(cè) layoutinflator.factory
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks { ? ? private SkinActivityLifecycle(Application application) { ? ? ? ? application.registerActivityLifecycleCallbacks(this); ? ? ? ? installLayoutFactory(application); ? ? ? ? // 注冊(cè)監(jiān)聽(tīng) ? ? ? ? SkinCompatManager.getInstance().addObserver(getObserver(application)); ? ? } ? ? @Override ? ? public void onActivityCreated(Activity activity, Bundle savedInstanceState) { ? ? ? ? if (isContextSkinEnable(activity)) { ? ? ? ? ? ? installLayoutFactory(activity); ? ? ? ? ? ? // 更新 acitvity 的窗口的背景 ? ? ? ? ? ? updateWindowBackground(activity); ? ? ? ? ? ? // 觸發(fā)換膚...如果 view 沒(méi)有創(chuàng)建是不是就容易導(dǎo)致 NPE? ? ? ? ? ? ? if (activity instanceof SkinCompatSupportable) { ? ? ? ? ? ? ? ? ((SkinCompatSupportable) activity).applySkin(); ? ? ? ? ? ? } ? ? ? ? } ? ? } ? ? private void installLayoutFactory(Context context) { ? ? ? ? try { ? ? ? ? ? ? LayoutInflater layoutInflater = LayoutInflater.from(context); ? ? ? ? ? ? LayoutInflaterCompat.setFactory2(layoutInflater, getSkinDelegate(context)); ? ? ? ? } catch (Throwable e) { /* ... */ } ? ? } ? ? // 獲取 LayoutInflater.Factory2,這里加了一層緩存 ? ? private SkinCompatDelegate getSkinDelegate(Context context) { ? ? ? ? if (mSkinDelegateMap == null) { ? ? ? ? ? ? mSkinDelegateMap = new WeakHashMap<>(); ? ? ? ? } ? ? ? ? SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context); ? ? ? ? if (mSkinDelegate == null) { ? ? ? ? ? ? mSkinDelegate = SkinCompatDelegate.create(context); ? ? ? ? ? ? mSkinDelegateMap.put(context, mSkinDelegate); ? ? ? ? } ? ? ? ? return mSkinDelegate; ? ? } ? ? // ... }
LayoutInflaterCompat.setFactory2()方法源碼如下:
public final class LayoutInflaterCompat { ? ? public static void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) { ? ? ? ? inflater.setFactory2(factory); ? ? ? ? if (Build.VERSION.SDK_INT < 21) { ? ? ? ? ? ? final LayoutInflater.Factory f = inflater.getFactory(); ? ? ? ? ? ? if (f instanceof LayoutInflater.Factory2) { ? ? ? ? ? ? ? ? forceSetFactory2(inflater, (LayoutInflater.Factory2) f); ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? forceSetFactory2(inflater, factory); ? ? ? ? ? ? } ? ? ? ? } ? ? } ? ? // 通過(guò)反射的方式直接修改 mFactory2 字段 ? ? private static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) { ? ? ? ? if (!sCheckedField) { ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2"); ? ? ? ? ? ? ? ? sLayoutInflaterFactory2Field.setAccessible(true); ? ? ? ? ? ? } catch (NoSuchFieldException e) { /* ... */ } ? ? ? ? ? ? sCheckedField = true; ? ? ? ? } ? ? ? ? if (sLayoutInflaterFactory2Field != null) { ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? sLayoutInflaterFactory2Field.set(inflater, factory); ? ? ? ? ? ? } catch (IllegalAccessException e) { /* ... */ } ? ? ? ? } ? ? } ? ? // ... }
3.4.2 LayoutInflater.Factory2
public class SkinCompatDelegate implements LayoutInflater.Factory2 { ? ? @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.add(new WeakReference<>((SkinCompatSupportable) view)); ? ? ? ? } ? ? ? ? return view; ? ? } ? ? @Override ? ? public View onCreateView(String name, Context context, AttributeSet attrs) { ? ? ? ? View view = createView(null, name, context, attrs); ? ? ? ? if (view == null) return null; ? ? ? ? // 加入緩存,繼承這個(gè)接口的主要是 view 和 activity 這些 ? ? ? ? if (view instanceof SkinCompatSupportable) { ? ? ? ? ? ? mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view)); ? ? ? ? } ? ? ? ? return view; ? ? } ? ? public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { ? ? ? ? // view 生成邏輯被包裝成了 SkinCompatViewInflater ? ? ? ? if (mSkinCompatViewInflater == null) { ? ? ? ? ? ? mSkinCompatViewInflater = new SkinCompatViewInflater(); ? ? ? ? } ? ? ? ? List<SkinWrapper> wrapperList = SkinCompatManager.getInstance().getWrappers(); ? ? ? ? for (SkinWrapper wrapper : wrapperList) { ? ? ? ? ? ? Context wrappedContext = wrapper.wrapContext(mContext, parent, attrs); ? ? ? ? ? ? if (wrappedContext != null) { ? ? ? ? ? ? ? ? context = wrappedContext; ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? //? ? ? ? ? return mSkinCompatViewInflater.createView(parent, name, context, attrs); ? ? } ? ? // ... }
3.4.3 SkinCompatViewInflater
上述方法中 SkinCompatViewInflater 獲取 view 的邏輯如下。
public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { ? ? // 通過(guò) inflator 創(chuàng)建 view ? ? View view = createViewFromHackInflater(context, name, attrs); ? ? if (view == null) { ? ? ? ? view = createViewFromInflater(context, name, attrs); ? ? } ? ? // 根據(jù) view 標(biāo)簽創(chuàng)建 view ? ? if (view == null) { ? ? ? ? view = createViewFromTag(context, name, attrs); ? ? } ? ? // 處理 xml 中設(shè)置的點(diǎn)擊事件 ? ? if (view != null) { ? ? ? ? checkOnClickListener(view, attrs); ? ? } ? ? return 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; } private View createViewFromInflater(Context context, String name, AttributeSet attrs) { ? ? View view = null; ? ? for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) { ? ? ? ? view = inflater.createView(context, name, attrs); ? ? ? ? if (view == null) { ? ? ? ? ? ? continue; ? ? ? ? } else { ? ? ? ? ? ? break; ? ? ? ? } ? ? } ? ? return view; } public View createViewFromTag(Context context, String name, AttributeSet attrs) { ? ? // <view class="xxxx"> 形式的 tag,和 <xxxx> 一樣 ? ? if ("view".equals(name)) { ? ? ? ? name = attrs.getAttributeValue(null, "class"); ? ? } ? ? try { ? ? ? ? // 構(gòu)造參數(shù)緩存 ? ? ? ? mConstructorArgs[0] = context; ? ? ? ? mConstructorArgs[1] = attrs; ? ? ? ? if (-1 == name.indexOf('.')) { ? ? ? ? ? ? for (int i = 0; i < sClassPrefixList.length; i++) { ? ? ? ? ? ? ? ? // 通過(guò)構(gòu)造方法創(chuàng)建 view ? ? ? ? ? ? ? ? final View view = createView(context, name, sClassPrefixList[i]); ? ? ? ? ? ? ? ? if (view != null) { ? ? ? ? ? ? ? ? ? ? return view; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? ? ? return null; ? ? ? ? } else { ? ? ? ? ? ? return createView(context, name, null); ? ? ? ? } ? ? } catch (Exception e) { ? ? ? ? return null; ? ? } finally { ? ? ? ? mConstructorArgs[0] = null; ? ? ? ? mConstructorArgs[1] = null; ? ? } } 這里用來(lái)創(chuàng)建視圖 的充氣器 是通過(guò) 獲取的。這樣設(shè)計(jì)的目的在于暴露接口給調(diào)用者,用來(lái)自定義控件的充氣器 邏輯。比如,針對(duì)三方控件和自定義控件的邏輯等。SkinCompatManager.getInstance().getInflaters() 該庫(kù)自帶的一個(gè)實(shí)現(xiàn)是, public class SkinAppCompatViewInflater implements SkinLayoutInflater, SkinWrapper { ? ?@Override ? ? public View createView(Context context, String name, AttributeSet attrs) { ? ? ? ? View view = createViewFromFV(context, name, attrs); ? ? ? ? if (view == null) { ? ? ? ? ? ? view = createViewFromV7(context, name, attrs); ? ? ? ? } ? ? ? ? return view; ? ? } ? ? 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; ? ? ? ? ? ? // ... 其他控件的實(shí)現(xiàn)邏輯 ? ? ? ? } ? ? } ? ? // ... }
四、其他方案
除了上面介紹的方案外,還有如下的一些方案:
4.1 TG換膚方案
TG 的換膚只支持夜間和日間主題之間的切換,所以,相對(duì)上面幾種方案 TG 的換膚就簡(jiǎn)單得多。
在閱讀 TG 的代碼的時(shí)候,我也 TG 在做頁(yè)面布局的時(shí)候做了一件很瘋狂的事情——他們沒(méi)有使用任何 xml 布局,所有布局都是通過(guò) java 代碼實(shí)現(xiàn)的。
為了支持對(duì)主題的自定義 TG 把項(xiàng)目?jī)?nèi)幾乎所有的顏色分別定義了一個(gè)名稱,對(duì)以文本形式記錄到一個(gè)文件中,數(shù)量非常多,然后將其放到 assets 下面,應(yīng)用內(nèi)通過(guò)讀取這個(gè)資源文件來(lái)獲取各個(gè)控件的顏色。
4.2 自定義控件 + 全局廣播實(shí)現(xiàn)換膚
這種方案根前面 hook LayoutInflator 的自動(dòng)替換視圖 的方案差不多。不過(guò),這種方案不需要做 hook,而是對(duì)應(yīng)用的內(nèi)常用的控件全部做一邊自定義。自定義控件內(nèi)部監(jiān)聽(tīng)換膚的事件。
當(dāng)自定義控件接收到換膚事件的時(shí)候,自定義控件內(nèi)部觸發(fā)換膚邏輯。不過(guò)這種換膚的方案相對(duì)于上述通過(guò) hook LayoutInflator 的方案而言,可控性更好一些。
以上就是Android 換膚指南的詳細(xì)內(nèi)容,更多關(guān)于Android 換膚指南的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
獲取Android簽名證書的公鑰和私鑰的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇獲取Android簽名證書的公鑰和私鑰的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12flutter實(shí)現(xiàn)倒計(jì)時(shí)加載頁(yè)面
這篇文章主要為大家詳細(xì)介紹了flutter實(shí)現(xiàn)倒計(jì)時(shí)加載頁(yè)面,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Android中使用Kotlin實(shí)現(xiàn)一個(gè)簡(jiǎn)單的登錄界面
Kotlin 是一種在 Java 虛擬機(jī)上運(yùn)行的靜態(tài)類型編程語(yǔ)言,被稱之為 Android 世界的Swift,由 JetBrains 設(shè)計(jì)開(kāi)發(fā)并開(kāi)源。接下來(lái)本文通過(guò)實(shí)例代碼給大家講解Android中使用Kotlin實(shí)現(xiàn)一個(gè)簡(jiǎn)單的登錄界面,一起看看吧2017-09-09Android自定義view實(shí)現(xiàn)電影票在線選座功能
這篇文章主要為大家詳細(xì)介紹了Android自定義view實(shí)現(xiàn)選座功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android Studio 當(dāng)build時(shí)候出錯(cuò)解決辦法
這篇文章主要介紹了 Android Studio在build的時(shí)候出現(xiàn)transformClassesWithDexForDebug錯(cuò)誤解決辦法的相關(guān)資料,需要的朋友可以參考下2017-05-05Android ExpandableListView使用方法案例詳解
這篇文章主要介紹了Android ExpandableListView使用方法案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Android 6.0開(kāi)發(fā)實(shí)現(xiàn)關(guān)機(jī)菜單添加重啟按鈕的方法
這篇文章主要介紹了Android 6.0開(kāi)發(fā)實(shí)現(xiàn)關(guān)機(jī)菜單添加重啟按鈕的方法,涉及Android6.0針對(duì)相關(guān)源碼的修改與功能添加操作技巧,需要的朋友可以參考下2017-09-09Android實(shí)現(xiàn)滑動(dòng)屏幕切換圖片
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)滑動(dòng)屏幕切換圖片,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08