Android中夜间模式的三种实现方式

参考:https://www.jianshu.com/p/f3aaed57fa15

在本篇文章中给出了三种实现日间/夜间模式切换的方案:

  1. 使用 setTheme 的方法让 Activity 重新设置主题;
  2. 设置 Android Support Library 中的 UiMode 来支持日间/夜间模式的切换;
  3. 通过资源 id 映射,回调自定义 ThemeChangeListener 接口来处理日间/夜间模式的切换。

 一、使用 setTheme 方法

我们先来看看使用 setTheme 方法来实现日间/夜间模式切换的方案。这种方案的思路很简单,就是在用户选择夜间模式时,Activity 设置成夜间模式的主题,之后再让 Activity 调用 recreate() 方法重新创建一遍就行了。 

第一步:在 colors.xml 中定义两组颜色,分别表示日间和夜间的主题色。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 日间主题色 -->
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
    <!-- 夜间主题色 -->
    <color name="nightColorPrimary">#3b3b3b</color>
    <color name="nightColorPrimaryDark">#383838</color>
    <color name="nightColorAccent">#a72b55</color>
</resources>

第二步:在 styles.xml 中定义两组主题,也就是日间主题和夜间主题。

<resources>

    <!-- Base application theme. 日间主题 -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:textColor">@android:color/black</item>
        <item name="mainBackground">@android:color/white</item>
    </style>

    <!-- 夜间主题 -->
    <style name="NightAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/nightColorPrimary</item>
        <item name="colorPrimaryDark">@color/nightColorPrimaryDark</item>
        <item name="colorAccent">@color/nightColorAccent</item>
        <item name="android:textColor">@android:color/white</item>
        <item name="mainBackground">@color/nightColorPrimaryDark</item>
    </style>

</resources>

第三步:在主题中的 mainBackground 属性是我们自定义的属性,用来表示背景色。所以我们需要在values里创建一个attrs.xml。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="mainBackground" format="color|reference"></attr>
</resources>

第四步:在布局中调用mainBackground属性。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="?attr/mainBackground"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_theme"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="切换日/夜间模式" />

</LinearLayout>

<LinearLayout>android:background 属性中,我们使用 "?attr/mainBackground" 来表示,这样就代表着 LinearLayout 的背景色会去引用在主题中事先定义好的 mainBackground 属性的值。这样就实现了日间/夜间模式切换的换色了。

第五步:在Activity中的配置。

public class MainActivity extends AppCompatActivity {

    // 默认是日间模式
    private int theme = R.style.AppTheme;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 判断是否有主题存储
        if(savedInstanceState != null){
            theme = savedInstanceState.getInt("theme");
            setTheme(theme);
        }
        setContentView(R.layout.activity_main);
        Button btn_theme = (Button) findViewById(R.id.btn_theme);
        btn_theme.setOnClickListener(new View.OnClickListener() {

            @Override public void onClick(View v) {
                theme = (theme == R.style.AppTheme) ? R.style.NightAppTheme : R.style.AppTheme;
                MainActivity.this.recreate();
            }
        });
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("theme", theme);
    }

    @Override protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        theme = savedInstanceState.getInt("theme");
    }

}

在 Activity 中有几点要注意一下:

  1. 调用 recreate() 方法后 Activity 的生命周期会调用 onSaveInstanceState(Bundle outState) 来备份相关的数据,之后也会调用 onRestoreInstanceState(Bundle savedInstanceState) 来还原相关的数据,因此我们把 theme 的值保存进去,以便 Activity 重新创建后使用。

  2. 我们在 onCreate(Bundle savedInstanceState) 方法中还原得到了 theme 值后,setTheme() 方法一定要在 setContentView() 方法之前调用,否则的话就看不到效果了。

  3. recreate() 方法是在 API 11 中添加进来的,所以在 Android 2.X 中使用会抛异常。

 二、使用 Android Support Library 中的 UiMode 方法

使用 UiMode 的方法也很简单,我们需要把 colors.xml 定义为日间/夜间两种。之后根据不同的模式会去选择不同的 colors.xml 。在 Activity 调用 recreate() 之后,就实现了切换日/夜间模式的功能。

第一步:在values/colors.xml配置。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
    <color name="textColor">#FF000000</color>
    <color name="backgroundColor">#FFFFFF</color>
</resources>

第二步:除了 values/colors.xml 之外,我们还要创建一个 values-night/colors.xml 文件,用来设置夜间模式的颜色,其中 <color> 的 name 必须要和 values/colors.xml 中的相对应。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3b3b3b</color>
    <color name="colorPrimaryDark">#383838</color>
    <color name="colorAccent">#a72b55</color>
    <color name="textColor">#FFFFFF</color>
    <color name="backgroundColor">#3b3b3b</color>
</resources>

第三步:在values里创建一个attrs.xml。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="mainBackground" format="color|reference"></attr>
</resources>

第四步:在 styles.xml 中去引用我们在 colors.xml 中定义好的颜色。

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:textColor">@color/textColor</item>
        <item name="mainBackground">@color/backgroundColor</item>
    </style>

</resources>

第五步:在布局中调用mainBackground属性。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="?attr/mainBackground"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_theme"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="切换日/夜间模式" />

</LinearLayout>

第六步:创建一个Application先选择一个默认的 Mode。

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 默认设置为日间模式
        AppCompatDelegate.setDefaultNightMode(
                AppCompatDelegate.MODE_NIGHT_NO);
    }
}

要注意的是,这里的 Mode 有四种类型可以选择:

  • MODE_NIGHT_NO: 使用亮色(light)主题,不使用夜间模式;
  • MODE_NIGHT_YES:使用暗色(dark)主题,使用夜间模式;
  • MODE_NIGHT_AUTO:根据当前时间自动切换 亮色(light)/暗色(dark)主题;
  • MODE_NIGHT_FOLLOW_SYSTEM(默认选项):设置为跟随系统,通常为 MODE_NIGHT_NO

第七步 :在Activity中控制调用。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_theme = (Button) findViewById(R.id.btn_theme);
        btn_theme.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
                getDelegate().setLocalNightMode(currentNightMode == Configuration.UI_MODE_NIGHT_NO ? AppCompatDelegate.MODE_NIGHT_YES :
                        AppCompatDelegate.MODE_NIGHT_NO); // 同样需要调用recreate方法使之生效 recreate();
                }
        });
    }
}

三、通过资源 id 映射,回调接口

第三种方法的思路就是根据设置的主题去动态地获取资源 id 的映射,然后使用回调接口的方式让 UI 去设置相关的属性值。我们在这里先规定一下:夜间模式的资源在命名上都要加上后缀 “_night” ,比如日间模式的背景色命名为 color_background ,那么相对应的夜间模式的背景资源就要命名为 color_background_night 。

第一步:在colors.xml中配置。

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <color name="colorPrimary">#008577</color>
    <color name="colorPrimary_night">#3b3b3b</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorPrimaryDark_night">#383838</color>
    <color name="colorAccent">#D81B60</color>
    <color name="colorAccent_night">#a72b55</color>
    <color name="textColor">#FF000000</color>
    <color name="textColor_night">#FFFFFF</color>
    <color name="backgroundColor">#FFFFFF</color>
    <color name="backgroundColor_night">#3b3b3b</color>

</resources>

 第二步:创建ThemeManager类。

public class ThemeManager {

    // 默认是日间模式
    private static ThemeMode mThemeMode = ThemeMode.DAY;
    // 主题模式监听器
    private static List<OnThemeChangeListener> mThemeChangeListenerList = new LinkedList<>();
    // 夜间资源的缓存,key : 资源类型, 值<key:资源名称, value:int值>
    private static HashMap<String, HashMap<String, Integer>> sCachedNightResrouces = new HashMap<>();
    // 夜间模式资源的后缀,比如日件模式资源名为:R.color.activity_bg, 那么夜间模式就为 :R.color.activity_bg_night
    private static final String RESOURCE_SUFFIX = "_night";

    /**
     * 主题模式,分为日间模式和夜间模式
     */
    public enum ThemeMode {
        DAY, NIGHT
    }

    /**
     * 设置主题模式
     *
     * @param themeMode
     */
    public static void setThemeMode(ThemeMode themeMode) {
        if (mThemeMode != themeMode) {
            mThemeMode = themeMode;
            if (mThemeChangeListenerList.size() > 0) {
                for (OnThemeChangeListener listener : mThemeChangeListenerList) {
                    listener.onThemeChanged();
                }
            }
        }
    }

    /**
     * 根据传入的日间模式的resId得到相应主题的resId,注意:必须是日间模式的resId
     * * *
     * @param dayResId 日间模式的resId
     * @return 相应主题的resId,若为日间模式,则得到dayResId;反之夜间模式得到nightResId
     */
    public static int getCurrentThemeRes(Context context, int dayResId) {
        if (getThemeMode() == ThemeMode.DAY) {
            return dayResId;
        }
        // 资源名
        String entryName = context.getResources().getResourceEntryName(dayResId);
        // 资源类型
        String typeName = context.getResources().getResourceTypeName(dayResId);
        HashMap<String, Integer> cachedRes = sCachedNightResrouces.get(typeName);
        // 先从缓存中去取,如果有直接返回该id
        if (cachedRes == null) {
            cachedRes = new HashMap<>();
        }
        Integer resId = cachedRes.get(entryName + RESOURCE_SUFFIX);
        if (resId != null && resId != 0) {
            return resId;
        } else {
            //如果缓存中没有再根据资源id去动态获取
            try {
                // 通过资源名,资源类型,包名得到资源int值
                int nightResId = context.getResources().getIdentifier(entryName + RESOURCE_SUFFIX, typeName, context.getPackageName());
                // 放入缓存中
                cachedRes.put(entryName + RESOURCE_SUFFIX, nightResId);
                sCachedNightResrouces.put(typeName, cachedRes);
                return nightResId;
            } catch (Resources.NotFoundException e) {
                e.printStackTrace();
            }
        }
        return 0;
    }

    /**
     * 注册ThemeChangeListener
     *
     * @param listener
     */
    public static void registerThemeChangeListener(OnThemeChangeListener listener) {
        if (!mThemeChangeListenerList.contains(listener)) {
            mThemeChangeListenerList.add(listener);
        }
    }

    /**
     * 反注册ThemeChangeListener
     *
     * @param listener
     */
    public static void unregisterThemeChangeListener(OnThemeChangeListener listener) {
        if (mThemeChangeListenerList.contains(listener)) {
            mThemeChangeListenerList.remove(listener);
        }
    }

    /**
     * 得到主题模式
     *
     * @return
     */

    public static ThemeMode getThemeMode() {
        return mThemeMode;
    }

    /**
     * 主题模式切换监听器
     */
    public interface OnThemeChangeListener {
        /**
         * 主题切换时回调
         */
        void onThemeChanged();
    }
}

上面 ThemeManager 的代码基本上都有注释,想要看懂并不困难。其中最核心的就是 getCurrentThemeRes 方法了。在这里解释一下 getCurrentThemeRes 的逻辑。参数中的 dayResId 是日间模式的资源id,如果当前主题是日间模式的话,就直接返回 dayResId 。反之当前主题为夜间模式的话,先根据 dayResId 得到资源名称和资源类型。比如现在有一个资源为 R.color.colorPrimary ,那么资源名称就是 colorPrimary ,资源类型就是 color 。然后根据资源类型和资源名称去获取缓存。如果没有缓存,那么就要动态获取资源了。这里使用方法的是

context.getResources().getIdentifier(String name, String defType, String defPackage)
  • name 参数就是资源名称,不过要注意的是这里的资源名称还要加上后缀 “_night” ,也就是上面在 colors.xml 中定义的名称;
  • defType 参数就是资源的类型了。比如 color,drawable等;
  • defPackage 就是资源文件的包名,也就是当前 APP 的包名。

有了上面的这个方法,就可以通过 R.color.colorPrimary 资源找到对应的 R.color.colorPrimary_night 资源了。最后还要把找到的夜间模式资源加入到缓存中。这样的话以后就直接去缓存中读取,而不用再次去动态查找资源 id 了。

第三步:布局。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/relativeLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_theme"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="切换日/夜间模式" />

    <TextView
        android:id="@+id/tv"
        android:layout_below="@id/btn_theme"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:text="通过setTheme()的方法" />

</RelativeLayout>

第三步:在Activity中控制显示。

public class MainActivity extends AppCompatActivity implements ThemeManager.OnThemeChangeListener {

    private TextView tv;
    private Button btn_theme;
    private RelativeLayout relativeLayout;
    private ActionBar supportActionBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ThemeManager.registerThemeChangeListener(this);
        supportActionBar = getSupportActionBar();
        btn_theme = (Button) findViewById(R.id.btn_theme);
        relativeLayout = (RelativeLayout) findViewById(R.id.relativeLayout);
        tv = (TextView) findViewById(R.id.tv);
        btn_theme.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                ThemeManager.setThemeMode(ThemeManager.getThemeMode() == ThemeManager.ThemeMode.DAY ? ThemeManager.ThemeMode.NIGHT : ThemeManager.ThemeMode.DAY);
            }
        });
    }

    public void initTheme() {
        tv.setTextColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.textColor)));
        btn_theme.setTextColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.textColor)));
        relativeLayout.setBackgroundColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.backgroundColor)));
        // 设置标题栏颜色
        if(supportActionBar != null){
            supportActionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.colorPrimary))));
        }
        // 设置状态栏颜色
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = getWindow();
            window.setStatusBarColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.colorPrimary)));
        }
    }

    @Override
    public void onThemeChanged() {
        initTheme();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ThemeManager.unregisterThemeChangeListener(this);
    }
}

在 MainActivity 中实现了 OnThemeChangeListener 接口,这样就可以在主题改变的时候执行回调方法。然后在 initTheme() 中去重新设置 UI 的相关颜色属性值。还有别忘了要在 onDestroy() 中移除 ThemeChangeListener 。

四、总结

  • setTheme 方法:可以配置多套主题,比较容易上手。除了日/夜间模式之外,还可以有其他五颜六色的主题。但是需要调用 recreate() ,切换瞬间会有黑屏闪现的现象;

  • UiMode 方法:优点就是 Android Support Library 中已经支持,简单规范。但是也需要调用 recreate() ,存在黑屏闪现的现象;

  • 动态获取资源 id ,回调接口:该方法使用起来比前两个方法复杂,另外在回调的方法中需要设置每一项 UI 相关的属性值。但是不需要调用 recreate() ,没有黑屏闪现的现象。

猜你喜欢

转载自blog.csdn.net/qq_40441190/article/details/83421132
今日推荐