Android 夜间模式系列笔记(二)通过更换主题实现夜间模式

    上一篇文章
    Android 夜间模式系列笔记(一)AppCompatDelegate

    介绍了如何利用系统提供的Theme.AppCompat.DayNight主题,来实现应用夜间模式,使用这种能很方便的实现夜间模式,但是缺陷也很明显,每次都需要调用recreate重新启动activity。

     这次介绍另外一种实现方式,可以不重启activity,就能实现夜间模式。

     大体思路是给控制设置两套主题(白天/夜间),在切换主题时,会从跟view开始遍历各个控件,然后根据控件的主题id找到TypedArray对象,再拿到对应的属性值,重新给组件赋值。

     具体实现:

(1)自定义主题参数

     在attrs.xml中,声明"theme_default","theme_night"表示白天,夜间主题

     "dayNightStyle"用于在xml使用我们自定义的白天夜间的主题

     

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="DayNightTheme">
        <attr name="theme_default" format="reference" />
        <attr name="theme_night" format="reference" />
    </declare-styleable>

    <declare-styleable name="ThemeAll">
        <attr name="dayNightStyle" format="reference" />
    </declare-styleable>
</resources>
(2)声明夜间模式相关的接口

    

public interface DayNightThemeInterface {

    static final String THEME_NIGHT = "night"; //现用于夜间模式。
    static final String THEME_DEFAULT = "default"; //普通模式。
    void applyTheme(String whichTheme);
    void addTheme(String whichTheme, int styleId);

}
(3)自定义view,实现上面的接口

     因为实现的方案是通过遍历view依次重新根据设置的日间/白天模式的style来重新设置各种属性值的,所以我们需要自定义view,实现上面的接口,来达到实现的效果。

    比如最简单的TextView

     

public class DayNightTextView extends TextView implements DayNightThemeInterface{

    private String mCurrentTheme;
    private HashMap<String, Integer> mThemeSet = new HashMap<String, Integer>(5);

    public DayNightTextView(Context context) {
        super(context);
        init(context, null, 0);
    }

    public DayNightTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, R.attr.dayNightStyle);
    }

    public DayNightTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyle) {
        ThemeUtils.applyStyle_TextView(this, 0);
        int[] defaultAndCustomTheme = new int[] {0, 0};
        if (ThemeUtils.getTheme_DayNightView(context, attrs, defStyle, defaultAndCustomTheme)) {
            addTheme(THEME_DEFAULT, defaultAndCustomTheme[0]);
            addTheme(THEME_NIGHT, defaultAndCustomTheme[1]);
            applyTheme(CommonSettings.getCurrentTheme());
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        applyTheme(CommonSettings.getCurrentTheme());
    }

    @Override
    public void onFinishTemporaryDetach() {
        super.onFinishTemporaryDetach();
        applyTheme(CommonSettings.getCurrentTheme());
    }

    @Override
    public void applyTheme(String whichTheme) {
        if (whichTheme.equals(mCurrentTheme)) {
            return;
        }
        mCurrentTheme = whichTheme;
        int styleId = 0;
        Integer tmp = mThemeSet.get(mCurrentTheme);
        if (tmp != null && tmp != 0) {
            styleId = tmp;
        }
        if (styleId != 0) {
            ThemeUtils.applyStyle_View(this, styleId);
            ThemeUtils.applyStyle_TextView(this, styleId);
        }
    }

    @Override
    public void addTheme(String whichTheme, int styleId) {
        mThemeSet.put(whichTheme, styleId);
    }
}
      在初始话的时候先把设置的日间/夜间的主题保存出来

     addTheme(THEME_DEFAULT, defaultAndCustomTheme[0]);
     addTheme(THEME_NIGHT, defaultAndCustomTheme[1]);

    然后切换日夜间模式的时候,会调用applyTheme接口,根据缓存的主题,拿到对应的主题,再根据主题去拿控件的属性值,重新设置

(4)日夜间主题的取出

      首先我们设置主题的方式为:

     在styles.xml中声明主题(主要以DayNightTextView举例)

<style name="text_color_theme_day">
        <item name="android:textColor">#000000</item>
    </style>
    <style name="text_color_theme_night">
        <item name="android:textColor">#ffffff</item>
    </style>
    <style name="text_color_theme">
        <item name="theme_default">@style/text_color_theme_day</item>
        <item name="theme_night">@style/text_color_theme_night</item>
    </style>
     这里声明了theme_default(白天)和theme_night(夜间)主题

    然后在xml中,设置给DayNightTextView    

<com.muzi.nightmode.widget.DayNightTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:dayNightStyle="@style/text_color_theme" />
    在第三点中,我们可以看到设置的日夜间主题是在控件初始化的时候就取出来缓存了的,下面是具体怎么取出来的实现。

   

/**
         *
         * 第一种情况:
         * 从这里开始到 a.recycle() 方法,适用于在 xml 中以下列方式声明的属性
         * -----------
         * app:theme_default="@style/sampleStyleDay"
         * app:theme_night="@style/sampleStyleNight"
         * -----------
         *
         */
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                R.styleable.DayNightTheme, defStyle, 0);
        int theme_default = a.getResourceId(R.styleable.DayNightTheme_theme_default, 0);
        int theme_night = a.getResourceId(R.styleable.DayNightTheme_theme_night, 0);
        a.recycle();

        /**
         *
         * 第二种情况:
         * 如果在布局文件 xml 标签中没有全部声明 theme_default 和 theme_night,那么会尝试读取 dayNightStyle 声明的属性
         * (这也是我们最常使用的方式)
         *
         * -----------
         * <! -- 布局文件 xml 中声明 dayNightStyle 属性,它是一个 style,在 style.xml 中定义  -->
         * app:dayNightStyle="@style/sampleStyle"
         *
         * <!- style.xml 中的声明 -->
         * <style name="sampleStyleDay">
         *      <item name="android:background">#ffffff</item>
         * </style>
         * <style name="sampleStyleNight">
         *      <item name="android:background">#99ffffff</item>
         * </style>
         * <style name="sampleStyle">
         *      <item name="theme_default">@style/sampleStyleDay</item>
         *      <item name="theme_custom">@style/sampleStyleNight</item>
         * </style>
         * -----------
         * 解析分为两个步骤:
         * (1) 先解析 self:dayNightStyle 中的属性,它指向 sampleStyle 引用
         * (2) 从 sampleStyle 分别获取 sampleStyleDay 和 sampleStyleNight 这两个白天/夜间模式的主题。
         *
         */
        if (theme_default == 0 || theme_night == 0) {
            a = context.getTheme().obtainStyledAttributes(attrs,
                    new int[]{R.attr.dayNightStyle}, defStyle, 0);
            int themeRef = a.getResourceId(0, 0);
            a.recycle();
            if (themeRef != 0) {
                a = context.getTheme().obtainStyledAttributes(themeRef,
                        R.styleable.DayNightTheme);
                if (theme_default == 0) {
                    theme_default = a.getResourceId(R.styleable.DayNightTheme_theme_default, 0);
                }
                if (theme_night == 0) {
                    theme_night = a.getResourceId(R.styleable.DayNightTheme_theme_night, 0);
                }
                a.recycle();
            }
        }

        defaultAndNightTheme[0] = theme_default;
        defaultAndNightTheme[1] = theme_night;
        return theme_default != 0 || theme_night != 0;
    }

 (5)在Activity中,切换日夜间模式时,需要从rootview开始遍历view,然后调用其applyTheme方法(其他不可见的view,在attachWindow的时候会调用)

       

private void applyActivityTheme(boolean isNight) {
        getTheme().applyStyle(isNight ? R.style.AppThemeNight : R.style.AppThemeDay, true);
    }

    private void applyViewTheme(View view, String currentTheme) {
        if (view == null) {
            return;
        }
        if (view instanceof DayNightThemeInterface) {
            ((DayNightThemeInterface) view).applyTheme(currentTheme);
        }
        if (view instanceof ViewGroup) {
            ViewGroup group = (ViewGroup)view;
            for (int i = 0, count = group.getChildCount(); i < count; i++) {
                applyViewTheme(group.getChildAt(i), currentTheme);
            }
        }
    }

    private void toggleNightMode(boolean isNight) {
        applyActivityTheme(isNight);
        View rootView = mBtnToggleNightMode.getRootView();
        String currentTheme = CommonSettings.getCurrentTheme();
        applyViewTheme(rootView, currentTheme);
    }
(6) applyTheme方法的具体实现

   
public void applyTheme(String whichTheme) {
        if (whichTheme.equals(mCurrentTheme)) {
            return;
        }
        mCurrentTheme = whichTheme;
        int styleId = 0;
        Integer tmp = mThemeSet.get(mCurrentTheme);
        if (tmp != null && tmp != 0) {
            styleId = tmp;
        }
        if (styleId != 0) {
            ThemeUtils.applyStyle_View(this, styleId);
            ThemeUtils.applyStyle_TextView(this, styleId);
        }
    }
     ThemeUtils.applyStyle_View的实现      

/**
     * STYLEABLE_ARRAY_View 是 View 的属性集合,通过反射获取到 com.android.internal.R$styleable 类,
     * 再通过该类获取到其内部与 View 相关的 attr 属性,com.android.internal.R 中除了 styleable 外,还包含了其它的类
     * 包括 attr/id/string/style/styleable,都可以通过反射来获取。
     *
     * @param v 需要应用属性的 View。
     * @param styleId 主题的 styleId,对应 theme_default/theme_night 的主题。
     *
     */
    public static void applyStyle_View(View v, int styleId) {
        TypedArray a = v.getContext().getTheme().obtainStyledAttributes(styleId,
                STYLEABLE_ARRAY_View);
        int N = a.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            if (attr == STYLEABLE_View_background) {
                Drawable backgroundDr = a.getDrawable(attr);
                v.setBackground(backgroundDr);
            } else if (attr == STYLEABLE_View_paddingLeft) {
                v.setPadding(a.getDimensionPixelOffset(attr, 0), v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom());
            } else if (attr == STYLEABLE_View_alpha) {
                v.setAlpha(a.getFloat(attr, 1));
            }
        }
        a.recycle();
    }

    ThemeUtils.applyStyle_TextView(this, styleId)的实现

public static void applyStyle_TextView(TextView v, int styleId) {
        TypedArray a = v.getContext().getTheme().obtainStyledAttributes(styleId,
                STYLEABLE_ARRAY_TextView);
        int N = a.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            if (attr == STYLEABLE_TextView_textSize) {
                int textSize = a.getDimensionPixelSize(attr, 0);
                if (textSize != 0) {
                    v.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
                }
            } else if (attr == STYLEABLE_TextView_textColor) {
                ColorStateList textColor = a.getColorStateList(attr);
                v.setTextColor(textColor);
            } else if (attr == STYLEABLE_TextView_textColorHint) {
                ColorStateList textColorHint = a.getColorStateList(attr);
                v.setHintTextColor(textColorHint);
            } else if (attr == STYLEABLE_TextView_privateImeOptions) {
                String imeOpt = a.getString(attr);
                v.setPrivateImeOptions(imeOpt);
            } else if(attr == STYLEABLE_TextView_textCursorDrawable){
                int  mCursorDrawableRes = a.getResourceId(attr, 0);
                if(mCursorDrawableRes != 0){
                    setCursorDrawable(v,mCursorDrawableRes);
                }
            }
        }
        a.recycle();
    }

     到此,基本实现就完了,可能有些东西说的不是很清楚,可以具体看demo

(7)总结

      通过这种方式可以避免重启activity,但是会显得相对繁琐,需要自定义多套主题,需要自定义view实现对应的接口,而且由于
 com.android.internal.R$styleable类被系统隐藏了,不能直接调用到,所以需要大量的反射来获取view的attr属性


  demo路径:https://github.com/swustmuzi/PNightMode
    
     
          



猜你喜欢

转载自blog.csdn.net/qqwuy_muzi/article/details/79043790