Android实现动态换肤-原理篇

学习是一个过程。

Activity中LayoutInflater加载布局总体时序图

在这里插入图片描述

LayoutInflater源码讲解(api28)

  • onCreate加载布局,是不是都很熟悉。

       @Override
        protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_happy);
        }
    
  • AppCompatActivity的onCreate()方法,注意这不是Activity的onCreate方法。

        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            final AppCompatDelegate delegate = getDelegate();
            //注意此方法是做换肤的关键。
            delegate.installViewFactory();
            delegate.onCreate(savedInstanceState);
            super.onCreate(savedInstanceState);
        }
    
    
  • AppCompatDelegatelmpl的installViewFactory方法,AppCompatDelegatelmpl实现了Factory2接口。

        public void installViewFactory() {
            LayoutInflater layoutInflater = LayoutInflater.from(mContext);
            if (layoutInflater.getFactory() == null) {
                //AppCompatDelegatelmpl实现了Factory2接口
                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中的setFactory2方法,此方法不允许重复设置值,如果设置值会产生异常,所以如果做动态换肤设置Factory2时,要放在super.onCreate()方法之前,防止异常退出。

        public void setFactory2(Factory2 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;
            //mFactory与mFactory2一块赋值,mFractory2是按照扩展的方法进行开发的。
            if (mFactory == null) {
                mFactory = mFactory2 = factory;
            } else {
                mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
            }
        }
    
  • 继续分析setContentView()方法,AppCompatActivity中的setContentView调用的是AppCompatDelegatelmpl的方法。

      @Override
        public void setContentView(@LayoutRes int layoutResID) {
            getDelegate().setContentView(layoutResID);
        }
    
    
  • AppCompatDelegatelmpl的setContentView方法,此方法主要是加载我们自定义的布局,将布局添加到容器中。

      public void setContentView(int resId) {
          //主要是初始化根布局,用来存放我们自定义的布局。
            ensureSubDecor();
          //存放我们自定义布局的View,此View的类型是FrameLayout
            ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
            contentParent.removeAllViews();
          //加载自定义布局,将布局添加到contentParent中。
            LayoutInflater.from(mContext).inflate(resId, contentParent);
            mAppCompatWindowCallback.getWrapped().onContentChanged();
        }
    
    
  • LayoutInflater的inflate方法,此方法中有以后插件化用到的关键代码,此处先留意一下,以后有机会再进行分享插件化相关的技术。

        public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
            //此处是做插件化的关键,activity自定义getResources()方法,用来生产插件对应的资源。
            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();
            }
        }
    
    
  • 继续分析inflate方法,其重要流程是创建根布局,然后创建

     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 {
                    // Look for the root node.
                    int type;
                    while ((type = parser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty
                    }
    
                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(parser.getPositionDescription()
                                + ": No start tag found!");
                    }
    
                    final String name = parser.getName();
    
                    if (DEBUG) {
                        System.out.println("**************************");
                        System.out.println("Creating root view: "
                                + name);
                        System.out.println("**************************");
                    }
    
                    //处理 merge 标签
                    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
                        //自定义View的根布局,就是自己写的布局的根布局。
                        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.
                        //把自定义的xml所有除根布局之外的控件全部实例化然后添加进根布局
                        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;
                        }
                    }
    
                } catch (XmlPullParserException e) {
                    final InflateException ie = new InflateException(e.getMessage(), e);
                    ie.setStackTrace(EMPTY_STACK_TRACE);
                    throw ie;
                } catch (Exception e) {
                    final InflateException ie = new InflateException(parser.getPositionDescription()
                            + ": " + e.getMessage(), e);
                    ie.setStackTrace(EMPTY_STACK_TRACE);
                    throw ie;
                } finally {
                    // Don't retain static reference on context.
                    mConstructorArgs[0] = lastContext;
                    mConstructorArgs[1] = null;
    
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
    
                return result;
            }
        }
    
  • LayoutInflater的createViewFromTag方法,注意这里有一个BlinkLayout的闪烁小彩蛋,用来闪烁布局。

     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();
            }
    
         ``//闪烁的菜单,是为了庆祝??? 1995年庆祝什么节日?
            if (name.equals(TAG_1995)) {
                // Let's party like it's 1995!
                return new BlinkLayout(context, attrs);
            }
    
            try {
                View view;
    
                //如果是AppCompatActivity在这初始化,这个可以自己创建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);
                }
    
                //如果是Activity在这初始化。
                if (view == null) {
                    final Object lastContext = mConstructorArgs[0];
                    mConstructorArgs[0] = context;
                    try {
                        //不带包路径的View,最终都会调用到createView的这个方法
                        if (-1 == name.indexOf('.')) {
                            //这个最终会调用createView(name,“android.view.”,attrs),携带android.view前缀。
                            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;
            }
        }
    
  • 我们跟踪一下Layoutlnflater的createView方法,

    //利用反射创建对象,为啥不直接new对象呢?因为有些不能访问到?
    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;
    
                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);
            }
        }
    
    
  • 继续分析mFactory2.onCreateView()的方法,其最终会调用到AppCompatDelegatelmpl的createView(),此函数主要对mAppCompatViewInflater进行初始化,然后调用其createView()方法。

        public View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs) {
            if (mAppCompatViewInflater == null) {
                TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
                String viewInflaterClassName =
                        a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
                if ((viewInflaterClassName == null)
                        || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
                    // Either default class name or set explicitly to null. In both cases
                    // create the base inflater (no reflection)
                    mAppCompatViewInflater = new AppCompatViewInflater();
                } else {
                    try {
                        Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
                        mAppCompatViewInflater =
                                (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                        .newInstance();
                    } catch (Throwable t) {
                        Log.i(TAG, "Failed to instantiate custom view inflater "
                                + viewInflaterClassName + ". Falling back to default.", t);
                        mAppCompatViewInflater = new AppCompatViewInflater();
                    }
                }
            }
    
            boolean inheritContext = false;
            if (IS_PRE_LOLLIPOP) {
                inheritContext = (attrs instanceof XmlPullParser)
                        // If we have a XmlPullParser, we can detect where we are in the layout
                        ? ((XmlPullParser) attrs).getDepth() > 1
                        // Otherwise we have to use the old heuristic
                        : shouldInheritContext((ViewParent) parent);
            }
    
            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 */
            );
        }
    
    
  • AppCompatViewInflater的createView()来创建View,其最终都转换为AppCompat对应的组件,

    
        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;
    
            // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
            // by using the parent's 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
            //这里对View进行转换,自动转换为AppCompat对应的View。
            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;
                case "ToggleButton":
                    view = createToggleButton(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);
            }
    
            //如果不是以上对应的View,则调用以下方法进行创建。
            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;
        }
    
  • AppCompatViewInflater的createViewFromTag()方法,其与LayoutInflater的createViewFromTag方法有些类似,

        private View createViewFromTag(Context context, String name, AttributeSet attrs) {
            if (name.equals("view")) {
                name = attrs.getAttributeValue(null, "class");
            }
    
            try {
                mConstructorArgs[0] = context;
                mConstructorArgs[1] = attrs;
    
                if (-1 == name.indexOf('.')) {
                    //尝试增加不同的前缀进行创建,用反射,如果 反射失败则加载不成功,再重新尝试。
                    for (int i = 0; i < sClassPrefixList.length; i++) {
                        final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
                        if (view != null) {
                            return view;
                        }
                    }
                    return null;
                } else {
                    return createViewByPrefix(context, name, null);
                }
            } catch (Exception e) {
                // We do not want to catch these, lets return null and let the actual LayoutInflater
                // try
                return null;
            } finally {
                // Don't retain references on context.
                mConstructorArgs[0] = null;
                mConstructorArgs[1] = null;
            }
        }
    
    
  • 尝试用不同的前缀反射创建View,到了这里View就创建成功了。

        private View createViewByPrefix(Context context, String name, String prefix)
                throws ClassNotFoundException, InflateException {
            Constructor<? extends View> constructor = sConstructorMap.get(name);
    
            try {
                if (constructor == null) {
                    // Class not found in the cache, see if it's real, and try to add it
                    Class<? extends View> clazz = Class.forName(
                            prefix != null ? (prefix + name) : name,
                            false,
                            context.getClassLoader()).asSubclass(View.class);
    
                    constructor = clazz.getConstructor(sConstructorSignature);
                    //为了增加速度,这里也尝试了缓存的方式。
                    sConstructorMap.put(name, constructor);
                }
    
                constructor.setAccessible(true);
                return constructor.newInstance(mConstructorArgs);
            } catch (Exception e) {
                // We do not want to catch these, lets return null and let the actual LayoutInflater
                // try
                return null;
            }
        }
    

LayoutInflater设置Factory2

  • 在onCreateView中设置Factory2,通过回调函数我们能拿到View的名称(注意,我们连系统的根布局的名称也是可以拿到的),同事也可以拿到其对应的属性值,这样我们就可以根据这些值来创建我们的View,在这里也可以动态的设置我们想要的皮肤。

     @Override
        protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
            getLayoutInflater().setFactory2(new LayoutInflater.Factory2() {//这里主要负责View的创建。
                @Override
                public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                    Log.e(TAG, "name: " + name);
    
                    int attributeCount = attrs.getAttributeCount();
                    for (int i = 0; i < attributeCount; i++) {
                        String attributeName = attrs.getAttributeName(i);
                        Log.e(TAG, "attributeName: " + attributeName);
                    }
    
                    Log.e(TAG, "------------------------------------divide line------------------- ");
    
                    return null;
                }
    
                @Override
                public View onCreateView(String name, Context context, AttributeSet attrs) {
                    return null;
                }
            });
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_happy);
        }
    

    日志打印:

    name: LinearLayout
    attributeName: orientation
    attributeName: fitsSystemWindows
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    name: ViewStub
    attributeName: theme
    attributeName: id
    attributeName: layout
    attributeName: inflatedId
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    name: FrameLayout
    attributeName: id
    attributeName: layout_width
    attributeName: layout_height
    attributeName: foreground
    attributeName: foregroundGravity
    attributeName: foregroundInsidePadding
    ------------------------------------divide line------------------- 
    name: androidx.appcompat.widget.FitWindowsLinearLayout
    attributeName: orientation
    attributeName: id
    attributeName: fitsSystemWindows
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    name: androidx.appcompat.widget.ViewStubCompat
    attributeName: id
    attributeName: layout
    attributeName: inflatedId
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    name: androidx.appcompat.widget.ContentFrameLayout
    attributeName: id
    attributeName: layout_width
    attributeName: layout_height
    attributeName: foreground
    attributeName: foregroundGravity
    ------------------------------------divide line------------------- 
    name: LinearLayout
    attributeName: orientation
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    

实现方式

我们可以以apk的方式来打包资源,这个apk资源包中只存在资源文件,然后我们通过动态加载技术来加载apk资源包,实现动态换肤的功能。

  • 加载apk资源工具类,此工具类主要是加载资源apk里面的资源引用,这样就可以拿到网络下载来的资源包。
public class SkinResourceManager {
    private static SkinResourceManager skinManager = new SkinResourceManager();
    private Context context;
    private Resources resources;
    //获取到资源包中的包名
    private String skinPackageName;


    private SkinResourceManager(){}

    public static SkinManager getInstance(){
        return skinManager;
    }

    public void init(Context context){
        this.context = context;
    }

    /**
     * 根据资源包的 存储路劲去加载资源
     * @param path
     */
    public void loadSkinApk(String path){
        //真正的目的是得到资源包的资源对象
        try {
            PackageManager packageManager = context.getPackageManager();
            PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path,
                    PackageManager.GET_ACTIVITIES);
            skinPackageName = packageArchiveInfo.packageName;
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPath.setAccessible(true);
            //在assetManager中执行addAssetPath方法
            addAssetPath.invoke(assetManager,path);
            //创建一个资源对象  管理资源包里面的资源
            resources = new Resources(assetManager,context.getResources().getDisplayMetrics(),
                    context.getResources().getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    //写匹配各种资源的方法
    /**
     * 去匹配颜色
     * @param resId 需要去匹配ID
     * @return  匹配到的资源ID(在资源包中的资源ID)
     */
    public int getColor(int resId){
        if(resourceIsNull()){
            return resId;
        }
        //获取到资源名字和资源类型
        String resourceTypeName = context.getResources().getResourceTypeName(resId);
        String resourceEntryName = context.getResources().getResourceEntryName(resId);
        //去资源包的资源对象中去匹配
        int identifier = resources.getIdentifier(resourceEntryName,
                resourceTypeName, skinPackageName);
        if(identifier == 0){
            return resId;
        }
        return resources.getColor(identifier);
    }


    /**
     * 从资源包中拿到drawable的资源id
     */
    public Drawable getDrawable(int id){
        if(resourceIsNull()){
            //获取到这个id在当前应用中的Drawable对象
            return ContextCompat.getDrawable(context,id);
        }
        //获取到资源id的类型
        String resourceTypeName = context.getResources().getResourceTypeName(id);
        //获取到的就是资源id的名字
        String resourceEntryName = context.getResources().getResourceEntryName(id);
        //就是colorAccent这个资源在外置APK中的id
        int identifier = resources.getIdentifier(resourceEntryName, resourceTypeName, skinPackageName);
        if(identifier == 0){
            return ContextCompat.getDrawable(context,id);
        }
        return resources.getDrawable(identifier);
    }

    /**
     * 从资源包中拿到drawable的资源id
     */
    public int getDrawableId(int id){
        if(resourceIsNull()){
            return id;
        }
        //获取到资源id的类型
        String resourceTypeName = context.getResources().getResourceTypeName(id);
        //获取到的就是资源id的名字
        String resourceEntryName = context.getResources().getResourceEntryName(id);
        //就是colorAccent这个资源在外置APK中的id
        int identifier = resources.getIdentifier(resourceEntryName, resourceTypeName, skinPackageName);
        if(identifier == 0){
            return id;
        }
        return identifier;
    }


    public boolean resourceIsNull(){
        if(resources == null){
            return true;
        }
        return false;
    }

    public Resources getResources() {
        return resources;
    }
}
  • View的工厂方法,主要用来生成View。

    public class SkinFactory implements LayoutInflater.Factory2 {
        //当Activity继承自AppcompatActivity的时候  去通过这个对象来实例化控件
        private AppCompatDelegate delegate;
        private static final String[] prxfixList = {
                "android.widget.",
                "android.view.",
                "android.webkit."
        };
        //缓存构造方法的map
        private HashMap<String, Constructor<? extends View>> sConstructorMap =
                new HashMap<String, Constructor<? extends View>>();
        private Class<?>[] mConstructorSignature = new Class[] {
                Context.class, AttributeSet.class};
        List<SkinView> skinViews = new ArrayList<>();
    
        public SkinFactory(AppCompatDelegate delegate){
            this.delegate = delegate;
        }
    
        @Override
        public View onCreateView(View view, String s, Context context, AttributeSet attributeSet) {
            if(s.contains("LinearLayout")){
                Log.e("MN-------->","1111111111");
            }
            //实例化每个控件
            View crrentView = null;
            if(delegate !=null){
                crrentView = delegate.createView(view, s, context, attributeSet);
            }
            //兼顾了两种情况  第一种情况就是delegate为空 没有去实例化控件
            // 第二种情况就是delegate不为空 但是它没有替我们去实例化控件
            if(crrentView == null){
                //如果是Activity的情况下  通过反射去讲控件实例化的
                //1.带包名  2.不带包名
                if(s.contains(".")){
                    crrentView = onCreateView(s,context,attributeSet);
                }else{
                    for (String s1 : prxfixList) {
                        String className = s1+s;
                        crrentView = onCreateView(className,context,attributeSet);
                        if(crrentView!=null){
                            break;
                        }
                    }
                }
            }
            //收集需要换肤的控件
            if(crrentView!=null){
                paserView(context,crrentView,attributeSet);
            }
    
            return crrentView;
        }
    
        public void apply(){
            for (SkinView skinView : skinViews) {
                skinView.apply();
            }
        }
    
        /**
         * 收集需要换肤的控件
         * @param context
         * @param view
         * @param attributeSet
         */
        private void paserView(Context context, View view, AttributeSet attributeSet) {
    
            TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.Skinable);
            boolean isKin = typedArray.getBoolean(R.styleable.Skinable_is_skin, false);
            if(!isKin){
                return;
            }
            //再去找到这个控件需要换肤的属性
            List<SkinItem> skinItems =  new ArrayList<>();
            for(int x=0;x<attributeSet.getAttributeCount();x++){
                //属性的名字  类似 textColor  src
                String attributeName = attributeSet.getAttributeName(x);
                //如果说符合条件 句证明这条属性是需要换肤的
                if(attributeName.contains("textColor") || attributeName.contains("src")
                || attributeName.contains("background")){
                    //属性的名字  ID  类型
                    String resIdStr = attributeSet.getAttributeValue(x);
                    int resId = Integer.parseInt(resIdStr.substring(1));
                    String resourceTypeName = context.getResources().getResourceTypeName(resId);
                    String resourceEntryName = context.getResources().getResourceEntryName(resId);
                    SkinItem skinItem = new SkinItem(attributeName,resourceTypeName,
                            resourceEntryName,resId);
                    skinItems.add(skinItem);
                }
            }
            if(skinItems.size()>0){
                SkinView skinView = new SkinView(skinItems,view);
                skinViews.add(skinView);
            }
        }
    
        @Override
        public View onCreateView(String s, Context context, AttributeSet attributeSet) {
            View  view = null;
            try {
                //获取到控件的class对象
                Class aClass = context.getClassLoader().loadClass(s);
                Constructor<? extends View> constructor;
                constructor = sConstructorMap.get(s);
                if(constructor == null){
                    constructor = aClass.getConstructor(mConstructorSignature);
                    sConstructorMap.put(s,constructor);
                }
                view = constructor.newInstance(context,attributeSet);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return view;
        }
    
    
        public List<SkinView> getSkinViews() {
            return skinViews;
        }
    }
    
    
  • 对应的实体类

    
    
    /**
     * 每条属性的封装对象
     */
    public class SkinItem {
        //属性的名字  textColor text background
        String name;
        //属性的值的类型 color mipmap
        String typeName;
        //属性的值的名字  colorPrimary
        String entryName;
        //属性的资源ID
        int resId;
    
    
        public SkinItem(String name, String typeName, String entryName, int resId) {
            this.name = name;
            this.typeName = typeName;
            this.entryName = entryName;
            this.resId = resId;
        }
    
        public String getName() {
            return name;
        }
    
        public String getTypeName() {
            return typeName;
        }
    
        public String getEntryName() {
            return entryName;
        }
    
        public int getResId() {
            return resId;
        }
    }
    
    //----------------------------divide line-----------------
    public class SkinView {
        //这个控件需要换肤的属性对象的集合
        List<SkinItem> skinItems;
        View view;
    
    
        public SkinView(List<SkinItem> skinItems, View view) {
            this.skinItems = skinItems;
            this.view = view;
        }
    
        public void apply(){
            for (SkinItem skinItem : skinItems) {
                //判断这条属性是background吗?
                if(skinItem.getName().equals("background")){
                    //1. 设置的是color颜色  2.设置的是图片
                    if(skinItem.getTypeName().equals("color")){
                        //将资源ID  传给ResouceManager  去进行资源匹配   如果匹配到了  就直接设置给控件
                        // 如果没有匹配到  就把之前的资源ID  设置控件
                        view.setBackgroundColor(SkinResourceManager.getInstance().getColor(skinItem.getResId()));
                    }else if(skinItem.getTypeName().equals("drawable") || skinItem.getTypeName().equals("mipmap")){
                        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN){
                            view.setBackground(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));
                        }else{
                            view.setBackgroundDrawable(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));
                        }
                    }
                }else if(skinItem.getName().equals("src")){
                    if(skinItem.getTypeName().equals("drawable") || skinItem.getTypeName().equals("mipmap")){
                        //将资源ID  传给ResouceManager  去进行资源匹配   如果匹配到了  就直接设置给控件
                        // 如果没有匹配到  就把之前的资源ID  设置控件
                        ((ImageView)view).setImageDrawable(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));
                    }else if(skinItem.getTypeName().equals("color")){
    //                    ((ImageView)view).setImageResource(SkinManager.getInstance().getColor(skinItem.getResId()));
                    }
                }else if(skinItem.getName().equals("textColor")){
                    ((TextView)view).setTextColor(SkinResourceManager.getInstance().getColor(skinItem.getResId()));
                }
            }
        }
    
        public View getView() {
            return view;
        }
    
        public void setView(View view) {
            this.view = view;
        }
    
        public List<SkinItem> getSkinItems() {
            return skinItems;
        }
    
        public void setSkinItems(List<SkinItem> skinItems) {
            this.skinItems = skinItems;
        }
    }
    
    
    
  • 使用,

    注意:

    • 需要在Application初始化SkinResourceManager以及加载资源包
    • 需要在需要换肤的属性上面注册is_skin="true"属性,这个属于自定义属性,很简单就不在这赘述了。
    public class SkinActivity extends AppCompatActivity {
        public SkinFactory skinFactory;
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            skinFactory = new SkinFactory(getDelegate());
            getLayoutInflater().setFactory2(skinFactory);
            super.onCreate(savedInstanceState);
    
        }
    
    
        public void apply(){
            skinFactory.apply();
        }
    
    
        @Override
        protected void onResume() {
            super.onResume();
            skinFactory.apply();
        }
    }
    
    

LayoutInflater源码总结

  1. Activity与AppCompatActivity都是调用LayoutInflater进行View的创建,但是AppCompatActivity的View是用Factory2进行创建的,我们可以用这种机制来实现替换动态换肤的实现。
  2. LayoutInflater的Factory2是带有重设校验的,它是不支持重复设置参数的,我们可以有两种方式来设置我们的Factory2。
    • 在Activity的的surper.onCrate()方法调用setFactory2的方法设置Factory2.
    • 通过反射设置Factory2 的值。
  3. 在LayoutInflater里面有个1995闪烁layout,我们可以通过在标签中使用blink标签,来达到闪烁布局的效果。
  4. 创建View是通过反射进行创建的,为了加快构造方法的创建速度,将之前生成的构造方法进行缓存。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈德山

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值