Android 主题切换



介绍

所谓的多主题切换,就是能够根据不同的设定,呈现不同风格的界面给用户。想实现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();
}