溫馨提示:閱讀本文需要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;
}
private View 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地址請點選這裡