深入淺出換膚相關技術以及如何實現

顧林海發表於2019-02-20

人生一切難題,知識給你答案

溫馨提示:閱讀本文需要60-70分鐘
微信公眾號:顧林海

完成換膚需要解決兩個問題:

未命名檔案 (15).png

如何獲取換膚的View,利用LayoutInflater內部介面Factory2提供的onCreateView方法獲取需要換膚的View,我們從setContentView方法的具體作用來了解LayoutInflater.Factory2介面的作用,以具體原始碼進行分析,MainActivity程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
複製程式碼

MainActivity繼承自AppCompatActivity,AppCompatActivity是Android Support Library包下的類,點選進入AppCompatActivity的setContentView方法:

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
複製程式碼

通過getDelegate()方法返回一個AppCompatDelegate物件,並呼叫AppCompatDelegate物件的setContentView方法。

    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }
複製程式碼

通過AppCompatDelegate的create方法建立AppCompatDelegate物件:

    public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
        return create(activity, activity.getWindow(), callback);
    }
複製程式碼

通過create方法返回AppCompatDelegate物件:

    private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        if (Build.VERSION.SDK_INT >= 24) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else {
            return new AppCompatDelegateImplV14(context, window, callback);
        }
    }
複製程式碼

AppCompatDelegate物件的建立是根據SDK的不同版本而建立的,其中AppCompatDelegateImplN、AppCompatDelegateImplV23以及AppCompatDelegateImplV14的繼承結構如下圖所示:

未命名檔案 (13).png

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中的一個內部類,它會載入相應的系統佈局。如下圖:

未命名檔案 (14).png

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;
    }

複製程式碼

整個呼叫流程圖如下:

未命名檔案 (17).png

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);
    }
}
複製程式碼

執行效果如下:

wq7.gif

從控制檯列印的資訊我們已經知道哪些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");
            }
        });
複製程式碼

執行效果如下:

wq8.gif

github地址請點選這裡

相關文章