近的項目中需要添加換膚機制。期望達到的效果:
研究了一下目前的開源換膚方案的實現,大概是通過以下幾種方式來實現:
下面說下整個機制的原理和過程。
所謂資源重定向,就是把系統默認的資源替換成我們想要更換的對應資源。這里就涉及到兩部分,默認資源和對應資源。
舉個例子,給TextView設置文字顏色android:textColor=”@color/white”,默認資源是R.color.white,重定向后,期望通過某種機制將R.color.white映射到R.color.black上,通過setTextColor(R.color.black)可修改TextView的文字顏色。
這個過程中,應該注意的對象有View(TextView)、屬性(textColor)、屬性值(@color/white)。換膚要實現的就是改變屬性值。
這一步為換膚做準備,記下所有感興趣的View及其屬性和屬性值。
我們從布局xml出發,找到一個記住View的時機。
Android布局xml中View的實例化過程可以參看我另一篇博文。
LayoutInflater完成了從layoutResID到View創建的過程。接下來祈禱能找到某個地方安插代碼吧,好不用自行解析xml和遍歷View。
LayoutInflater在遞歸inflate時,我們沒機會侵入代碼。直到LayoutInflater.createViewFromTag()。
Activtiy implements LayoutInflater.Factory2,會調用到Activity的onCreateView()
第4種選擇是系統默認實例化View的方式。到了這里,生米已成熟飯,記錄感興趣的View及其屬性就沒有希望了。
1和2可通過LayoutInflater.setFactory2()和LayoutInflater.setFactory(),實現自己的LayoutInflater Factory插入代碼。法3的mPrivateFactory可以通過Activity的onCreateView()自行初始化View。
從這里看,1、2、3沒有區別,demo里選取法2,截斷系統默認createView()的過程。
前面也說了,我們通過Activity.getLayoutInflater().setFactory()來更改原有factory,首先實現我們的factory,并實現onCreateView方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app.", "android.view." }; public class SkinLayoutInflaterFactory implements LayoutInflater.Factory { @Override public View onCreateView(String name, Context context, AttributeSet attrs) { // 實例化View。自行預處理前綴,調用LayoutInflater.createView()實例化 for (String prefix : sClassPrefixList) { try { View view = mLayoutInflater.createView(name, prefix, attrs); if (view != null) { // 記錄需要改變屬性的View及其屬性 addSkinViewIfNecessary(view, attrs); return view; } } catch (ClassNotFoundException e) { // In this case we want to let the base class take a crack // at it. } } return mLayoutInflater.createView(name, null, attrs); } } List<TextViewTextColorItem> textViewTextColorList = new ArrayList<>(); private void addSkinViewIfNecessary(View view, AttributeSet attrs) { if (view instanceof TextView) { int n = attrs.getAttributeCount(); for (int i = 0; i < n; i++) { String attrName = attrs.getAttributeName(i); if (attrName.equals("textColor")) { int id = 0; String attrValue = attrs.getAttributeValue(i); if (attrValue.startsWith("@")) { // 如"@2131427389" id = Integer.parseInt(attrValue.substring(1)); textViewTextColorList.add(new TextViewTextColorItem(view, id)); } } } } } |
對這里的onCreateView()實現有疑問,請繼續深入博文中LayoutInflater.createViewFromTag()部分。
遍歷記錄感興趣的View及其屬性的數據結構,通過重定向資源更新屬性值。為了減小項目修改,默認主題外的皮膚資源文件用命名后綴標識。
以替換TextView的textColor為例。
布局:
1 2 3 4 5 6 |
<TextView android:id="@+id/change_text_color" android:textColor="@color/textColor" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!"/>
|
更換主題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void reSkin(String suffix) { for (TextViewTextColorItem skinItem : textViewTextColorList) { skinItem.view.setTextColor(getColor(skinItem.id, suffix)); } } // 用老的資源id獲取新主題資源,需要一次華麗的轉身:id -> name -> new name -> new id private int getColor(int oldResId, String suffix) { String oldResName = mContext.getResources().getResourceEntryName(oldResId); String newResName = oldResName + suffix; int newResId = mContext.getResources().getIdentifier(newResName, "color", mContext .getPackageName()); return mContext.getResources().getColor(newResId); } |
示例說明:
生成的R.java中textColor resource id:
1 2 3 4 5 |
public static final class color { ... public static final int textColor=0x7f0b003d; public static final int textColor_night=0x7f0b003e; } |
oldResId = 2131427389,轉16進制為0x7f0b003d,即R.color.textColor
oldResName = textColor
newResName = textColor_night
newResId 2131427390,轉16進制為0x7f0b003e,即R.color.textColor_night
使用新resource id通過Resource獲取新色值,設置到view里,完成這個TextView的textColor屬性值更改。
對,這就是我們想要的!
以上理論分析中我們只添加了一種關注的View和屬性,接下來在換膚庫中,我們將添加以下View和屬性的支持,請持續關注項目eastmoneyandroid/Reskin。
- color
- drawable
- src
- View background(color/drawable)
- ListView divider(color/drawable)
- AbsListView listSelector(color/drawable)
- ImageView src(src)
- TextView textColor textHintColor drawableLeft
- ExpandableListView childDivider
- CheckBox button
- ProgressBar
- 自定義View
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。