介绍
所谓的多主题切换,就是能够根据不同的设定,呈现不同风格的界面给用户。想实现Android多套主题的切换,网络上方案已经很多了,也看了许多大神的实现方式,但心里总想着自己去实现一遍,就这么借鉴GitHub的开源实现了一个简单的Android换肤框架。
实现的思路
通过LayoutInflaterCompat.setFactory方式,在onCreateView的回调中,解析每一个View的attrs, 判断是否有已标记需要换肤的属性, 比方说background, textColor, 或者说相应资源是否为skin_开头等等.然后保存到集合中, 将相应的属性收集到一起。 这种方式相对是比较简单的,易于实现的方式。于是我也采用了这种方式去捉摸一番。
最后实现的效果
项目地址:
https://github.com/zguop/Towards 存在于wt_library下 theme包中。
一张图了解Android中的主题颜色
我们可以根据这里的颜色定义成多套的主题Style,来应用我们的Android应用。
主题实现
public void init(final AppCompatActivity activity) {
LayoutInflaterCompat.setFactory(LayoutInflater.from(activity), new LayoutInflaterFactory() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
List skinAttrsList = getSkinAttrs(attrs, context);
//如果属性为null 并且名字没有包含. 说明不是自定义的view
if (skinAttrsList == null || skinAttrsList.isEmpty()) {
if (!name.contains(".")) {
return null;
}
}
View view = activity.getDelegate().createView(parent, name, activity, attrs);
if (view == null) {
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
if (skinAttrsList == null && !(view instanceof SkinCompatSupportable)) {
return null;
}
mSkinViewList.add(new SkinView(view, skinAttrsList));
}
return view; }
});
}
oncreate中会回调当前页面中的view名称及view中所使用到的属性,
该方法主要是获取到视图中,符合条件换肤的view,和需要变更的xml属性,保存到集合中。
关于LayoutInflaterCompat.setFactory的作用,这里有一篇文章讲解
http://blog.csdn.net/lmj623565791/article/details/51503977
List skinAttrsList = getSkinAttrs(attrs, context);
首先我们通过getSkinAttrs()方法获取到一个view中需要换肤的属性集合,
private static final String COLOR_PRIMARY = "colorPrimary";
private static final String COLOR_PRIMARY_DARK = "colorPrimaryDark";
private static final String COLOR_ACCENT = "colorAccent";
private static final String ATTR_PREFIX = "skin"; //开头
private List getSkinAttrs(AttributeSet attrs, Context context) {
List skinAttrsList = null;
SkinAttr skinAttr;
//遍历所有属性
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//获取到当前的属性名字,
String attributeName = attrs.getAttributeName(i);
//改方法获取到枚举中定义好的需要更改的属性进行匹配
SkinAttrType attrType = getSupportAttrType(attributeName);
if (attrType == null) {
continue;
}
//获取当前属性对应的值 并解析,如果是使用?attr/ 或者是 @color/属性
String attributeValue = attrs.getAttributeValue(i);
if (attributeValue.startsWith("?") || attributeValue.startsWith("@")) {
//获取到该资源的id
int id = ThemeUtils.getAttrResId(attributeValue);
if (id != 0) {
//通过资源id 获取到资源的名称
String entryName = context.getResources().getResourceEntryName(id);
//如果匹配 资源名称 表示都是使用了换肤的 属性则保存起来
if (entryName.equals(COLOR_PRIMARY) || entryName.equals(COLOR_PRIMARY_DARK) || entryName.equals(COLOR_ACCENT) || entryName.startsWith(ATTR_PREFIX)) {
if (skinAttrsList == null) {
skinAttrsList = new ArrayList<>();
}
String typeName = context.getResources().getResourceTypeName(id);
skinAttr = new SkinAttr(attrType, entryName, attributeName, typeName);
skinAttrsList.add(skinAttr);
}
}
}
}
return skinAttrsList;
}
遍历所有属性,获取每一个属性的名字,通过getSupportAttrType()方法进行属性匹配,
private SkinAttrType getSupportAttrType(String attrName) {
for (SkinAttrType attrType : SkinAttrType.values()) {
if (attrType.getAttrType().equals(attrName))
return attrType;
}
return null;
}
实现定义好一个需要换肤的一些属性枚举,这里定义了三个枚举值,当前大家也可以根据自己的需要定义更多的属性。background背景色,textColor字体颜色,src图片,当遍历的属性刚好是以下属性的时候,那么就认为是一个有效的,需要换肤的view。
enum SkinAttrType {
BACKGROUD("background") {
@Override
public void apply(View view, String attrName, String attrValueResName, String attrValueTypeName) {
Context context = view.getContext();
view.setBackground(ThemeUtils.getDrawable(context, getResId(context, attrName)));
}
},
COLOR("textColor") {
@Override
public void apply(View view, String attrName, String attrValueResName, String attrValueTypeName) {
Context context = view.getContext();
((TextView) view).setTextColor(ThemeUtils.getColorStateList(context, getResId(context, attrName)));
}
},
SRC("src") {
@Override
public void apply(View view, String attrName, String attrValueResName, String attrValueTypeName) {
Context context = view.getContext();
((ImageView) view).setImageDrawable(ThemeUtils.getDrawable(context, getResId(context, attrName)));
}
}
}
枚举中定义了一个抽象方法
public abstract void apply(View view, String attrName, String attrValueResName, String attrValueTypeName);
用于最后的更换颜色调用apply方法的实现。
匹配换肤属性的规则:定义好匹配的属性 这里选择了 系统的 三种 colorPrimary 名字 或者是 skin开头的,这里的资源属性命名有需要规范了,例如需要background背景色,那么?attr/colorPrimary使用或者是
@color/skin_bottom_bar_not skin命名开头的资源,就认定为时需要换肤的属性值。
实例
app:background="?attr/colorPrimary"
app:background="@drawable/skin_bottom_bar_not"
获取到每一条有效的换肤属性,保存到集合中返回。
这里就是将所有需要换肤的属性返回来了,属性有 了,那么还需要生成对应的view,最后更换主题的时候去设置view所需要改变的颜色。
回到onCreateView方法中
View view = activity.getDelegate().createView(parent, name, activity, attrs);
if (view == null) {
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
if (skinAttrsList == null && !(view instanceof SkinCompatSupportable)) {
return null;
}
mSkinViewList.add(new SkinView(view, skinAttrsList));
}
return view;
如果有存在需要换肤的属性集合,才会去创建该view,保存到一个最终的集合中。这里就完成了一个初始化的过程,获取所以需要换肤的view,保存起来。那么接下就可以变更主题了。对外提供了两个方法
* 装载改变主题的view 及 需要改变的主题元素
public class SkinView {
private View view;
private List attrs;
public SkinView(View view, List skinAttrs) {
this.view = view;
this.attrs = skinAttrs;
}
public void apply() {
if (view == null) {
return;
}
if (view instanceof SkinCompatSupportable) {
((SkinCompatSupportable) view).applySkin();
}
if (attrs == null) {
return;
}
for (SkinAttr attr : attrs) {
attr.apply(view);
}
}
}
/** * 设置当前主题 */
public void setTheme(Activity ctx) {
int theme = USharedPref.get().getInteger(PRE_THEME_MODEL);
ThemeEnum themeEnum = ThemeEnum.valueOf(theme);
ctx.setTheme(themeEnum.getTheme());
}
/** * 更改主题 */
public void changeNight(Activity ctx, ThemeEnum themeEnum) {
ctx.setTheme(themeEnum.getTheme());
showAnimation(ctx);
refreshUI(ctx);
USharedPref.get().put(PRE_THEME_MODEL, themeEnum.getTheme());
}
设置主题和更改当前的主题,当然我们还需要在style中定义多套主题。
蓝色调主题
红色调主题
多套主题的定义,当调用更改主题时
/** * 更改主题 */
public void changeNight(Activity ctx, ThemeEnum themeEnum) {
ctx.setTheme(themeEnum.getTheme());
showAnimation(ctx);
refreshUI(ctx);
USharedPref.get().put(PRE_THEME_MODEL, themeEnum.getTheme());
}
开始了一个过渡动画,然后进行页面的UI刷新,
/** * 刷新UI界面 */
private void refreshUI(Activity ctx) {
refreshStatusBar(ctx);
for (SkinView skinView : mSkinViewList) {
skinView.apply();
}
}
public void apply() {
if (view == null) {
return;
}
if (view instanceof SkinCompatSupportable) {
((SkinCompatSupportable) view).applySkin();
}
if (attrs == null) {
return;
}
for (SkinAttr attr : attrs) {
attr.apply(view);
}
}
继续调用枚举中apply这个抽象方法
public void apply(View view) {
attrType.apply(view, attrName, attrValueResName, attrValueTypeName);
}
每个属性下实现各自的apply更改UI的实现
背景色的修改
BACKGROUD("background") {
@Override
public void apply(View view, String attrName, String attrValueResName, String attrValueTypeName) {
Context context = view.getContext();
view.setBackground(ThemeUtils.getDrawable(context, getResId(context, attrName)));
}
},
字体颜色
COLOR("textColor") {
@Override
public void apply(View view, String attrName, String attrValueResName, String attrValueTypeName) {
Context context = view.getContext();
((TextView) view).setTextColor(ThemeUtils.getColorStateList(context, getResId(context, attrName)));
}
},
图片
SRC("src") {
@Override
public void apply(View view, String attrName, String attrValueResName, String attrValueTypeName) {
Context context = view.getContext();
((ImageView) view).setImageDrawable(ThemeUtils.getDrawable(context, getResId(context, attrName)));
}
},
这样一个简单的换肤就实现完成啦,具体的细节可以到代码中查看。
https://github.com/zguop/Towards 存在于wt_library下 theme包中。
关于自定义view设置的自定义属性设置换肤属性,可以实现SkinCompatSupportable接口来更改自定义属性的,来调用自己view中的方法。
public interface SkinCompatSupportable {
void applySkin();
}