Activity一键换肤
Activity一键换肤
Activity绘制过程
ActivityThread
查看ActivityThread代码源码,
performLaunchActivity函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
......
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);
Activity
我们在看Activity类中的attach函数中的部分1
2
3
4
5mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
再看我们在Activity中设置布局的setContentView函数在Activity中的实现1
2
3
4public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
这里可以看出我们传入的layout布局是设置到window对象上的,继续往里看
Window类中的setContentView是一个抽象方法,而getWindow()返回的是我们上面的PhoneWindow对象,我们看PhoneWindow中的setContentView方法
PhoneWindow
PhoneWindow.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
当初次运行时,肯定会走到installDecor 函数,在此函数中,又会新建一个DecorView绑定到window上。
也就是说我们的window上还有挂载一个DecorView
而在installDecor中,会对mContentParent判断,当为空时,会初始化
mContentParent = (DecorContentParent) mDecor.findViewById(R.id.decor_content_parent);
所以其实我们的view的层级其实是:
Window -> DecorView -> mContentParent + 我们自己的布局
而我们自己的View最后都是调用到LayoutInflater.inflate来加载的。
LayoutInflater
最后所有View的加载会走到tryCreateView函数
LayoutInflater 中的 tryCreateView函数
1 | @UnsupportedAppUsage(trackingBug = 122360734) |
可以看得出先判断mFactory2是否存在,存在的话,会通过mFactory2.onCreateView来创建View
再判断mFactory是否存在,存在的话,会通过mFactory.onCreateView来创建View
如果上面两个都不存在,则会调用mPrivateFactory来创建
实践
设计思路
根据上面的描述,我们知道所有的View创建都会走到LayoutInflater.tryCreateView函数,
而且这里有一个工厂类mFactory2,如果我们能创建一个mFactory2对象,并设置上去,那所有的view创建就都会走到我们自己的代码,
则我们可以获取到所有的view
换肤:是替换所有可以替换的颜色、背景,包括背景颜色、背景图片,因为我们替换了背景颜色,所以文本颜色也是需要替换。
我们明确了换肤的目标,原理呢就是:
换肤插件module中具有的资源名与宿主app的资源名都一致,只是资源值不同,
如在宿主app中res/value/colors.xml中1
2
3
4
5
6
7
8
9
10
11
12
13
14<!--宿主app-->
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="tabSelectedTextColor">#ffce3d3a</color>
</resources>
<!--插件module-->
<resources>
<color name="colorPrimary">#1F1F1F</color>
<color name="colorPrimaryDark">#373935</color>
<color name="colorAccent">#ffffff</color>
<color name="tabSelectedTextColor">#FFA500</color>
</resources>
如上所示,所有的资源名称都是一致,只是资源内容不同,图片也是如此。
当需要换肤时,通过获取宿主app的资源名称,到插件module中获取该名称的对应资源,然后替换