1. 程式人生 > >Android 主題切換

Android 主題切換

介紹

所謂的多主題切換,就是能夠根據不同的設定,呈現不同風格的介面給使用者。想實現Android多套主題的切換,網路上方案已經很多了,也看了許多大神的實現方式,但心裡總想著自己去實現一遍,就這麼借鑑GitHub的開源實現了一個簡單的Android換膚框架。

實現的思路

通過LayoutInflaterCompat.setFactory方式,在onCreateView的回撥中,解析每一個View的attrs, 判斷是否有已標記需要換膚的屬性, 比方說background, textColor, 或者說相應資源是否為skin_開頭等等.然後儲存到集合中, 將相應的屬性收集到一起。 這種方式相對是比較簡單的,易於實現的方式。於是我也採用了這種方式去捉摸一番。

最後實現的效果

這裡寫圖片描述

一張圖瞭解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<SkinAttr> 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<SkinAttr> 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<SkinAttr> getSkinAttrs(AttributeSet attrs, Context context) {
        List<SkinAttr> 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<SkinAttr> attrs;

    public SkinView(View view, List<SkinAttr> 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中定義多套主題。

藍色調主題
 <style name="AppThemeBlue" parent="AppTheme">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorBlue</item>
        <item name="colorPrimaryDark">@color/colorBlue</item>
        <item name="colorAccent">@color/colorBlue</item>
        <!--白色字型-->
        <item name="skin_kind_color_not">@color/colorWhite</item>
        <!--相近暗淡色-->
        <item name="skin_home_title_not">@color/home_title3</item>
        <!--dialog背景色-->
        <item name="skin_dialog_bg_not">@color/color_ef6f26</item>
        <!--相反色-->
        <item name="skin_contrast_color_not">@color/color_ef6f26</item>
        <!--主題透明度色-->
        <item name="skin_transparent_theme_color">@color/color_blue_80</item>
        <!--字型暗色-->
        <item name="skin_text_dark_color">@color/color_373737</item>
        <!--灰色字型-->
        <item name="skin_text_gray_color">@color/color_a7a7a7</item>
        <!--背景圖片-->
        <item name="skin_Background_drawable">@drawable/theme_bg3</item>
    </style>
紅色調主題
 <style name="AppThemeRed" parent="AppTheme">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorRed</item>
        <item name="colorPrimaryDark">@color/colorRed</item>
        <item name="colorAccent">@color/colorRed</item>
        <item name="skin_kind_color_not">@color/colorWhite</item>
        <item name="skin_home_title_not">@color/home_title2</item>
        <item name="skin_dialog_bg_not">@color/color_31c2c5</item>
        <item name="skin_contrast_color_not">@color/color_31c2c5</item>
        <item name="skin_transparent_theme_color">@color/color_red_80</item>
        <item name="skin_text_dark_color">@color/color_373737</item>
        <item name="skin_text_gray_color">@color/color_a7a7a7</item>
        <item name="skin_Background_drawable">@drawable/theme_bg1</item>
    </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();
}