动态换肤框架是仿照网易云音乐来换肤的,换肤的方式就是通过解压 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 的方法
网友评论