深入浅出换肤相关技术以及如何实现(下)

温馨提示:阅读本文需要35-40分钟


继续回到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 {

 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


wx号:gulinhai531

顾林海公众号

不定期推出优质文

章,喜欢的朋友们

给我个好看。

©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页