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

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

舒適提示:閱讀本文須要60-70分鐘
微信公衆號:顧林海php

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

未命名文件 (15).png

如何獲取換膚的View,利用LayoutInflater內部接口Factory2提供的onCreateView方法獲取須要換膚的View,咱們從setContentView方法的具體做用來了解LayoutInflater.Factory2接口的做用,以具體源碼進行分析,MainActivity代碼以下:android

public class MainActivity extends AppCompatActivity {

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

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

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

經過getDelegate()方法返回一個AppCompatDelegate對象,並調用AppCompatDelegate對象的setContentView方法。github

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

經過AppCompatDelegate的create方法建立AppCompatDelegate對象:web

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

經過create方法返回AppCompatDelegate對象:緩存

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

AppCompatDelegate對象的建立是根據SDK的不一樣版本而建立的,其中AppCompatDelegateImplN、AppCompatDelegateImplV23以及AppCompatDelegateImplV14的繼承結構以下圖所示:微信

未命名文件 (13).png

AppCompatDelegate是一個抽象類,AppCompatDelegateImplBase也是抽象類,主要對AppCompatDelegate功能的擴展,具體的實現類是AppCompatDelegateImplV9,以上根據SDK版本建立的類都繼承自AppCompatDelegateImplV9。app

繼續回到AppCompatActivity的setContentView方法:ide

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

獲取AppCompatDelegate對象後,經過該對象的setContentView方法設置ContentView,這個setContentView方法的具體調用是在AppCompatDelegateImplV9中,查看源碼以下:

//android.support.v7.app.AppCompatDelegateImplV9

    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        //註釋1
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

複製代碼

setContentView方法最核心的地方就是在註釋1處,經過LayoutInflater加載layout.xml文件,contentParent是咱們建立佈局後所要添加進去的一個容器,在建立Activity時會建立頂層視圖,也就是DecorView,DecorView實際上是PhoneWindow中的一個內部類,它會加載相應的系統佈局。以下圖:

未命名文件 (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地址請點擊這裏



關注公衆號:

相關文章
相關標籤/搜索