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();
}