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">#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>
之后在 styles.xml 中定义两组主题,也就是日间主题和夜间主题:
在主题中的 mainBackground
属性是我们自定义的属性,用来表示背景色:
接下来就是看一下布局 activity_main.xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<? 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" >
< 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 >
|
在 <RelativeLayout> 的 android:background
属性中,我们使用 "?attr/mainBackground" 来表示,这样就代表着RelativeLayout
的背景色会去引用在主题中事先定义好的mainBackground
属性的值。这样就实现了日间/夜间模式切换的换色了。
最后就是 MainActivity
的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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" );
}
}
|
在 MainActivity
中有几点要注意一下:
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 :
1
2
3
4
5
6
7
8
|
<? 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/colors.xml 之外,我们还要创建一个 values-night/colors.xml 文件,用来设置夜间模式的颜色,其中 <color> 的 name 必须要和 values/colors.xml 中的相对应:
1
2
3
4
5
6
7
8
|
<? 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 >
|
在 styles.xml 中去引用我们在 colors.xml 中定义好的颜色:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
< 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 >
|
activity_main.xml 布局的内容和上面 setTheme()
方法中的相差无几,这里就不贴出来了。之后的事情就变得很简单了,在 MyApplication 中先选择一个默认的 Mode :
1
2
3
4
5
6
7
8
9
10
11
|
public class MyApplication extends Application {
@Override
public void onCreate() {
super .onCreate();
// 默认设置为日间模式
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_NO);
}
}
|
要注意的是,这里的 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
当用户点击按钮切换日/夜间时,重新去设置相应的 Mode :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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();
}
});
}
}
|
==========================================================
就前两种方法而言,配置比较简单,最后的实现效果也都基本上是一样的。但是缺点就是需要调用 recreate()
使之生效。而让 Activity 重新创建就必须涉及到一些状态的保存。这就增加了一些难度。所以,我们一起来看看第三种解决方法。
通过资源 id 映射,回调接口
第三种方法的思路就是根据设置的主题去动态地获取资源 id 的映射,然后使用回调接口的方式让 UI 去设置相关的属性值。我们在这里先规定一下:夜间模式的资源在命名上都要加上后缀 “_night” ,比如日间模式的背景色命名为 color_background ,那么相对应的夜间模式的背景资源就要命名为 color_background_night 。好了,下面就是我们的 Demo 所需要用到的 colors.xml :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<? 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 >
|
可以看到每一项 color 都会有对应的 “_night” 与之匹配。
看到这里,肯定有人会问,为什么要设置对应的 “_night” ?到底是通过什么方式来设置日/夜间模式的呢?下面就由 ThemeManager 来为你解答:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
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 。然后根据资源类型和资源名称去获取缓存。如果没有缓存,那么就要动态获取资源了。这里使用方法的是
1
|
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 了。
ThemeManager 中剩下的代码应该都是比较简单的,相信大家都可以看得懂了。
现在我们来看看 MainActivity 的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
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 。
========================================================================================
1.在 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="nightColorPrimary">#3b3b3b</color> <color name="nightColorPrimaryDark">#383838</color> <color name="nightColorAccent">#a72b55</color> </resources>
2.之后在 styles.xml 中定义两组主题,也就是日间主题和夜间主题:
3.在主题中的 mainBackground
属性是我们自定义的属性,用来表示背景色: