舒適提示:閱讀本文須要60-70分鐘
微信公衆號:顧林海php
完成換膚須要解決兩個問題:java
如何獲取換膚的View,利用LayoutInflater內部接口Factory2提供的onCreateView方法獲取須要換膚的View,咱們從setContentView方法的具體做用來了解LayoutInflater.Factory2接口的做用,以具體源碼進行分析,MainActivity代碼以下:android
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方法:git
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
複製代碼
經過getDelegate()方法返回一個AppCompatDelegate對象,並調用AppCompatDelegate對象的setContentView方法。github
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
複製代碼
經過AppCompatDelegate的create方法建立AppCompatDelegate對象:web
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。app
繼續回到AppCompatActivity的setContentView方法:ide
@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地址請點擊這裏
關注公衆號: