美文网首页最近需要做的
动态换肤一(前期预备知识)

动态换肤一(前期预备知识)

作者: radish520like | 来源:发表于2018-05-14 14:41 被阅读50次

      动态换肤框架是仿照网易云音乐来换肤的,换肤的方式就是通过解压 apk 文件从中获取到皮肤包的资源,然后替换我们主包中的资源。

    创建项目

      首先我们新建一个项目,再在这个项目里面新建一个 module 模拟第三方框架引入。


    image.png
    image.png

    模拟使用者使用

      ,假如我们是调用者,我们使用这个框架的时候,当然是希望越简单越好,如果我是使用者,我可能会希望这样使用这个框架。

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            findViewById(R.id.tv_click).setOnClickListener(v -> changeSkin());
        }
    
        private void changeSkin() {
            /**
             * 这里我希望传入一个皮肤包的路径,然后框架帮我换肤,
             * 如果我传入的是一个空的字符串,框架帮我换到主项目的原始皮肤
             */
            XXX.load("xxxxx");
        }
    }
    

    思考的问题

      我们这个换肤,其实就是给每个控件的某个属性换一个不同的值,比如:换肤前,TextView 的 android:textColor="@color/white",那么我们点击换肤按钮后, TextView 的 android:textColor="@color/black",大体就是这个意思。
      那么我们首先要思考下面几个问题:

    • 1.我们如何拿到 View 来进行换肤,框架层,肯定不能用 findViewById
    • 2.拿到 View 后,我们如何拿到皮肤包中的资源文件
        也就两个问题,我们将这些问题逐个进行解决。

    问题1:如何拿到 View

      该问题应该拆分成两步,第一步是拿到该 Activity 或者说这个 Activity 的布局文件中的所有控件;第二部是从这些全部的控件中筛选出我们需要换肤的 View,因为并不是所有的 View 都需要换肤。

    拿到每个 Activity 的布局文件中的所有的 View

      首先,我们在 Activity 的 onCreate() 方法里面,可以直接通过 findViewById() 方法拿到对应的控件,说明我们所有的 View 都已经创建完毕并且加载到当前的 Activity 里面了,那么我们的换肤框架也想要拿到所有的 View ,怎么办?观察 Activity ,发现 setContentView() 方法有蹊跷。

    注意:我的项目中 Activity 继承自 AppCompatActivity,(API level = 26)

      查看 setContentView() 源码:


    AppCompatActivity&setContentView()

      这个 getDelegate() 方法返回的是一个 AppCompatDelegate,点进去发现,实际上调用的是 AppCompatDelegate.create() 方法,一路跟踪下去:


    AppCompatDelegate&create()
      我们发现,这就做了一些兼容性处理,我们随便选择一个,全局搜索 setContentVIew() 方法,我最终在 AppCompatDelegateImplV9 类里面找到了setContentView() 方法的具体实现。
    setContentView 源码

      找到这,那么我们就要看看 LayoutInflater 的 inflate() 方法干了什么。


    inflate()
      在这个方法里面,首先获取了 Resources 对象,然后通过 getLayout() 方法获取了一个 XML 解析器(这里用的是 Pull 解析),最后调用 inflate() 的另一个重载方法,将生成的 View 返回。
      重载的 inflate() 代码有点长,就不全部截取了。
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            synchronized (mConstructorArgs) {
                try {
                    // Look for the root node.
                    int type;
    //              你看,Pull 解析
                    while ((type = parser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty
                    }
    
                    if (TAG_MERGE.equals(name)) {
                       ...
                        }
                        rInflate(parser, root, inflaterContext, attrs, false);
                    } else {
                        // Temp is the root view that was found in the xml
    //                  获取到 temp
                        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                        ...
    //                  将 temp 赋值给 result
                        if (root == null || !attachToRoot) {
                            result = temp;
                        }
                    }
    
                } catch (XmlPullParserException e) {
                  ...
                } finally {
                   ...
                }
    //          最终返回的是 result
                return result;
            }
        }
    

      我们发现,最后 return 的是 result ,而这个 result 在前面又被 temp 赋值了,而这个 temp 是通过 createViewFromTag() 方法返回的。我们继续看 createViewFromTag() 方法。

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
            ...
    //      注意下面代码
            try {
                View view;
                if (mFactory2 != null) {
                    view = mFactory2.onCreateView(parent, name, context, attrs);
                } else if (mFactory != null) {
                    view = mFactory.onCreateView(name, context, attrs);
                } 
                ...
                if (view == null && mPrivateFactory != null) {
                    view = mPrivateFactory.onCreateView(parent, name, context, attrs);
                }
                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);
                        }
                    } 
                    ...
                }
                return view;
            } catch (InflateException e) {
                ...
            } 
        }
    

      OK!看到这,我们大概明白了,是通过 mFactory2 或者 mFactory 或者 mPrivateFactory 的 onCreateView() 方法来创建的 View,如果上述方法都不行,则通过反射调用构造方法的方式来创建相应的 View 的。
      看到这里,有点想法,这个 mFactory2 的 onCreateView() 方法里面可以拿到 View 的 name,还有 attrs 属性,那通过反射的方式,就可以拿到对应的 View 了。正好这个 Factory2 是一个借口,我们给 LayoutInfalter 提供我们自定义的 Factory2,就会调用到我们的 onCreateView() 方法来创建 View 了。
      而且,很巧的是,LayoutInfalter 为我们提供了一个设置 Factory2 的方法。


    设置 Factory2 的方法

    下一篇文章地址:https://www.jianshu.com/p/1e180d8ed33b

    相关文章

      网友评论

        本文标题:动态换肤一(前期预备知识)

        本文链接:https://www.haomeiwen.com/subject/mqgddftx.html