Android?換膚實現(xiàn)指南demo及案例解析
一、換膚方案
目前,市面上Android的換膚方案主要有Resource方案和AssetManager替換方案兩種方案。
其中,Resource方案是用戶提前自定義一些主題,然后將指定主題對應的 id 設置成默認的主題即可。而AssetManager替換方案,使用的是Hook系統(tǒng)AssetMananger對象,然后再編譯期靜態(tài)對齊資源文件對應的id數值。
1.1 Resource方案
Resource方案的原理大概如下:
1、創(chuàng)建新的Resrouce對象(代理的Resource)
2、替換系統(tǒng)Resource對象
3、運行時動態(tài)映射(原理相同資源在不同的資源表中的Type和Name一樣)
4、xml布局解析攔截(xml布局中的資源不能通過代理Resource加載,LayoutInflater)
此方案的優(yōu)勢是支持String/Layout的替換,不過缺點也很明顯:
- 資源獲取效率有影響
- 不支持style、asset目錄
Resource多出替換,Resource包裝類代碼量大
1.2 AssetManager方案
使用的是Hook系統(tǒng)AssetMananger對象,然后再編譯期靜態(tài)對齊資源文件對應的id數值,達到替換資源的目的。此種方案,最常見的就是Hook LayoutInflater進行換膚。
二、Resource換膚
此種方式采用的方案是:用戶提前自定義一些主題,然后當設置主題的時候將指定主題對應的 id 記錄到本地文件中,當 Activity RESUME 的時候,判斷 Activity 當前的主題是否和之前設置的主題一致,不一致的話就調用當前 Activity 的recreate()方法進行重建。
比如,在這種方案中,我們可以通過如下的方式預定義一些屬性:
<?xml version="1.0" encoding="utf-8"?> <resources> ? ? <attr name="themed_divider_color" format="color"/> ? ? <attr name="themed_foreground" format="color"/> ? ? <!-- .... --> </resources>
然后,在自定義主題中使用為這些預定義屬性賦值。
<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>
最后,在布局文件中通過如下的方式引用這些自定義屬性。
<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 工作原理
通過 Hook LayoutInflater 進行換膚的方案是眾多開源方案中比較常見的一種。在分析這種方案之前,我們最好先了解下 LayoutInflater 的工作原理。通常,當我們想要自定義 Layout 的 Factory 的時候可以調用下面兩個方法將我們的 Factory 設置到系統(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); ? ? ? ? } ? ? } }
當我們調用 inflator()方法從 xml 中加載布局的時候,將會走到如下代碼真正執(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 標簽 ? ? ? ? ? ? if (TAG_MERGE.equals(name)) { ? ? ? ? ? ? ? ? rInflate(parser, root, inflaterContext, attrs, false); ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? // 從 xml 中加載布局控件 ? ? ? ? ? ? ? ? final View temp = createViewFromTag(root, name, inflaterContext, attrs); ? ? ? ? ? ? ? ? // 生成布局參數 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; ? ? } }
接下來,我們看一下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。所以,如果我們想要對 view 創(chuàng)建過程進行 hook,就應該 hook 這里的 mFactory2,因為它的優(yōu)先級最高。
注意到這里的 方法中并沒有循環(huán),所以,第一次的時候只能加載根布局。那么根布局內的子控件是如何加載的呢?這就用到了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 標簽 ? ? ? ? ? ? pendingRequestFocus = true; ? ? ? ? ? ? consumeChildElements(parser); ? ? ? ? } else if (TAG_TAG.equals(name)) { ? ? ? ? ? ? // 處理 tag 標簽 ? ? ? ? ? ? parseViewTag(parser, parent, attrs); ? ? ? ? } else if (TAG_INCLUDE.equals(name)) { ? ? ? ? ? ? // 處理 include 標簽 ? ? ? ? ? ? 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 標簽 ? ? ? ? ? ? throw new InflateException("<merge /> must be the root element"); ? ? ? ? } else { ? ? ? ? ? ? // 這里處理的是普通的 view 標簽 ? ? ? ? ? ? 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(); ? ? } }
注意到該方法內部又調用了createViewFromTag和rInflateChildren方法,也就是說,這里通過遞歸的方式實現(xiàn)對整個 view 樹的遍歷,從而將整個 xml 加載為 view 樹。以上是安卓的 LayoutInflater 從 xml 中加載控件的邏輯,可以看出我們可以通過 hook 實現(xiàn)對創(chuàng)建 view 的過程的“監(jiān)聽”。
上面我們說了下?lián)Q膚的原理,下面我們介紹幾種Android換膚的技術框架:Android-Skin-Loader、ThemeSkinning和Android-skin-support。
3.2 Android-Skin-Loader
3.2.1 使用流程
學習了 Hook LayoutInflator 的底層原理之后,我們來看幾個基于這種原理實現(xiàn)的換膚方案。首先是 Android-Skin-Loader 這個庫,這個庫需要你覆寫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 設置給了LayoutInflator,SkinInflaterFactory的實現(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 的邏輯(低版本是沒有完整包名) ? ? ? ? ? ? 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>(); ? ? ? ? // 對 xml 中控件的屬性進行解析 ? ? ? ? for (int i = 0; i < attrs.getAttributeCount(); i++){ ? ? ? ? ? ? String attrName = attrs.getAttributeName(i); ? ? ? ? ? ? String attrValue = attrs.getAttributeValue(i); ? ? ? ? ? ? // 判斷屬性是否支持,屬性是預定義的 ? ? ? ? ? ? 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)){ ? ? ? ? ? ? // 構建該控件的屬性關系 ? ? ? ? ? ? SkinItem skinItem = new SkinItem(); ? ? ? ? ? ? skinItem.view = view; ? ? ? ? ? ? skinItem.attrs = viewAttrs; ? ? ? ? ? ? mSkinItems.add(skinItem); ? ? ? ? ? ? if(SkinManager.getInstance().isExternalSkin()){ ? ? ? ? ? ? ? ? skinItem.apply(); ? ? ? ? ? ? } ? ? ? ? } ? ? } }
這里自定義了一個 xml 屬性,用來指定是否啟用換膚配置。然后在創(chuàng)建 view 的過程中解析 xml 中定義的 view 的屬性信息,比如,background 和 textColor 等屬性。并將其對應的屬性、屬性值和控件以映射的形式記錄到緩存中。當發(fā)生換膚的時候根據這里的映射關系在代碼中更新控件的屬性信息。
public class BackgroundAttr extends SkinAttr { ? ? @Override ? ? public void apply(View view) { ? ? ? ? if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){ ? ? ? ? ? ? // 注意這里獲取屬性值的時候是通過 SkinManager 的方法獲取的 ? ? ? ? ? ? view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId)); ? ? ? ? }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){ ? ? ? ? ? ? Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId); ? ? ? ? ? ? view.setBackground(bg); ? ? ? ? } ? ? } }
如果是動態(tài)添加的 view,比如在 java 代碼中,該庫提供了 等方法來動態(tài)添加映射關系到緩存中。在 activity 的生命周期方法中注冊監(jiān)聽換膚事件:
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); ? ? ? ? // 清理緩存數據 ? ? ? ? mSkinInflaterFactory.clean(); ? ? } ? ? @Override ? ? public void onThemeUpdate() { ? ? ? ? if(!isResponseOnSkinChanging){ ? ? ? ? ? ? return; ? ? ? ? } ? ? ? ? mSkinInflaterFactory.applySkin(); ? ? } ? ? // ...? }
當換膚的時候會通知到 Activity 并觸發(fā)onThemeUpdate方法,接著調用 SkinInflaterFactory 的 apply 方法。SkinInflaterFactory 的 apply 方法中對緩存的屬性信息遍歷更新實現(xiàn)換膚。
3.2.2 皮膚包加載邏輯
接下來,我們看一下皮膚包的加載邏輯,即通過自定義的 AssetManager 實現(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); }
然后,在獲取值的時候使用下面的方法:
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 方案特點
此種方案換膚,有如下的一些特點:
- 換膚需要繼承自定義 activity
- 皮膚包和 APK 如果使用了資源混淆加載的時候就會出現(xiàn)問題
- 沒處理屬性值通過 的形式引用的情況?attr
- 每個換膚的屬性需要自己注冊并實現(xiàn)
- 有些控件的一些屬性可能沒有提供對應的 java 方法,因此在代碼中換膚就行不通
- 沒有處理使用 style 的情況
- 基于 實現(xiàn),版本太老android.app.Activity
在 inflator 創(chuàng)建 view 的時候,其實只做了對屬性的攔截處理操作,可以通過代理系統(tǒng)的 Factory 實現(xiàn)創(chuàng)建 view 的操作
3.3 ThemeSkinning
這個庫是基于上面的 Android-Skin-Loader 開發(fā)的,在其基礎之上做了許多的調整,其地址是 ThemeSkinning。主要調整的內容如下:
3.3.1 AppCompactActivity調整
該庫基于 AppCompactActivity 和LayoutInflaterCompat.setFactory開發(fā),改動的內容如下:
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(); ? ? } ? ? // ... }
同時,該庫也提供了修改狀態(tài)欄的方法,雖然能力比較有限。
3.3.2 SkinInflaterFactory調整
SkinInflaterFactory對創(chuàng)建View做了一些調整,代碼如下:
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); ? ? ? ? // 對字體兼容做了支持,這里是通過靜態(tài)方式將其緩存到內存,動態(tài)新增和移除,加載字體之后調用 textview 的 settypeface 方法替換 ? ? ? ? if (view instanceof TextView && SkinConfig.isCanChangeFont()) { ? ? ? ? ? ? TextViewRepository.add(mAppCompatActivity, (TextView) view); ? ? ? ? } ? ? ? ? if (isSkinEnable || SkinConfig.isGlobalSkinApply()) { ? ? ? ? ? ? if (view == null) { ? ? ? ? ? ? ? ? // 創(chuàng)建 view 的邏輯做了調整 ? ? ? ? ? ? ? ? view = ViewProducer.createViewFromTag(context, name, attrs); ? ? ? ? ? ? } ? ? ? ? ? ? if (view == null) { ? ? ? ? ? ? ? ? return null; ? ? ? ? ? ? } ? ? ? ? ? ? parseSkinAttr(context, attrs, view); ? ? ? ? } ? ? ? ? return view; ? ? } ? ? // ... }
以下是View的創(chuàng)建邏輯的相關代碼:
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 { ? ? ? ? ? ? // 構造參數,緩存,復用 ? ? ? ? ? ? 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 { ? ? ? ? ? ? ? ? // 通過構造方法創(chuàng)建 view ? ? ? ? ? ? ? ? return createView(context, name, null); ? ? ? ? ? ? } ? ? ? ? } catch (Exception e) { ? ? ? ? ? ? return null; ? ? ? ? } finally { ? ? ? ? ? ? mConstructorArgs[0] = null; ? ? ? ? ? ? mConstructorArgs[1] = null; ? ? ? ? } ? ? } ? ? // ... }
3.3.3 對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)) { ? ? ? ? ? ? // 對 style 的處理,從 theme 中獲取 TypedArray 然后獲取 resource id,再獲取對應的信息 ? ? ? ? ? ? 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()) {//如果當前皮膚來自于外部或者是處于夜間模式 ? ? ? ? ? ? skinItem.apply(); ? ? ? ? } ? ? } }
3.3.4 fragment 調整
在 Fragment 的生命周期方法結束的時候從緩存當中移除指定的 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); ? ? } }
這種方案相對第一個框架改進了很多,但是此庫已經有4,5年沒有維護了,組件和代碼都比較老。
3.4 Android-skin-support
接下來,我們再看一下Android-skin-support 。主要修改的部分如下:
3.4.1 自動注冊 layoutinflator.factory
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks { ? ? private SkinActivityLifecycle(Application application) { ? ? ? ? application.registerActivityLifecycleCallbacks(this); ? ? ? ? installLayoutFactory(application); ? ? ? ? // 注冊監(jiān)聽 ? ? ? ? SkinCompatManager.getInstance().addObserver(getObserver(application)); ? ? } ? ? @Override ? ? public void onActivityCreated(Activity activity, Bundle savedInstanceState) { ? ? ? ? if (isContextSkinEnable(activity)) { ? ? ? ? ? ? installLayoutFactory(activity); ? ? ? ? ? ? // 更新 acitvity 的窗口的背景 ? ? ? ? ? ? updateWindowBackground(activity); ? ? ? ? ? ? // 觸發(fā)換膚...如果 view 沒有創(chuàng)建是不是就容易導致 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); ? ? ? ? ? ? } ? ? ? ? } ? ? } ? ? // 通過反射的方式直接修改 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; ? ? ? ? // 加入緩存,繼承這個接口的主要是 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) { ? ? // 通過 inflator 創(chuàng)建 view ? ? View view = createViewFromHackInflater(context, name, attrs); ? ? if (view == null) { ? ? ? ? view = createViewFromInflater(context, name, attrs); ? ? } ? ? // 根據 view 標簽創(chuàng)建 view ? ? if (view == null) { ? ? ? ? view = createViewFromTag(context, name, attrs); ? ? } ? ? // 處理 xml 中設置的點擊事件 ? ? 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 { ? ? ? ? // 構造參數緩存 ? ? ? ? mConstructorArgs[0] = context; ? ? ? ? mConstructorArgs[1] = attrs; ? ? ? ? if (-1 == name.indexOf('.')) { ? ? ? ? ? ? for (int i = 0; i < sClassPrefixList.length; i++) { ? ? ? ? ? ? ? ? // 通過構造方法創(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; ? ? } } 這里用來創(chuàng)建視圖 的充氣器 是通過 獲取的。這樣設計的目的在于暴露接口給調用者,用來自定義控件的充氣器 邏輯。比如,針對三方控件和自定義控件的邏輯等。SkinCompatManager.getInstance().getInflaters() 該庫自帶的一個實現(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; ? ? ? ? ? ? // ... 其他控件的實現(xiàn)邏輯 ? ? ? ? } ? ? } ? ? // ... }
四、其他方案
除了上面介紹的方案外,還有如下的一些方案:
4.1 TG換膚方案
TG 的換膚只支持夜間和日間主題之間的切換,所以,相對上面幾種方案 TG 的換膚就簡單得多。
在閱讀 TG 的代碼的時候,我也 TG 在做頁面布局的時候做了一件很瘋狂的事情——他們沒有使用任何 xml 布局,所有布局都是通過 java 代碼實現(xiàn)的。
為了支持對主題的自定義 TG 把項目內幾乎所有的顏色分別定義了一個名稱,對以文本形式記錄到一個文件中,數量非常多,然后將其放到 assets 下面,應用內通過讀取這個資源文件來獲取各個控件的顏色。
4.2 自定義控件 + 全局廣播實現(xiàn)換膚
這種方案根前面 hook LayoutInflator 的自動替換視圖 的方案差不多。不過,這種方案不需要做 hook,而是對應用的內常用的控件全部做一邊自定義。自定義控件內部監(jiān)聽換膚的事件。
當自定義控件接收到換膚事件的時候,自定義控件內部觸發(fā)換膚邏輯。不過這種換膚的方案相對于上述通過 hook LayoutInflator 的方案而言,可控性更好一些。
以上就是Android 換膚指南的詳細內容,更多關于Android 換膚指南的資料請關注腳本之家其它相關文章!
相關文章
Android中使用Kotlin實現(xiàn)一個簡單的登錄界面
Kotlin 是一種在 Java 虛擬機上運行的靜態(tài)類型編程語言,被稱之為 Android 世界的Swift,由 JetBrains 設計開發(fā)并開源。接下來本文通過實例代碼給大家講解Android中使用Kotlin實現(xiàn)一個簡單的登錄界面,一起看看吧2017-09-09Android自定義view實現(xiàn)電影票在線選座功能
這篇文章主要為大家詳細介紹了Android自定義view實現(xiàn)選座功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11Android ExpandableListView使用方法案例詳解
這篇文章主要介紹了Android ExpandableListView使用方法案例詳解,本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下2021-08-08Android 6.0開發(fā)實現(xiàn)關機菜單添加重啟按鈕的方法
這篇文章主要介紹了Android 6.0開發(fā)實現(xiàn)關機菜單添加重啟按鈕的方法,涉及Android6.0針對相關源碼的修改與功能添加操作技巧,需要的朋友可以參考下2017-09-09