温馨提示:阅读本文需要60-70分钟
微信公众号:顾林海

完成换肤需要解决两个问题:

如何获取换肤的View,利用LayoutInflater内部接口Factory2提供的onCreateView方法获取需要换肤的View,我们从setContentView方法的具体作用来了解LayoutInflater.Factory2接口的作用,以具体源码进行分析,MainActivity代码如下:

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}
}

MainActivity继承自AppCompatActivity,AppCompatActivity是Android Support Library包下的类,点击进入AppCompatActivity的setContentView方法:

    @Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}

通过getDelegate()方法返回一个AppCompatDelegate对象,并调用AppCompatDelegate对象的setContentView方法。

    @NonNullpublic 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方法:

    @Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}

获取AppCompatDelegate对象后,通过该对象的setContentView方法设置ContentView,这个setContentView方法的具体调用是在AppCompatDelegateImplV9中,查看源码如下:

//android.support.v7.app.AppCompatDelegateImplV9@Overridepublic void setContentView(int resId) {ensureSubDecor();ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();//注释1LayoutInflater.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) {//注释1view = mFactory2.onCreateView(parent, name, context, attrs);} ...if (view == null && mPrivateFactory != null) {view = mPrivateFactory.onCreateView(parent, name, context, attrs);}//注释2if (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 {@Overrideprotected 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();//注释1delegate.installViewFactory();delegate.onCreate(savedInstanceState);...super.onCreate(savedInstanceState);}

注释1处调用了delegate的installViewFactory方法,这个delegate对象是通过getDelegate()方法:

    @NonNullpublic AppCompatDelegate getDelegate() {if (mDelegate == null) {mDelegate = AppCompatDelegate.create(this, this);}return mDelegate;}

这段代码应该很熟悉了吧,也就是说最终调用AppCompatDelegateImplV9的installViewFactory方法,查看源码:

class AppCompatDelegateImplV9 extends AppCompatDelegateImplBaseimplements MenuBuilder.Callback, LayoutInflater.Factory2 {...@Overridepublic void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(mContext);if (layoutInflater.getFactory() == null) {//注释1LayoutInflaterCompat.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) {//注释1inflater.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 Factory2forceSetFactory2(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) {//注释1view = mFactory2.onCreateView(parent, name, context, attrs);} ...if (view == null && mPrivateFactory != null) {view = mPrivateFactory.onCreateView(parent, name, context, attrs);}//注释2if (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方法:

    @Overridepublic final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {...return createView(parent, name, context, attrs);}

进入AppCompatDelegateImplV9的createView方法

    @Overridepublic 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 specifiedcontext = 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 versionsswitch (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:onClickcheckOnClickListener(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) {//注释1view = mFactory2.onCreateView(parent, name, context, attrs);} ...if (view == null && mPrivateFactory != null) {view = mPrivateFactory.onCreateView(parent, name, context, attrs);}//注释2if (view == null) {final Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = context;try {if (-1 == name.indexOf('.')) {//注释3view = onCreateView(parent, name, attrs);} else {//注释4view = 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 itclazz = 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 constructorif (mFilter != null) {// Have we seen this name before?Boolean allowedState = mFilterMap.get(name);if (allowedState == null) {// New class -- remember whether it is allowedclazz = 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;//注释1final 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 subclassfinal 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"><Buttonandroid: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" /><TextViewandroid: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" /><ImageViewandroid: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;}@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}@Overridepublic 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();}}//保存需要换肤的ViewSkinChange.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;@Overrideprotected 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;}@Overridepublic 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() {@Overridepublic void onClick(View v) {//进行换肤SkinChangeBiz.getInstance().change("skinPlugin.apk");}});

运行效果如下:

github地址请点击这里

深入浅出换肤相关技术以及如何实现相关推荐

  1. 前端vue项目一键换肤主题技术方案

    一.技术核心 通过切换 css 选择器的方式实现主题样式的切换. 在组件中保留不变的样式,将需要变化的样式进行抽离 提供多种样式,给不同的主题定义一个对应的 CSS 选择器 根据不同主题通过切换CSS ...

  2. opengl源码 实现无缝切换图片过场_手把手讲解 Android hook技术实现一键换肤

    前言 产品大佬又提需求啦,要求app里面的图表要实现白天黑夜模式的切换,以满足不同光线下都能保证足够的图表清晰度. 怎么办?可能解决的办法很多,你可以给图表view增加一个toggle方法,参数Str ...

  3. Android换肤技术

    所谓换肤技术,就是用户可以根据自己的喜好,选择自己喜欢的并且APP提供的颜色,背景图片,作为整个app的主题背景颜色,或者字体颜色等等...满足用户的需求. APP换肤 主要分为2种: 1.内置换肤: ...

  4. wegame一键蹲替换文件_手把手讲解 Android hook技术实现一键换肤

    前言 产品大佬又提需求啦,要求app里面的图表要实现白天黑夜模式的切换,以满足不同光线下都能保证足够的图表清晰度. 怎么办?可能解决的办法很多,你可以给图表view增加一个toggle方法,参数Str ...

  5. 网易云--手机QQ的换肤是怎么做到的,你对换肤有了解吗?看过换肤的原理没?

    面试官: 网易云QQ的换肤是怎么做到的,你对换肤有了解吗?看过换肤的原理没? 心理分析:没有接触过换肤技术 第一次听到该名词肯定会很茫然.面试官考的是对资源加载,监听布局,有没有了解.本文从换肤实战一 ...

  6. 反思|开启B站少女心模式,探究APP换肤机制的设计与实现

    反思 系列博客是我的一种新学习方式的尝试,该系列起源和目录请参考 这里 . 概述 换肤功能 并非奇技淫巧,而是已非常普及,尤其当Android Q推出了 深色模式 之后,国内绝大多数主流应用都至少提供 ...

  7. 探究APP换肤机制的设计与实现

    /   今日科技快讯   / 近日,谷歌母公司Alphabet公布了截至6月30日的2021年第二季度财报.报告显示,Alphabet第二季度总营收为618.80亿美元,较去年同期的382.97亿美元 ...

  8. Android主题换肤 无缝切换

    作者 _SOLID 关注 2016.04.17 22:04* 字数 4291 阅读 23224评论 123喜欢 679 今天再给大家带来一篇干货. Android的主题换肤 ,可插件化提供皮肤包,无需 ...

  9. Android主题换肤 无缝切换 你值得拥有

    链接:https://www.jianshu.com/p/af7c0585dd5b 天再给大家带来一篇干货. Android的主题换肤 ,可插件化提供皮肤包,无需Activity的重启直接实现无缝切换 ...

最新文章

  1. tkinter学习系列之(五)Checkbutton控件
  2. 循环链表的插入和删除
  3. 2.1 name_scope 简单入门(一)
  4. java reader 方法_Java Reader reset()方法
  5. Spring mvc介绍
  6. linux 模拟打电话,Android 调用打电话和发短信功能
  7. Servlet教程第6讲笔记
  8. 非常好用的jdk帮助文档jdk1.8中文谷歌翻译
  9. Android 热修复原理
  10. java html加密_能提供加密与解密
  11. 三调数据库标注插件v1.3
  12. result_of 用法
  13. 网络显示dns服务器错误,电脑出现网络dns异常是怎么回事
  14. 聊天室群聊以及私聊功能的实现
  15. 迅雷链:DPoA 与 VRF
  16. vue工程,高德地图信息窗体模块化插入,及信息窗口点击事件
  17. java算法int型整数反转的另类解法
  18. (算法设计与分析)第二章递归与分治策略-第二节:分治和典型分治问题
  19. php html转换成word,php如何实现html转换word?_后端开发
  20. Redis分布式部署

热门文章

  1. python链表的创建_《大话数据结构》配套源码:链表(Python版)
  2. 财务python招聘_会计、财务、HR等重复性质岗位学习python有什么帮助?
  3. Linux入门——一些linux基础
  4. CentOS 7.X 安装 Gitlab 笔记
  5. 创新时代的管理:《创新赢天下》
  6. HotSpotOverview.pdf
  7. 【Unity/Kinect】获取预制的手势信息KinectInterop.HandState
  8. LoadRunner解决超时错误
  9. Mask_RCNN训练自己的模型(练习)
  10. BZOJ.3004.[SDOI2012]吊灯(结论)