声明
整理自http://www.jb51.net/article/93852.htm
本文中给出三种切换日间/夜间模式的方案
- 用 setTheme 的方法让 Activity 重新设置主题;
- 设置 Android Support Library 中的 UiMode 来支持日间/夜间模式的切换;
- 通过资源 id 映射,回调自定义 ThemeChangeListener 接口来处理日间/夜间模式的切换。
使用setTheme方式,定义多种style,切换Activity的style
在color中定义两组颜色如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#3F51B5</color> <color name="colorPrimaryDark">#303F9F</color> <color name="colorAccent">#FF4081</color> <color name="nightColorPrimary">#3b3b3b</color> <color name="nightColorPrimaryDark">#383838</color> <color name="nightColorAccent">#a72b55</color> </resources>
在style中定义两组主题:
<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>
在主题中定义的manBackground属性是我们自定义的属性,可以在View中进行着色,但要在attr中进行声明
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="mainBackground" format="color|reference"></attr> </resources>
在布局中使用属性作为颜色值:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/mainBackground" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.yuqirong.themedemo.MainActivity"> </RelativeLayout>
在需要的地方使用如下代码即可:
theme = (theme == R.style.AppTheme) ? R.style.NightAppTheme : R.style.AppTheme; MainActivity.this.recreate();
注意:
在 MainActivity 中有几点要注意一下:
- 调用 recreate() 方法后 Activity 的生命周期会调用 onSaveInstanceState(Bundle outState) 来备份相关的数据,之后也会调用 onRestoreInstanceState(Bundle savedInstanceState) 来还原相关的数据,因此我们把 theme 的值保存进去,以便 Activity 重新创建后使用。
- 我们在 onCreate(Bundle savedInstanceState) 方法中还原得到了 theme 值后,setTheme() 方法一定要在 setContentView() 方法之前调用,否则的话就看不到效果了。
- recreate() 方法是在 API 11 中添加进来的,所以在 Android 2.X 中使用会抛异常。
`
使用UIMode方法,类似我们对于不同安卓版本的xml界面兼容
- 创建两个不同版本的xml文件,即如下为values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="textColor">#FF000000</color>
<color name="backgroundColor">#FFFFFF</color>
</resources>
- values-night/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>
- 在对应的地方调用如下
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();
要注意的是,这里的 Mode 有四种类型可以选择:
1、MODE_NIGHT_NO: 使用亮色(light)主题,不使用夜间模式;
2、MODE_NIGHT_YES:使用暗色(dark)主题,使用夜间模式;
3、MODE_NIGHT_AUTO:根据当前时间自动切换 亮色(light)/暗色(dark)主题;
4、MODE_NIGHT_FOLLOW_SYSTEM(默认选项):设置为跟随系统,通常为 MODE_NIGHT_NO
根据资源id映射,回调接口
在这里先规定一下:夜间模式的资源在命名上都要加上后缀 “_night” ,比如日间模式的背景色命名为 color_background ,那么相对应的夜间模式的背景资源就要命名为 color_background_night 。好了,下面就是我们的 Demo 所需要用到的 colors.xml :
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#3F51B5</color> <color name="colorPrimary_night">#3b3b3b</color> <color name="colorPrimaryDark">#303F9F</color> <color name="colorPrimaryDark_night">#383838</color> <color name="colorAccent">#FF4081</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>
自定义TheManager
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 了。
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 。也许有人会说和前两种方法的效果没什么差异啊,但是仔细看就会发现前面两种方法在切换模式的瞬间会有短暂黑屏现象存在,而第三种方法没有。这是因为前两种方法都要调用 recreate() 。而第三种方法不需要 Activity 重新创建,使用回调的方法来实现。到了这里,按照套路应该是要总结的时候了。那么就根据上面给的三种方法来一个简单的对比吧:
setTheme 方法:可以配置多套主题,比较容易上手。除了日/夜间模式之外,还可以有其他五颜六色的主题。但是需要调用 recreate() ,切换瞬间会有黑屏闪现的现象;
UiMode 方法:优点就是 Android Support Library 中已经支持,简单规范。但是也需要调用 recreate() ,存在黑屏闪现的现象;
动态获取资源 id ,回调接口:该方法使用起来比前两个方法复杂,另外在回调的方法中需要设置每一项 UI 相关的属性值。但是不需要调用 recreate() ,没有黑屏闪现的现象。