深入淺出換膚相關技術以及如何實現

溫馨提示:閱讀本文需要60-70分鐘
微信公眾號:顧林海
完成換膚需要解決兩個問題:

如何獲取換膚的View,利用LayoutInflater內部介面Factory2提供的onCreateView方法獲取需要換膚的View,我們從setContentView方法的具體作用來了解LayoutInflater.Factory2介面的作用,以具體原始碼進行分析,MainActivity程式碼如下:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } 複製程式碼
MainActivity繼承自AppCompatActivity,AppCompatActivity是Android Support Library包下的類,點選進入AppCompatActivity的setContentView方法:
@Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); } 複製程式碼
通過getDelegate()方法返回一個AppCompatDelegate物件,並呼叫AppCompatDelegate物件的setContentView方法。
@NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; } 複製程式碼
通過AppCompatDelegate的create方法建立AppCompatDelegate物件:
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) { return create(activity, activity.getWindow(), callback); } 複製程式碼
通過create方法返回AppCompatDelegate物件:
private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) { if (Build.VERSION.SDK_INT >= 24) { return new AppCompatDelegateImplN(context, window, callback); } else if (Build.VERSION.SDK_INT >= 23) { return new AppCompatDelegateImplV23(context, window, callback); } else { return new AppCompatDelegateImplV14(context, window, callback); } } 複製程式碼
AppCompatDelegate物件的建立是根據SDK的不同版本而建立的,其中AppCompatDelegateImplN、AppCompatDelegateImplV23以及AppCompatDelegateImplV14的繼承結構如下圖所示:

AppCompatDelegate是一個抽象類,AppCompatDelegateImplBase也是抽象類,主要對AppCompatDelegate功能的擴充套件,具體的實現類是AppCompatDelegateImplV9,以上根據SDK版本建立的類都繼承自AppCompatDelegateImplV9。
繼續回到AppCompatActivity的setContentView方法:
@Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); } 複製程式碼
獲取AppCompatDelegate物件後,通過該物件的setContentView方法設定ContentView,這個setContentView方法的具體呼叫是在AppCompatDelegateImplV9中,檢視原始碼如下:
//android.support.v7.app.AppCompatDelegateImplV9 @Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); //註釋1 LayoutInflater.from(mContext).inflate(resId, contentParent); mOriginalWindowCallback.onContentChanged(); } 複製程式碼
setContentView方法最核心的地方就是在註釋1處,通過LayoutInflater載入layout.xml檔案,contentParent是我們建立佈局後所要新增進去的一個容器,在建立Activity時會建立頂層檢視,也就是DecorView,DecorView其實是PhoneWindow中的一個內部類,它會載入相應的系統佈局。如下圖:

DecorView就是我們Activity顯示的全部檢視包括ActionBar,其中ContentView佈局是由我們來建立的,並通過LayoutInflater新增到ContentView中。
進入LayoutInflater的inflate方法中。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); } public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + Integer.toHexString(resource) + ")"); } final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } 複製程式碼
通過資源大管家,也就是Resources來載入layout檔案,最後通過inflate方法的一步步呼叫,會走到createViewFromTag方法,該方法內部會對每個標籤生成對應的View物件。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ... try { View view; if (mFactory2 != null) { //註釋1 view = mFactory2.onCreateView(parent, name, context, attrs); } ... if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } //註釋2 if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (Exception e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } } 複製程式碼
經過一些列呼叫進入註釋2處,通過mFactory2的onCreateView方法建立對應的View物件,mFactory2的賦值時機需要我們回到MainActivity程式碼中進行一步步檢視:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } 複製程式碼
進入AppCompatActivity的onCreate方法中:
protected void onCreate(@Nullable Bundle savedInstanceState) { final AppCompatDelegate delegate = getDelegate(); //註釋1 delegate.installViewFactory(); delegate.onCreate(savedInstanceState); ... super.onCreate(savedInstanceState); } 複製程式碼
註釋1處呼叫了delegate的installViewFactory方法,這個delegate物件是通過getDelegate()方法:
@NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; } 複製程式碼
這段程式碼應該很熟悉了吧,也就是說最終呼叫AppCompatDelegateImplV9的installViewFactory方法,檢視原始碼:
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase implements MenuBuilder.Callback, LayoutInflater.Factory2 { ... @Override public void installViewFactory() { LayoutInflater layoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { //註釋1 LayoutInflaterCompat.setFactory2(layoutInflater, this); } else { if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) { Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed" + " so we can not install AppCompat's"); } } } ... } 複製程式碼
AppCompatDelegateImplV9本身也實現了LayoutInflater.Factory2介面,在註釋1處呼叫LayoutInflaterCompat的setFactory2方法並傳入layoutInflater例項以及自身AppCompatDelegateImplV9物件。
進入LayoutInflaterCompat的setFactory2方法:
public void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) { //註釋1 inflater.setFactory2(factory); final LayoutInflater.Factory f = inflater.getFactory(); if (f instanceof LayoutInflater.Factory2) { forceSetFactory2(inflater, (LayoutInflater.Factory2) f); } else { // Else, we will force set the original wrapped Factory2 forceSetFactory2(inflater, factory); } } 複製程式碼
註釋1處將getDelegate()方法獲取到的AppCompatDelegate物件(具體實現類是AppCompatDelegateImplV9)通過inflater的setFactory2傳入進去。
進入LayoutInflater的setFactory2:
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); } } 複製程式碼
到這裡我們知道了LayoutInflater的成員變數mFactory2就是AppCompatDelegateImplV9物件(AppCompatDelegateImplV9實現LayoutInflater.Factory2介面)。
繼續回到createViewFromTag方法中:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ... try { View view; if (mFactory2 != null) { //註釋1 view = mFactory2.onCreateView(parent, name, context, attrs); } ... if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } //註釋2 if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (Exception e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } } 複製程式碼
註釋1處呼叫mFactory2的onCreateView方法,也就是呼叫AppCompatDelegateImplV9的onCreateView方法。
進入AppCompatDelegateImplV9的onCreateView方法:
@Override public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) { ... return createView(parent, name, context, attrs); } 複製程式碼
進入AppCompatDelegateImplV9的createView方法
@Override public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { ... return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */ true, /* Read read app:theme as a fallback at all times for legacy reasons */ VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */ ); } 複製程式碼
呼叫mAppCompatViewInflater的createView方法,繼續進入:
final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { final Context originalContext = context; if (inheritContext && parent != null) { context = parent.getContext(); } if (readAndroidTheme || readAppTheme) { // We then apply the theme on the context, if specified context = themifyContext(context, attrs, readAndroidTheme, readAppTheme); } if (wrapContext) { context = TintContextWrapper.wrap(context); } View view = null; // We need to 'inject' our tint aware Views in place of the standard framework versions switch (name) { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; case "ImageView": view = createImageView(context, attrs); verifyNotNull(view, name); break; case "Button": view = createButton(context, attrs); verifyNotNull(view, name); break; case "EditText": view = createEditText(context, attrs); verifyNotNull(view, name); break; case "Spinner": view = createSpinner(context, attrs); verifyNotNull(view, name); break; case "ImageButton": view = createImageButton(context, attrs); verifyNotNull(view, name); break; case "CheckBox": view = createCheckBox(context, attrs); verifyNotNull(view, name); break; case "RadioButton": view = createRadioButton(context, attrs); verifyNotNull(view, name); break; case "CheckedTextView": view = createCheckedTextView(context, attrs); verifyNotNull(view, name); break; case "AutoCompleteTextView": view = createAutoCompleteTextView(context, attrs); verifyNotNull(view, name); break; case "MultiAutoCompleteTextView": view = createMultiAutoCompleteTextView(context, attrs); verifyNotNull(view, name); break; case "RatingBar": view = createRatingBar(context, attrs); verifyNotNull(view, name); break; case "SeekBar": view = createSeekBar(context, attrs); verifyNotNull(view, name); break; default: // The fallback that allows extending class to take over view inflation // for other tags. Note that we don't check that the result is not-null. // That allows the custom inflater path to fall back on the default one // later in this method. view = createView(context, name, attrs); } if (view == null && originalContext != context) { // If the original context does not equal our themed context, then we need to manually // inflate it using the name so that android:theme takes effect. view = createViewFromTag(context, name, attrs); } if (view != null) { // If we have created a view, check its android:onClick checkOnClickListener(view, attrs); } return view; } 複製程式碼
整個呼叫流程圖如下:

mAppCompatViewInflater的createView方法主要通過switch/case形式對相應的標籤名字建立對應的View物件,比如TextView呼叫createTextView方法建立TextView物件。這裡有個問題,如果是自定義的View或是在這裡並沒有判斷的View的話,View就為null。
繼續回到createViewFromTag方法中:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ... try { View view; if (mFactory2 != null) { //註釋1 view = mFactory2.onCreateView(parent, name, context, attrs); } ... if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } //註釋2 if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { //註釋3 view = onCreateView(parent, name, attrs); } else { //註釋4 view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (Exception e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } } 複製程式碼
註釋1處在上面已經解析過了就是對layout檔案中的標籤型別建立對應的View物件,如果是自定義的View或是layout檔案中相應的View標籤在這裡並沒有判斷(畢竟系統不可能全部都判斷到),這時View就為null。進入註釋2處對View為null的情況進行處理。
註釋3處如果不是全限定名的類名呼叫onCreateView方法:
protected View onCreateView(View parent, String name, AttributeSet attrs) throws ClassNotFoundException { return onCreateView(name, attrs); } protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs); } 複製程式碼
如果不是全限定的類名,預設加上“android.view.”。
繼續往下追蹤:
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { // Have we seen this name before? Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); if (!allowed) { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } Object lastContext = mConstructorArgs[0]; if (mConstructorArgs[0] == null) { // Fill in the context if not already within inflation. mConstructorArgs[0] = mContext; } Object[] args = mConstructorArgs; args[1] = attrs; //註釋1 final View view = constructor.newInstance(args); if (view instanceof ViewStub) { // Use the same context when inflating ViewStub later. final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } mConstructorArgs[0] = lastContext; return view; } catch (NoSuchMethodException e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (ClassCastException e) { // If loaded class is not a View subclass final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (ClassNotFoundException e) { // If loadClass fails, we should propagate the exception. throw e; } catch (Exception e) { final InflateException ie = new InflateException( attrs.getPositionDescription() + ": Error inflating class " + (clazz == null ? "<unknown>" : clazz.getName()), e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } 複製程式碼
上面程式碼比較多,總結就是在註釋1處通過反射建立相應的View物件。
到這裡我們知道了Layout資原始檔的載入是通過LayoutInflater.Factory2的onCreateView方法實現的。也就是如果我們自己定義一個實現了LayoutInflater.Factory2介面的類並實現onCreateView方法,在該方法中儲存需要換膚的View,最後給換膚的View設定外掛中的資源。
載入外部資源可以通過反射建立AssetManager物件,反射呼叫AssetManager的addAssetPath方法載入外部資源,最後建立Resources物件並傳入剛建立的AssetManager物件,通過剛建立的Resources物件獲取相應的資源。
首先獲取需要換膚的View,怎麼知道哪些View需要換膚,可以通過自定義屬性來判斷,新建attr.xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="Skin"> <attr name="skinChange" format="boolean" /> </declare-styleable> </resources> 複製程式碼
skinChange用於判斷View是否需要進行換膚。編寫我們的佈局檔案:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:skinChange="true" android:background="@drawable/girl" android:orientation="vertical"> <Button android:id="@+id/btn_skin" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/text_color" app:skinChange="true" android:text="點選進行換膚" tools:ignore="MissingPrefix" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" app:skinChange="true" android:textSize="15sp" android:textColor="@color/text_color" android:text="這是一段文字,當點選進行換膚時,顏色會進行相應的變化" tools:ignore="MissingPrefix" /> <ImageView android:layout_width="100dp" android:layout_height="100dp" app:skinChange="true" android:src="@drawable/level" android:layout_marginTop="10dp" tools:ignore="MissingPrefix" /> </LinearLayout> 複製程式碼
新建SkinFactory類並實現自LayoutInflater.Factory2介面:
public class SkinFactory implements LayoutInflater.Factory2 { public class SkinFactory implements LayoutInflater.Factory2 { private AppCompatDelegate mDelegate; static final Class<?>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};// final Object[] mConstructorArgs = new Object[2]; private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>(); static final String[] prefix = new String[]{ "android.widget.", "android.view.", "android.webkit." }; public void setDelegate(AppCompatDelegate delegate) { this.mDelegate = delegate; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { View view = mDelegate.createView(parent, name, context, attrs); if (view == null) { mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { view = createViewByPrefix(context, name, prefix, attrs); } else { view = createViewByPrefix(context, name, null, attrs); } } catch (Exception e) { e.printStackTrace(); } } //儲存需要換膚的View SkinChange.getInstance().saveSkin(context, attrs, view); return view; } privateView createViewByPrefix(Context context, String name, String[] prefixs, AttributeSet attrs) { Constructor<? extends View> constructor = sConstructorMap.get(name); Class<? extends View> clazz = null; if (constructor == null) { try { if (prefixs != null && prefixs.length > 0) { for (String prefix : prefixs) { clazz = context.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); if (clazz != null) break; } } else { clazz = context.getClassLoader().loadClass(name).asSubclass(View.class); } if (clazz == null) { return null; } constructor = clazz.getConstructor(mConstructorSignature); } catch (Exception e) { e.printStackTrace(); return null; } constructor.setAccessible(true); //快取 sConstructorMap.put(name, constructor); } Object[] args = mConstructorArgs; args[1] = attrs; try { //通過反射建立View物件 return constructor.newInstance(args); } catch (Exception e) { e.printStackTrace(); } return null; } } 複製程式碼
Factory2的onCreateView的實現的邏輯與原始碼差不多,通過系統的AppCompatDelegate的createView方法建立View,如果建立的View為空,通過反射建立View物件,最主要的一步是SkinChange.getInstance().saveSkin方法,用於儲存換膚的View,具體程式碼如下,新建SkinChange類:
public class SkinChange { private SkinChange(){} public static SkinChange getInstance(){ return Holder.SKIN_CHANGE; } private static class Holder{ private static final SkinChange SKIN_CHANGE=new SkinChange(); } private List<SkinChange.Skin> mSkinListView = new ArrayList<>(); public List<SkinChange.Skin> getSkinViewList(){ return mSkinListView; } public void saveSkin(Context context, AttributeSet attrs, View view) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Skin); boolean skin = a.getBoolean(R.styleable.Skin_skinChange, false); if (skin) { final int Len = attrs.getAttributeCount(); HashMap<String, String> attrMap = new HashMap<>(); for (int i = 0; i < Len; i++) { String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); attrMap.put(attrName, attrValue); Log.d("saveSkin","attrName="+attrName+"attrValue="+attrValue); } SkinChange.Skin skinView = new SkinChange.Skin(); skinView.view = view; skinView.attrsMap = attrMap; mSkinListView.add(skinView); } } public static class Skin{ View view; HashMap<String, String> attrsMap; } } 複製程式碼
將屬性skinChange為true的View以及它的所有屬性儲存起來。
新建BaseActivity,實現onCreate方法,在setContentView方法之前替換LayoutInflater的成員變數mFactory2:
public abstract class BaseActivity extends AppCompatActivity { private SkinFactory mSkinFactory; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { if(null == mSkinFactory){ mSkinFactory=new SkinFactory(); } mSkinFactory.setDelegate(getDelegate()); LayoutInflater layoutInflater=LayoutInflater.from(this); layoutInflater.setFactory2(mSkinFactory); super.onCreate(savedInstanceState); } } 複製程式碼
執行效果如下:

從控制檯列印的資訊我們已經知道哪些View的屬性需要進行換膚,剩下的就是載入外部apk中的資源,建立LoadResources類:
public class LoadResources { private Resources mSkinResources; private Context mContext; private String mOutPkgName; public static LoadResources getInstance() { return Holder.LOAD_RESOURCES; } private LoadResources() { } private static class Holder{ private static final LoadResources LOAD_RESOURCES=new LoadResources(); } public void init(Context context) { mContext = context.getApplicationContext(); } public void load(final String path) { File file = new File(path); if (!file.exists()) { return; } PackageManager mPm = mContext.getPackageManager(); PackageInfo mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES); mOutPkgName = mInfo.packageName; AssetManager assetManager; try { assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, path); mSkinResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration()); } catch (Exception e) { e.printStackTrace(); } } public int getColor(int resId) { if (mSkinResources == null) { return resId; } String resName = mSkinResources.getResourceEntryName(resId); int outResId = mSkinResources.getIdentifier(resName, "color", mOutPkgName); if (outResId == 0) { return resId; } return mSkinResources.getColor(outResId); } public Drawable getDrawable(int resId) { if (mSkinResources == null) { return ContextCompat.getDrawable(mContext, resId); } String resName = mSkinResources.getResourceEntryName(resId); int outResId = mSkinResources.getIdentifier(resName, "drawable", mOutPkgName); if (outResId == 0) { return ContextCompat.getDrawable(mContext, resId); } return mSkinResources.getDrawable(outResId); } } 複製程式碼
LoadResources類非常簡單,通過反射建立AssetManager,並執行addAssetPath來載入外部apk,最後建立一個外部資源的Resources。
新建介面ISkinView用於約定換膚方法:
public interface ISkinView { void change(String path); } 複製程式碼
建立SkinChangeBiz並實現ISkinView介面:
public class SkinChangeBiz implements ISkinView { private static class Holder { private static final ISkinView SKIN_CHANGE_BIZ = new SkinChangeBiz(); } public static ISkinView getInstance() { return Holder.SKIN_CHANGE_BIZ; } @Override public void change(String path) { File skinFile = new File(Environment.getExternalStorageDirectory(), path); LoadResources.getInstance().load(skinFile.getAbsolutePath()); for (SkinChange.Skin skinView : SkinChange.getInstance().getSkinViewList()) { changeSkin(skinView); } } void changeSkin(SkinChange.Skin skinView) { if (!TextUtils.isEmpty(skinView.attrsMap.get("background"))) { int bgId = Integer.parseInt(skinView.attrsMap.get("background").substring(1)); String attrType = skinView.view.getResources().getResourceTypeName(bgId); if (TextUtils.equals(attrType, "drawable")) { skinView.view.setBackgroundDrawable(LoadResources.getInstance().getDrawable(bgId)); } else if (TextUtils.equals(attrType, "color")) { skinView.view.setBackgroundColor(LoadResources.getInstance().getColor(bgId)); } } if (skinView.view instanceof TextView) { if (!TextUtils.isEmpty(skinView.attrsMap.get("textColor"))) { int textColorId = Integer.parseInt(skinView.attrsMap.get("textColor").substring(1)); ((TextView) skinView.view).setTextColor(LoadResources.getInstance().getColor(textColorId)); } } } } 複製程式碼
SkinChangeBiz的change方法中先載入外部資源,再遍歷之前儲存的換膚View,對相關屬性進行設定。
前期工作已經準備好了,剩下的建立面板外掛,新建工程,新增需要換膚的資源,注意資源名必須與宿主的資源名一樣,面板外掛的sdk版本也必須保持一致,面板外掛工程就不貼出來了,比較簡單。
mBtnSkin.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //進行換膚 SkinChangeBiz.getInstance().change("skinPlugin.apk"); } }); 複製程式碼
執行效果如下:

github地址請點選 這裡