背景
很多app需要进行换肤,到网络上找到了一个库--android-skin-support。很快就实现了需求,根据文档集成很方便,代码侵入性也低。
本来也没有去深究它的实现原理,前几天有个迭代的需求如下:
美股市场是“绿涨红跌”,而A股市场是“红涨绿跌”,产品要求用户可以自由选择。这个需求跟以前的换肤很类似,所以就仔细研究学习了"android-skin-support"库的实现原理,并根据其原理实现“红涨绿跌”.
原理
-
使用观察者模式
框架抽象了一个SkinCompatSupportable
接口:
public interface SkinCompatSupportable {
void applySkin();
}
所有需要换肤的控件都实现该接口,在用户执行“换肤”操作时候,通知所有实现该接口的订阅者执行换肤操作applySkin()
。
按照上面的原理,那所有的控件都需要实现接口SkinCompatSupportable
,那原生的控件怎么办呢?框架层面把所有常用的原生控件都重写了一遍,在包skin.support.widget
下。框架层在解析布局文件的时候会把原生的控件替换成实现了接口SkinCompatSupportable
的对应控件。框架层是怎么做的呢?请继续查看下文。
-
注册activity生命周期监听器
查看类skin.support.app.SkinActivityLifecycle
,private SkinActivityLifecycle(Application application) { application.registerActivityLifecycleCallbacks(this); installLayoutFactory(application); SkinCompatManager.getInstance() .addObserver(getObserver(application)); } private void installLayoutFactory(Context context) { LayoutInflater layoutInflater = LayoutInflater.from(context); try { Field field = LayoutInflater.class.getDeclaredField("mFactorySet"); field.setAccessible(true); field.setBoolean(layoutInflater, false); LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context)); } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { if (isContextSkinEnable(activity)) { installLayoutFactory(activity); updateStatusBarColor(activity); updateWindowBackground(activity); if (activity instanceof SkinCompatSupportable) { ((SkinCompatSupportable) activity).applySkin(); } if (activity instanceof SkinCompatChangeGreenRed) { boolean isSupport = ((SkinCompatChangeGreenRed)activity).isSupportChange(); ((SkinCompatChangeGreenRed)activity).applyChangeColor(isSupport ? SkinCompatChangeGreenRed.STATE_CHANGE : SkinCompatChangeGreenRed.STATE_DEFAULT); } } }
注意 installLayoutFactory 方法,在每个activity的#onActivityCreated中把自己的LayoutInflaterFactory类(SkinCompatDelegate)设置进去,而把原生控件替换成库中的控件就在这个类中实现的,并且把所有实现SkinCompatSupportable接口的观察者都收集起来。这样代码的侵入性就变得很低,使得几行代码就可以实现换肤操作。
或许有人疑惑为什么这样做就可以实现xml解析拦截? 我们再来看看 ```AppCompatActivity``` 的代码实现。
```java
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
我们看到有一个AppCompatDelegate,它是Activity的委托,AppCompatActivity将大部分生命周期都委托给了AppCompatDelegate,这点可从上面的源码中可以看出.
继续看源码我们发现,解析xml布局的解析起也是在AppCompatDelegate对象中设置的。
AppCompatDelegateImplV9.java
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
LayoutInflaterCompat.setFactory2(layoutInflater, this);最终是调用的LayoutInflater的setFactory2()方法,看看实现
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
这里有个小细节,Factory2只能被设置一次,设置完成后mFactorySet属性就为true,下一次设置时被直接抛异常.
那么Factory2有什么用呢?看看其实现
它是一个接口,只有一个方法,看起来是用来创建View的.
综上所述,我们就可以根据其机制实现xml解析并拦截,让解析xml原生控件的时候返回我们想要的支持“换肤”动作的对应控件。
- 拦截xml控件解析事件
代码如下
SkinCompatViewInflater.java
private View createViewFromFV(Context context, String name, AttributeSet attrs) {
View view = null;
if (name.contains(".")) {
return null;
}
switch (name) {
case "View":
view = new SkinCompatView(context, attrs);
break;
case "LinearLayout":
view = new SkinCompatLinearLayout(context, attrs);
break;
case "RelativeLayout":
view = new SkinCompatRelativeLayout(context, attrs);
break;
case "FrameLayout":
view = new SkinCompatFrameLayout(context, attrs);
break;
case "TextView":
view = new SkinCompatTextView(context, attrs);
break;
case "ImageView":
view = new SkinCompatImageView(context, attrs);
break;
case "Button":
view = new SkinCompatButton(context, attrs);
break;
case "EditText":
view = new SkinCompatEditText(context, attrs);
break;
case "Spinner":
view = new SkinCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new SkinCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new SkinCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new SkinCompatRadioButton(context, attrs);
break;
case "RadioGroup":
view = new SkinCompatRadioGroup(context, attrs);
break;
case "CheckedTextView":
view = new SkinCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new SkinCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new SkinCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new SkinCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new SkinCompatSeekBar(context, attrs);
break;
case "ProgressBar":
view = new SkinCompatProgressBar(context, attrs);
break;
case "ScrollView":
view = new SkinCompatScrollView(context, attrs);
break;
}
return view;
}
这里还是有点迷惑, 那么我们再来看看android创建view的过程
平时我们最常使用的Activity中的setContentView()设置布局ID,看看Activity中的实现,
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
调用的是Window中的setContentView(),而Window只有一个实现类,就是PhoneWindow.看看setContentView()实现
@Override
public void setContentView(int layoutResID) {
...
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
看到了今天的主角mLayoutInflater,mLayoutInflater是在PhoneWindow的构造方法中初始化的.用mLayoutInflater去加载这个布局(layoutResID).点进去看看实现,来看看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);
} else {
view = null;
}
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);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
...
return view;
}
可以看到如果mFactory2不为空的话,那么就会调用mFactory2去创建View(mFactory2.onCreateView(parent, name, context, attrs)) . 这句结论很重要.前面的答案已揭晓.如果设置了mFactory2就会用mFactory2去创建View.而mFactory2在上面已经被我们替换了。****
- 加载皮肤
我们前面已经了解了换肤的原理,现在就根据上述原理进行换肤。 该框架提供了几种加载皮肤的策略,前后缀价值,apk加载等等。
SkinCompatManager.getInstance().loadSkin
总结
简单总结一下原理(本文精髓)
监听APP所有Activity的生命周期(registerActivityLifecycleCallbacks())
在每个Activity的onCreate()方法调用时setFactory(),设置创建View的工厂.将创建View的琐事交给SkinCompatViewInflater去处理.
库中自己重写了系统的控件(比如View对应于库中的SkinCompatView),实现换肤接口(接口里面只有一个applySkin()方法),表示该控件是支持换肤的.并且将这些控件在创建之后收集起来,方便随时换肤.
在库中自己写的控件里面去解析出一些特殊的属性(比如:background, textColor),并将其保存起来
在切换皮肤的时候,遍历一次之前缓存的View,调用其实现的接口方法applySkin(),在applySkin()中从皮肤资源(可以是从网络或者本地获取皮肤包)中获取资源.获取资源后设置其控件的background或textColor等,就可实现换肤.
借鉴应用
现在根据上述原理低侵入性实现“红涨绿跌”,
- 接口抽象
定义一个接口,让支持“红涨绿跌”切换的控件都实现该接口
SkinCompatChangeGreenRed
- 所有观察者都集合起来
- 执行“红涨绿跌”操作的时候通知所有观察者。
public void notifyChangeColor(boolean isSupport){
SkinObserver[] arrLocal;
synchronized (this) {
arrLocal = observers.toArray(new SkinObserver[observers.size()]);
}
for (int i = arrLocal.length-1; i>=0; i--)
arrLocal[i].updateChangeColor(this, isSupport? SkinCompatChangeGreenRed.STATE_CHANGE : SkinCompatChangeGreenRed.STATE_DEFAULT);
}
- 切换颜色的动作 每个控制自己处理。