亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android?換膚實(shí)現(xiàn)指南demo及案例解析

 更新時(shí)間:2023年06月01日 09:43:57   作者:xiangzhihong  
這篇文章主要為大家介紹了Android換膚指南demo及案例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

一、換膚方案

目前,市面上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 &lt; 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í)例

    獲取Android簽名證書的公鑰和私鑰的簡(jiǎn)單實(shí)例

    下面小編就為大家?guī)?lái)一篇獲取Android簽名證書的公鑰和私鑰的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-12-12
  • flutter實(shí)現(xiàn)倒計(jì)時(shí)加載頁(yè)面

    flutter實(shí)現(xiàn)倒計(jì)時(shí)加載頁(yè)面

    這篇文章主要為大家詳細(xì)介紹了flutter實(shí)現(xiàn)倒計(jì)時(shí)加載頁(yè)面,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • Android中使用Kotlin實(shí)現(xiàn)一個(gè)簡(jiǎn)單的登錄界面

    Android中使用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-09
  • Android開(kāi)發(fā)之線程通信詳解

    Android開(kāi)發(fā)之線程通信詳解

    這篇文章主要為大家詳細(xì)介紹了Android開(kāi)發(fā)中線程間通信的相關(guān)資料,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Android有一定的幫助,?需要的可以了解一下
    2022-11-11
  • Android自定義view實(shí)現(xiàn)電影票在線選座功能

    Android自定義view實(shí)現(xiàn)電影票在線選座功能

    這篇文章主要為大家詳細(xì)介紹了Android自定義view實(shí)現(xiàn)選座功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-11-11
  • Android Studio 當(dāng)build時(shí)候出錯(cuò)解決辦法

    Android Studio 當(dāng)build時(shí)候出錯(cuò)解決辦法

    這篇文章主要介紹了 Android Studio在build的時(shí)候出現(xiàn)transformClassesWithDexForDebug錯(cuò)誤解決辦法的相關(guān)資料,需要的朋友可以參考下
    2017-05-05
  • Android ExpandableListView使用方法案例詳解

    Android ExpandableListView使用方法案例詳解

    這篇文章主要介紹了Android ExpandableListView使用方法案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • Android 6.0開(kāi)發(fā)實(shí)現(xiàn)關(guān)機(jī)菜單添加重啟按鈕的方法

    Android 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-09
  • Android編程輸入事件流程詳解

    Android編程輸入事件流程詳解

    這篇文章主要介紹了Android編程輸入事件流程,較為詳細(xì)的分析了Android輸入事件原理、相關(guān)概念與具體操作流程,需要的朋友可以參考下
    2016-10-10
  • Android實(shí)現(xiàn)滑動(dòng)屏幕切換圖片

    Android實(shí)現(xiàn)滑動(dòng)屏幕切換圖片

    這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)滑動(dòng)屏幕切換圖片,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-08-08

最新評(píng)論