Android 换肤的思路

释放双眼,带上耳机,听听看~!

结合代码看更好理解:文章底部 代码链接

换肤思路:

我们需要解决的几个问题

1.什么时候换肤?

xml加载前换肤,如果xml加载后换肤,用户将会看见换肤之前的色彩,用户体验不好。

2.皮肤是什么?

皮肤就是apk,是一个资源包,包含了颜色、图片等。

3.什么样的控件应该进行换肤?

包含背景图片的控件,例如textView文字颜色。

4.皮肤与已安装的资源如何匹配?

资源名字匹配

思路解析

首先换肤的基本思想是更换资源索引和路径(图片,颜色值,背景等等),需要注意就是规划好,比如颜色值 要提出来到color.xml 中,不要写死成“#xxxxxx”
我们制作一个皮肤插件包,这个就由一个资源apk来承载,用到了系统application初始化时候的原理,会在LoadApk的时候加载Resources通过AssertManager进行资源的加载
实现这个就需要在布局Xml加载之前setContentView()替换掉,这样用户体验比较好(之后替换也是可以但是会不自然发生切换)
根据源码:需要在自己代码setContentView 之前,自己实现一个SkinFactory extends Factory2 。 并且把这个SkinFactory 设置
类似,LayoutInflaterCompat.setFactory(getLayoutInflater(),skinFactory);
这里需要注意,setFactory 之后会有一个标记 为 true,我们需要用反射吧这个true改成false,来避免只能设置一次
整体思路:
用一个SkinFactoryManager 管理类处理资源管理过滤。我们需要对要更换的view进行扫描记录,过滤和更换对应的属性和资源的路径等。
1. 设置自己实现Factory2 的SkinFactory
2.我们需要 用一个数据结构记录下,我们应该换肤的View,然后过滤View的属性进行换肤替换
List<我们要更换的View>
List<(View 的属性)>
LIst<Paie(属性,view)>
3. 获取资源,通过SkinManager 加载apk 资源
4. 执行更换,通过记录的List<View> 循环View 设置颜色,背景等属性

原理分析

四个方面去分析原理

1.UI布局流程分析
2.LayoutInflate原理
3.Android资源加载流程
4 .插件化换肤原理分析

UI 布局流程分析

一般情况下我们会有这两个入口

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvp);

ActivityThread——>main函数 是app启动过程,这个过程先忽略,

我们入口从performLaunchActivity开始

->「起始点」
我们跟踪到方法临时变量 window = r.mPendingRemoveWindow

    r.mPendingRemoveWindow  初始值 null
继续搜索r.mPendingRemoveWindow = 找到赋值点
r.mPendingRemoveWindow = r.window;
继续搜索 r.window 赋值点,为null的不用看
r.window = r.activity.getWindow();—————就是Activity上的属性mWindow
目前位置看回到起始点,我们的赋值为空,只是拿到Activity的属性mWindow的引用
随后我们会调用activity.attach
Activity————mWindow = new PhoneWindow(this, window, activityConfigCallback);

「总结点1:--------------- 层次Activity---> PhoneWindow」

    上面的setContentView 就是在PhoneWindow中实现的,所以看下PhoneWindow的源代码
installDecor();———其实就是 mDecor = new DecorView  中间做了写操作
查看 mDector 就是PhoneWindow的成员

「总结点2: 层次 Activity——>PhoneWindow——->DecorView」

    installDector—> generateLayout
看到源码注释:  Inflate the window decor 解析decor
layoutResource 会有各种R.layout.xxxxx     这些其实都是AS中的模板等,还有一些其他的
去Framework 源码搜索,screen_simplev 看下他的结构
-> screen_simple.xml  这就是我们的root布局文件
<LinearLayout     xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
如果我们的布局文件选定,就会调用
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource)
DectorVIew 就会调用 final View root = inflater.inflate(layoutResource, null);解析刚刚的布局
再通过addView的方式把他加到DectorView

「总结点3: 层次 Activity——>PhoneWindow——->DecorView()---> 我们的content的布局xml」

PhoneWindow 再次 inflate ———> mLayoutInflater.inflate(layoutResID, mContentParent);
而这个mContentParent 就是我们的DectorView

回到 PhoneWindow 创建generateLayout-》mContentParent

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
content 就是我们布局中的content。  所以  DectorView—mContentParent—FrameLayout
最终调用了LayouInflater->inflate 方法
根据Tag创建临时temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}

判断如果attchToRoot = false 我们的参数就会通过xml 获取属性写进去 如果true 就需要我们addView 手动将布局参数填写进去

LayoutInflater继续往下看createViewFromTag ——> createView()
看到是通过根据view name进行反射构造方法。

继续看tryCreateView 在onCreateVIew 之前
会有三个工厂,Factory2(包含父类的),Factory(无相关父类),Factory2 privateFactory. 如果工厂不为null后面的onCreateView就会被拦截

——————Hook点

所以换肤的思路,一个是 实现Factory,对 onCrateView 提前进行拦截

第二个思路,重写Inflate(会有一定的侵入性质)

资源加载的原理

apk 包 resource.asrc 二进制文件信息

-> 入口:handleBindApplication

看到一个关键信息

mInstrumentation = new Instrumentation(); 创建了一个仪表盘
初始化我们的Application
new ContextImpl
然后获取我们的资源
LoadedApk->getResources-> getOrCreateResources——> createResourcesImpl(ResourceImpl)
->final AssetManager assets = createAssetManager(key);

总结点1:加载的层次调用

LoadedAPK
Resources
AssertManager
根据mInstrumentation 信息调用Application初始化
mInstrumentation.callApplicationOnCreate(app);

代码链接 :https://github.com/zcwfeng/zcw_android_demo/tree/master/android_skin

 

人已赞赏
Android文章

AndroidStudio更新时不小心点了ignore This Update,解决办法

2021-1-7 16:05:15

Android文章

iOS关于ScrollViewer滚动条滚动至最底部的实现

2021-1-15 5:31:29

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索