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

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

创建项目

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


动态换肤一(前期预备知识)_第1张图片
image.png

动态换肤一(前期预备知识)_第2张图片
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() 源码:


动态换肤一(前期预备知识)_第3张图片
AppCompatActivity&setContentView()

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


动态换肤一(前期预备知识)_第4张图片
AppCompatDelegate&create()

  我们发现,这就做了一些兼容性处理,我们随便选择一个,全局搜索 setContentVIew() 方法,我最终在 AppCompatDelegateImplV9 类里面找到了setContentView() 方法的具体实现。
动态换肤一(前期预备知识)_第5张图片
setContentView 源码

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


动态换肤一(前期预备知识)_第6张图片
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 的方法。


动态换肤一(前期预备知识)_第7张图片
设置 Factory2 的方法

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

你可能感兴趣的:(动态换肤一(前期预备知识))