关于换肤技能,信任同伴们都见过一些大型app,到了某些节日或许活动,e.g. 双十一、双十二、新年等等,app的ICON还有内部的页面主题布景都被换成对应的皮肤,像这种换肤必定不是为了某个活动独自发一个版本,这样的话就太鸡肋了,许多大厂都有自己的换肤技能,不需求经过发版就能够实时换肤,活动结束之后主动下掉,所以有哪些资源能够经过换肤来进行切换的呢?

其实在Android的res目录下一切资源都能够进行换肤,像图片、文字色彩、字体、布景等都能够经过换肤来进行无卡顿切换,那么究竟怎么才干高效稳定地完结换肤,咱们需求关于View的生命周期以及加载流程有必定的知道。

1 XML布局的解析流程

假如没有使用Compose,咱们现阶段的Android开发布局依然是在XML文件中,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:background="#2196F3"
        android:text="这是顶部TextView"
        android:gravity="center"
        android:textColor="#FFFFFF"
        app:layout_behavior=".behavior.ScrollBehavior"/>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_child"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        app:layout_behavior=".behavior.RecyclerViewBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

所以假如想要改动字体色彩,就需求动态修正textColor特点;假如想改动布景,就需求修正background特点;当一个Activity想要加载某个布局文件的时分,就需求调用setContentView办法,实例化View;

setContentView(R.layout.activity_main)

那么咱们是否能够改动体系加载布局文件的逻辑,让其加载咱们自己的皮肤包,那这样是不是就能够完结动态换肤?

1.1 setContentView源码剖析

我这边看的是Android 11的源码,算是比较新的了吧,同伴们能够跟着看一下。

@Override
public void setContentView(@LayoutRes int layoutResID) {
    initViewTreeOwners();
    getDelegate().setContentView(layoutResID);
}

一般情况下,咱们传入的便是一个布局id,内部完结是调用了AppCompatDelegate完结类的setContentView办法,AppCompatDelegate是一个抽象类,它的完结类为AppCompatDelegateImpl。

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

所以咱们看下AppCompatDelegateImpl的setContentView办法。

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.bypassOnContentChanged(mWindow.getCallback());
}

首要调用了ensureSubDecor办法,这儿我就不细说了,这个办法的目的便是确保DecorView创立成功,咱们看下这个图,布局的层级是这样的。

Android进阶宝典 -- 使用Hook技术拦截系统实例化View过程实现App换肤功能

咱们一切的自定义布局,都是加载在DecorView这个容器上,咱们看下面这个布局:

<androidx.appcompat.widget.ActionBarOverlayLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/decor_content_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">
    <!--布局id为 action_bar_activity_content---->
    <include layout="@layout/abc_screen_content_include"/>
    <androidx.appcompat.widget.ActionBarContainer
            android:id="@+id/action_bar_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            style="?attr/actionBarStyle"
            android:touchscreenBlocksFocus="true"
            android:gravity="top">
        <androidx.appcompat.widget.Toolbar
                android:id="@+id/action_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:navigationContentDescription="@string/abc_action_bar_up_description"
                style="?attr/toolbarStyle"/>
        <androidx.appcompat.widget.ActionBarContextView
                android:id="@+id/action_context_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:visibility="gone"
                android:theme="?attr/actionModeTheme"
                style="?attr/actionModeStyle"/>
    </androidx.appcompat.widget.ActionBarContainer>
</androidx.appcompat.widget.ActionBarOverlayLayout>

看布局你或许会觉得,这个是啥?这个是体系appcompat包中的一个布局文件,姓名为adb_screen_toolbar.xml,当咱们新建一个app项目的时分,见到的第一个页面,如下图所示

Android进阶宝典 -- 使用Hook技术拦截系统实例化View过程实现App换肤功能

红框展现的布局便是上面这个XML,也便是DecorView加载的布局文件R.layout.adb_screen_toolbar.xml

final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
        R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
    // There might be Views already added to the Window's content view so we need to
    // migrate them to our content view
    while (windowContentView.getChildCount() > 0) {
        final View child = windowContentView.getChildAt(0);
        windowContentView.removeViewAt(0);
        contentView.addView(child);
    }
    // Change our content FrameLayout to use the android.R.id.content id.
    // Useful for fragments.
    windowContentView.setId(View.NO_ID);
    contentView.setId(android.R.id.content);
    // The decorContent may have a foreground drawable set (windowContentOverlay).
    // Remove this as we handle it ourselves
    if (windowContentView instanceof FrameLayout) {
        ((FrameLayout) windowContentView).setForeground(null);
    }
}

关于DecorView的加载,由于设置不同主题就会加载不同的XML,这儿我不做过多的解说,由于首要目标是换肤,可是上面这段代码需求关注一下,便是DecorView布局加载出来之后,获取了include中的id为action_bar_activity_content的容器,将其id替换成了content

咱们再回到setContentView办法中,咱们看又是经过mSubDecor获取到了content这个id对应的容器,经过Inflate的办法将咱们的布局加载到这个容器傍边,所以中心点便是Inflate是怎么加载并实例化View的

1.2 LayoutInflater源码剖析

咱们换肤的要点便是关于LayoutInflater源码的剖析,尤其是inflate办法,直接回来了一个View。

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) + ")");
    }
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    // 这儿是进行XML布局解析
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

首要是经过XmlParser工具进行布局解析,这部分就不讲了没有意义,要点看下面的代码完结:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;
        try {
            advanceToRootNode(parser);
            // 代码 - 1
            final String name = parser.getName();
            // ...... 省略部分代码
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                // 代码 - 2
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                ViewGroup.LayoutParams params = null;
                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }
                if (DEBUG) {
                    System.out.println("-----> start inflating children");
                }
                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);
                if (DEBUG) {
                    System.out.println("-----> done inflating children");
                }
                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        }
        return result;
    }
}

同伴们从代码中标记的tag,自行找对应的代码解说

代码 – 1

前面咱们经过XML布局解析,拿到了布局文件中的信息,这个name其实便是咱们在XML中写的控件的名称,例如TextView、Button、LinearLayout、include、merge……

假如是merge标签的话,跟其他控件走的渲染办法不一样,咱们要点看 代码-2 中的完结。

代码 – 2

这儿有一个中心办法,createViewFromTag,终究回来了一个View,这儿就包含体系创立并实例化View的隐秘

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }
    // Apply a theme wrapper, if allowed and one is specified.
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }
    try {
        // 代码 - 3
        View view = tryCreateView(parent, name, context, attrs);
        // 代码 - 4
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(context, parent, name, attrs);
                } else {
                    view = createView(context, name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
        return view;
    }
}

代码 – 3

其实createViewFromTag这个办法中,终究的一个办法便是tryCreateView,在这个办法中回来的View便是createViewFromTag的回来值,当然也有或许创立失利,终究走到 代码-4中,但咱们先看下这个办法。

public final View tryCreateView(@Nullable View parent, @NonNull String name,
    @NonNull Context context,
    @NonNull AttributeSet attrs) {
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }
    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }
    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    return view;
}

在这个办法中,咱们看到创立View,其实是经过两个Factory,分别是:mFactory2和mFactory,经过调用它们的onCreateView办法进行View的实例化,假如这两个Factory都没有设置,那么终究回来的view = null;当然后边也有一个兜底策略,假如view = null,可是mPrivateFactory(其实也是Factory2)不为空,也能够经过mPrivateFactory创立。

1.3 Factory接口

在前面咱们说到两个成员变量,分别是:mFactory2和mFactory,这两个变量是LayoutInflater中的成员变量,咱们看下是在setFactory和setFactory2中进行赋值的。

public void setFactory(Factory 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 = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}
/**
 * Like {@link #setFactory}, but allows you to set a {@link Factory2}
 * interface.
 */
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);
    }
}

咱们体系在进行布局解析的时分,必定也是设置了自己的Factory,这样的话就直接走体系的初始化流程;

protected LayoutInflater(LayoutInflater original, Context newContext) {
    StrictMode.assertConfigurationContext(newContext, "LayoutInflater");
    mContext = newContext;
    mFactory = original.mFactory;
    mFactory2 = original.mFactory2;
    mPrivateFactory = original.mPrivateFactory;
    setFilter(original.mFilter);
    initPrecompiledViews();
}

可是假如咱们想完结换肤,是不是也可自定义换肤的Factory来代替体系的Factory,以此完结咱们想要的作用,e.g. 咱们在XML布局中设置了一个TextView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/cs_root"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <TextView
        android:id="@+id/tv_skin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="敞开换肤"
        android:textColor="#000000"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

咱们经过自定义的Factory2,在onCreateView中创立一个Button代替TextView。

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
    // 代码 - 5
    super.onCreate(savedInstanceState)
    val inflater = LayoutInflater.from(this)
    inflater.factory2 = object : LayoutInflater.Factory2 {
        override fun onCreateView(
            parent: View?,
            name: String,
            context: Context,
            attrs: AttributeSet
        ): View? {
            if (name == "TextView") {
                val button = Button(context)
                button.setText("换肤")
                return button
            }
            return null
        }
        override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
            return null
        }
    }
    val view = inflater.inflate(R.layout.layout_skin, findViewById(R.id.cs_root), false)
    setContentView(view)
}

可是运转之后,咱们发现报错了:

Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
        at android.view.LayoutInflater.setFactory2(LayoutInflater.java:314)
        at com.lay.learn.asm.MainActivity.onCreate(Unknown Source:22)

看报错的意思是现已设置了一个factory,不能重复设置。这行报错信息,咱们在1.3开头的代码中就能够看到,有一个标志位mFactorySet,假如mFactorySet = true,那么就直接报错了,可是在LayoutInflater源码中,只要在调用setFactory和setFactory2办法的时分,才会将其设置为true,那为什么还报错呢?

代码 – 5

既然只要在调用setFactory和setFactory2办法的时分,才会设置mFactorySet为true,那么原因只会有一个,便是重复调用。咱们看下super.onCreate(saveInstanceState)做了什么。

由于当时Activity继承了AppCompatActivity,在AppCompatActivity的结构办法中调用了initDelegate办法。

@ContentView
public AppCompatActivity(@LayoutRes int contentLayoutId) {
    super(contentLayoutId);
    initDelegate();
}
private void initDelegate() {
    // TODO: Directly connect AppCompatDelegate to SavedStateRegistry
    getSavedStateRegistry().registerSavedStateProvider(DELEGATE_TAG,
            new SavedStateRegistry.SavedStateProvider() {
                @NonNull
                @Override
                public Bundle saveState() {
                    Bundle outState = new Bundle();
                    getDelegate().onSaveInstanceState(outState);
                    return outState;
                }
            });
    addOnContextAvailableListener(new OnContextAvailableListener() {
        @Override
        public void onContextAvailable(@NonNull Context context) {
            final AppCompatDelegate delegate = getDelegate();
            delegate.installViewFactory();
            delegate.onCreate(getSavedStateRegistry()
                    .consumeRestoredStateForKey(DELEGATE_TAG));
        }
    });
}

终究会调用AppCompatDelegateImpl的installViewFactory办法。

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

在这个办法中,咱们能够看到,假如LayoutInflater获取到factory为空,那么就会调用setFactory2办法,这个时分mFactorySet = true,当咱们再次调用setContentView的时分,就直接报错,所以咱们需求在super.onCreate之前进行换肤的操作

当然咱们也能够经过反射的办法,在setFactory的时分将mFactorySet设置为false

1.4 小结

所以终究换肤的计划:经过Hook的办法,修正代替体系的Factory,从而自行完结组件的实例化,达到与体系行为共同的作用。

代码 – 4

假如有些View经过Factory没有实例化的,此时view为空,那么会经过反射的办法来完结组件实例化,像一些带包名的体系组件,或许自定义View。

2 换肤结构建立

其实在建立换肤结构的时分,咱们必定不行能对一切的控件都进行换肤,所以关于XML布局中的组件,咱们需求进行一次标记,那么标记的手段有哪些呢?

(1)创立一个接口,e.g. ISkinChange接口,然后重写体系一切需求换肤的控件完结这个接口,然后遍历获取XML中需求换肤的控件,进行换肤,这个是一个计划,可是成本比较高。

(2)自定义特点,由于关于每个控件来说都有各自的特点,假如咱们经过自定义特点的办法给每个需求换肤的控件加上这个特点,在实例化View的时分就能够进行区分。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Skinable">
        <attr name="isSupport" format="boolean"/>
    </declare-styleable>
</resources>

第一步:创立View并回来

这儿咱们创立了一个SkinFactory,完结了LayoutInflater.Factory2接口,这个类便是用于搜集需求换肤的组件,并完结换肤的功能。

class SkinFactory : LayoutInflater.Factory2 {
    override fun onCreateView(
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
    ): View? {
        //创立View
        //搜集能够换肤的组件
        return null
    }
    override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
        return null
    }
}

首要在onCreateView中,需求创立一个View并回来,咱们看下体系是怎么完结的。

Android进阶宝典 -- 使用Hook技术拦截系统实例化View过程实现App换肤功能

经过上面的截图咱们知道,经过AppCompatDelegate的完结类就能够完结view的创立。

override fun onCreateView(
    parent: View?,
    name: String,
    context: Context,
    attrs: AttributeSet
): View? {
    //创立View
    val view = delegate.createView(parent, name, context, attrs)
    if (view == null) {
        //TODO 没有创立成功,需求经过反射来创立
    }
    //搜集能够换肤的组件
    if (view != null) {
        collectSkinComponent(attrs, context, view)
    }
    return view
}
/**
 * 搜集能够进行换肤的控件
 */
private fun collectSkinComponent(attrs: AttributeSet, context: Context, view: View) {
    //获取特点
    val skinAbleAttr = context.obtainStyledAttributes(attrs, R.styleable.Skinable, 0, 0)
    val isSupportSkin = skinAbleAttr.getBoolean(R.styleable.Skinable_isSupport, false)
    if (isSupportSkin) {
        val attrsMap: MutableMap<String, String> = mutableMapOf()
        //搜集起来
        for (index in 0 until attrs.attributeCount) {
            val name = attrs.getAttributeName(index)
            val value = attrs.getAttributeValue(index)
            attrsMap[name] = value
        }
        val skinView = SkinView(view, attrsMap)
        skinList.add(skinView)
    }
    skinAbleAttr.recycle()
}

所以咱们在SkinFactory中传入一个AppCompatDelegate的完结类,调用createView办法先创立一个View,假如这个view不为空,那么会搜集每个View的特点,看是否支撑换肤。

搜集能够换肤的组件,其实便是依据自定义特点划分,经过获取View中自带悉数特点判断,假如支撑换肤,那么就存储起来,这部分仍是比较简略的。

第二步:换肤逻辑与Activity基类抽取

假如咱们想要进行换肤,例如替换布景、或许替换字体色彩等等,因而咱们需求设置几个换肤的类型如下:

sealed class SkinType{
    /**
     * 替换布景色彩
     * @param color 布景色彩
     */
    class BackgroundSkin(val color:Int):SkinType()
    /**
     * 替换布景图片
     * @param drawable 布景图片资源id
     */
    class BackgroundDrawableSkin(val drawable:Int):SkinType()
    /**
     * 替换字体色彩
     * @param color 字体色彩
     * NOTE 这个只能TextView才干是用
     */
    class TextColorSkin(val color: Int):SkinType()
    /**
     * 替换字体类型
     * @param textStyle 字体型号
     * NOTE 这个只能TextView才干是用
     */
    class TextStyleSkin(val textStyle: Typeface):SkinType()
}

当敞开换肤之后,需求遍历skinList中支撑换肤的控件,然后依据SkinType来对对应的控件设置特点,例如TextStyleSkin这类换肤类型,只能对TextView收效,因而需求依据view的类型来进行特点设置。

/**
 * 一键换肤
 */
fun changedSkin(vararg skinType: SkinType) {
    Log.e("TAG","skinList $skinList")
    skinList.forEach { skinView ->
        changedSkinInner(skinView, skinType)
    }
}
/**
 * 换肤的内部完结类
 */
private fun changedSkinInner(skinView: SkinView, skinType: Array<out SkinType>) {
    skinType.forEach { type ->
        Log.e("TAG", "changedSkinInner $type")
        when (type) {
            is SkinType.BackgroundSkin -> {
                skinView.view.setBackgroundColor(type.color)
            }
            is SkinType.BackgroundDrawableSkin -> {
                skinView.view.setBackgroundResource(type.drawable)
            }
            is SkinType.TextStyleSkin -> {
                if (skinView.view is TextView) {
                    //只要TextView能够换
                    skinView.view.typeface = type.textStyle
                }
            }
            is SkinType.TextColorSkin -> {
                if (skinView.view is TextView) {
                    //只要TextView能够换
                    skinView.view.setTextColor(type.color)
                }
            }
        }
    }
}

所以针对换肤的需求,咱们能够抽出一个抽象的Activity基类,叫做SkinActivity。

abstract class SkinActivity : AppCompatActivity() {
    private lateinit var skinFactory: SkinFactory
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.e("TAG", "onCreate")
        val inflate = LayoutInflater.from(this)
        //康复标志位
        resetmFactorySet(inflate)
        //敞开换肤模式
        skinFactory = SkinFactory(delegate)
        inflate.factory2 = skinFactory
        setContentView(inflate.inflate(getLayoutId(), getViewRoot(), false))
        initView()
    }
    open fun initView() {
    }
    protected fun changedSkin(vararg skinType: SkinType) {
        Log.e("TAG", "changedSkin")
        skinFactory.changedSkin(*skinType)
    }
    @SuppressLint("SoonBlockedPrivateApi")
    private fun resetmFactorySet(instance: LayoutInflater) {
        val mFactorySetField = LayoutInflater::class.java.getDeclaredField("mFactorySet")
        mFactorySetField.isAccessible = true
        mFactorySetField.set(instance, false)
    }
    abstract fun getLayoutId(): Int
    abstract fun getViewRoot(): ViewGroup?
}

在onCreate办法中,首要便是进行Factory的设置,这儿便是咱们前面说到的SkinFactory(完结了Factory2接口),然后定义了一个办法changedSkin,在任意子类中都能够调用。

class SkinChangeActivity : SkinActivity() {
    override fun initView() {
        findViewById<Button>(R.id.btn_skin).setOnClickListener {
            Toast.makeText(this,"替换布景",Toast.LENGTH_SHORT).show()
            changedSkin(
                SkinType.BackgroundSkin(Color.parseColor("#B81A1A"))
            )
        }
        findViewById<Button>(R.id.btn_skin_textColor).setOnClickListener {
            Toast.makeText(this,"替换字体色彩",Toast.LENGTH_SHORT).show()
            changedSkin(
                SkinType.TextColorSkin(Color.parseColor("#FFEB3B")),
                SkinType.BackgroundSkin(Color.WHITE)
            )
        }
        findViewById<Button>(R.id.btn_skin_textStyle).setOnClickListener {
            Toast.makeText(this,"替换字体样式",Toast.LENGTH_SHORT).show()
            changedSkin(
                SkinType.TextStyleSkin(Typeface.DEFAULT_BOLD),
            )
        }
    }
    override fun getLayoutId(): Int {
        return R.layout.activity_skin_change
    }
    override fun getViewRoot(): ViewGroup? {
        return findViewById(R.id.cs_root)
    }
}

具体的作用能够看动图:

Android进阶宝典 -- 使用Hook技术拦截系统实例化View过程实现App换肤功能

其实这儿仅仅完结了一个简略的换肤作用,其实在事务代码中,或许存在上千个View,那么经过这种办法就能够防止给每个View都去设置一遍布景、字体色彩……关键仍是在于原理的了解,其实真正的换肤现在主流的都是插件化换肤,经过下载皮肤包主动装备到App中,后续咱们就会介绍插件化换肤的中心思想。