温馨提示: 阅读本文需要 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, Windows Windows,
- AppCompatCallback callback) {
- if (Build.VERSION.SDK_INT>= 24) {
- return new AppCompatDelegateImplN(context, Windows, callback);
- } else if (Build.VERSION.SDK_INT>= 23) {
- return new AppCompatDelegateImplV23(context, Windows, callback);
- } else {
- return new AppCompatDelegateImplV14(context, Windows, 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 地址请点击这里 https://github.com/LinhaiGu/SkinProject
来源: https://juejin.im/post/5c6d20f4f265da2da00eaa44