Anroid自定义暗黑模式

前言

在app的日常使用中,也许是为了省电,为了护眼,开发者常常会为用户提供这样一种模式:深色模式\暗黑模式。那么这就涉及到Android中的主题切换。一般Material Components已经为我们提供多种主题方案,但是大多数情况下某些自定义控件或者第三方库的控件颜色依旧需要我们自己去适配。

自定义主题的步骤

①自定义属性

主题换肤和插件换肤的原理其实是一样的,就是控制不同模式下加载对应的资源文件。以往我们在写xml文件的时候,默认的属性赋值都是绝对的,即:

1
android:background="#FFFFFF" 或者android:background="@color/white"

而如果这样子设置控件的属性的话,那么默认的资源加载就会被限制。那么如何让控件的属性实现主题切换时的动态修改呢?下面以修改颜色为例,你只需要在xml文件中这样写:

1
2
3
4
5
6
7
8
<TextView
android:layout_width="wrap_content"
android:text="新闻"
android:textSize="25sp"
android:textStyle="bold"
android:textColor="?attr/custom_attr_text"
android:layout_height="wrap_content">
</TextView>

即书写语法为:?attr/custom_attr_text,这个名字就是你在资源文件中自定义属性的属性名。只要在xml文件中这样子定义,就可实现控件颜色的快速切换。

下面以一些自定义属性为例。首先在res-value目录下新建一个custom_theme_colors.xml资源文件。

自定义颜色属性如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="custom_attr_bg" format="color|reference"/>
<attr name="custom_attr_text" format="color|reference"/>
<attr name="custom_attr_cv1" format="color|reference"/>
<attr name="custom_attr_cv2" format="color|reference"/>
<attr name="custom_attr_cv3" format="color|reference"/>
<attr name="custom_attr_cv4" format="color|reference"/>
<attr name="custom_attr_cv5" format="color|reference"/>
<attr name="custom_attr_cv6" format="color|reference"/>
<attr name="custom_attr_cv7" format="color|reference"/>
<attr name="custom_attr_cv8" format="color|reference"/>
</resources>

这里需要定义多少个控件属性,具体要看你有多少个控件是需要切换主题的时候属性是要改变的。比如说我有三个按钮控件,我在切换主题的时候需要改变颜色,那么你就需要定义3个自定义属性。

name是自己定义的属性名字,后面的@color/xxxx 则是你的资源文件中的color.xml中定义的颜色名字。

②自定义主题的颜色

接着就是自定义主题的颜色资源,你也可以直接通过@color/xxxx 来在下面给对应的自定义属性赋值,也可以新键一个custom_theme_colors.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
<?xml version="1.0" encoding="utf-8"?>
<resources>
//日间模式的颜色
<color name="custom_color_bg_day">@color/white</color>
<color name="custom_color_text_day">@color/white</color>
<color name="custom_cv_day_color1">@color/blue</color>
<color name="custom_cv_day_color2">@color/orange</color>
<color name="custom_cv_day_color3">@color/teal_200</color>
<color name="custom_cv_day_color4">@color/fuchsia</color>
<color name="custom_cv_day_color5">@color/aqua</color>
<color name="custom_cv_day_color6">@color/dodgerblue</color>
<color name="custom_cv_day_color7">@color/lime</color>
<color name="custom_cv_day_color8">@color/pink</color>
//夜间模式的颜色
<color name="custom_color_bg_night">@color/black</color>
<color name="custom_color_text_night">@color/teal_200</color>
<color name="custom_cv_night_color1">@color/purple_500</color>
<color name="custom_cv_night_color2">@color/purple_200</color>
<color name="custom_cv_night_color3">@color/midnightblue</color>
<color name="custom_cv_night_color4">@color/steelblue</color>
<color name="custom_cv_night_color5">@color/grey</color>
<color name="custom_cv_night_color6">@color/darkblue</color>
<color name="custom_cv_night_color7">@color/teal</color>
<color name="custom_cv_night_color8">@color/darkslategray</color>

</resources>

③自定义Theme主题

那么既然是通过切换主题来切换应用的UI样式,所以在定义Style主题样式的时候,需要准备多套主题样式。在res-value目录下新键style属性的资源文件,例如:custom_theme_styles.xml。我们需要在这个文件中新建自定义主题,并对特定的系统、自定义属性进行赋值操作。

格式如下所示:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="自定义主题样式的名称" parent="继承的主题,可以是自定义主题样式也可以是系统主题样式">
<item name="属性名称">赋值的对应资源</item>
</style>
</resources>

具体实例如下:首先我们要有一个父主题Mariotheme,接着我们在定义两个子主题,分别是日间主题Mariotheme.Day和夜间主题Mariotheme.Night。如下所示:

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
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Mariotheme" parent="Theme.AppCompat.Light.NoActionBar">

</style>
//日间模式
<style name="Mariotheme.Day">
<item name="custom_attr_bg">@color/custom_color_bg_day</item>
<item name="custom_attr_text">@color/custom_color_text_day</item>
<item name="custom_attr_cv1">@color/custom_cv_day_color1</item>
<item name="custom_attr_cv2">@color/custom_cv_day_color2</item>
<item name="custom_attr_cv3">@color/custom_cv_day_color3</item>
<item name="custom_attr_cv4">@color/custom_cv_day_color4</item>
<item name="custom_attr_cv5">@color/custom_cv_day_color5</item>
<item name="custom_attr_cv6">@color/custom_cv_day_color6</item>
<item name="custom_attr_cv7">@color/custom_cv_day_color7</item>
<item name="custom_attr_cv8">@color/custom_cv_day_color8</item>
</style>
//夜间模式
<style name="Mariotheme.Night">
<item name="custom_attr_bg">@color/custom_color_bg_night</item>
<item name="custom_attr_text">@color/custom_color_text_night</item>
<item name="custom_attr_cv1">@color/custom_cv_night_color1</item>
<item name="custom_attr_cv2">@color/custom_cv_night_color2</item>
<item name="custom_attr_cv3">@color/custom_cv_night_color3</item>
<item name="custom_attr_cv4">@color/custom_cv_night_color4</item>
<item name="custom_attr_cv5">@color/custom_cv_night_color5</item>
<item name="custom_attr_cv6">@color/custom_cv_night_color6</item>
<item name="custom_attr_cv7">@color/custom_cv_night_color7</item>
<item name="custom_attr_cv8">@color/custom_cv_night_color8</item>
</style>
</resources>

具体解释一下,大家观察上面代码可以发现,我们的属性都是指向同一个,但是两个不同主题的资源赋值颜色不同,那么这就可以实现切换主题使控件改变颜色了。

除了颜色之外,你也可以自定义切换主题后图片的改变,图标的改变,还有shape,selector的改变等。如下:

④自定义drawable

在drawable资源文件下新建资源文件。

①selector

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/custom_color_text_pressed_day" android:state_pressed="true" />
<item android:color="@color/custom_color_text_day" />
</selector>
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/custom_color_text_pressed_night" android:state_pressed="true" />
<item android:color="@color/custom_color_text_night" />
</selector>

②shape

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/custom_color_user_photo_place_holder_bg_day" />
<corners android:radius="32dp" />
</shape>
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/custom_color_user_photo_place_holder_bg_night" />
<corners android:radius="32dp" />
</shape>

⑤在XML布局文件中使用自定义属性

1
android:需要修改的属性="?attr/自定义属性名称" 

Demo代码

下面展示一个页面的具体Demo代码

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:background="?attr/custom_attr_bg"
android:layout_height="match_parent">
<RelativeLayout
android:layout_gravity="center"
android:layout_marginTop="40dp"
android:padding="20dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.skydoves.elasticviews.ElasticCardView
android:id="@+id/cw_read_fr"
app:cardCornerRadius="10dp"
app:cardElevation="20dp"
app:cardView_duration="250"
app:cardBackgroundColor="?attr/custom_attr_cv1"
android:layout_width="150dp"
android:layout_height="250dp">
<LinearLayout
android:layout_width="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_height="wrap_content">
<ImageView
android:layout_width="90dp"
android:layout_height="90dp"
android:src="@drawable/news"/>
<TextView
android:layout_width="wrap_content"
android:text="新闻"
android:layout_marginTop="10dp"
android:textSize="25sp"
android:textStyle="bold"
android:textColor="?attr/custom_attr_text"
android:layout_gravity="center"
android:layout_height="wrap_content">
</TextView>
</LinearLayout>
</com.skydoves.elasticviews.ElasticCardView>
<com.skydoves.elasticviews.ElasticCardView
android:id="@+id/cv_call_btn"
app:cardCornerRadius="10dp"
app:cardElevation="20dp"
app:cardBackgroundColor="?attr/custom_attr_cv2"
android:layout_alignParentRight="true"
app:cardView_duration="250"
android:layout_width="150dp"
android:layout_height="250dp">

<LinearLayout
android:layout_width="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_height="wrap_content">
<ImageView
android:layout_width="90dp"
android:layout_height="90dp"
android:src="@drawable/call"/>
<TextView
android:layout_width="wrap_content"
android:text="呼叫"
android:layout_marginTop="10dp"
android:textSize="25sp"
android:textStyle="bold"
android:textColor="?attr/custom_attr_text"
android:layout_gravity="center"
android:layout_height="wrap_content">

</TextView>
</LinearLayout>
</com.skydoves.elasticviews.ElasticCardView>
</RelativeLayout>
<RelativeLayout
android:layout_gravity="center"
android:layout_marginTop="50dp"
android:padding="20dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.skydoves.elasticviews.ElasticCardView
android:id="@+id/cw_more_fr"
app:cardCornerRadius="10dp"
app:cardElevation="20dp"
app:cardView_duration="250"
app:cardBackgroundColor="?attr/custom_attr_cv3"
android:layout_width="150dp"
android:layout_height="250dp">

<LinearLayout
android:layout_width="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_height="wrap_content">
<ImageView

android:layout_width="90dp"
android:layout_height="90dp"
android:src="@drawable/ic_person_page"/>
<TextView
android:layout_width="wrap_content"
android:text="个人"
android:layout_marginTop="10dp"
android:textSize="25sp"
android:textStyle="bold"
android:textColor="?attr/custom_attr_text"
android:layout_gravity="center"
android:layout_height="wrap_content">

</TextView>
</LinearLayout>
</com.skydoves.elasticviews.ElasticCardView>
<com.skydoves.elasticviews.ElasticCardView
android:id="@+id/photo_bt"
app:cardCornerRadius="10dp"
app:cardElevation="20dp"
app:cardBackgroundColor="?attr/custom_attr_cv4"
android:layout_alignParentRight="true"
app:cardView_duration="250"
android:layout_width="150dp"
android:layout_height="250dp">

<LinearLayout
android:layout_width="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_height="wrap_content">
<ImageView
android:layout_width="90dp"
android:layout_height="90dp"
android:src="@drawable/iden_camera"/>
<TextView
android:layout_width="wrap_content"
android:text="识别"
android:layout_marginTop="10dp"
android:textSize="25sp"
android:textStyle="bold"
android:textColor="?attr/custom_attr_text"
android:layout_gravity="center"
android:layout_height="wrap_content">
</TextView>
</LinearLayout>
</com.skydoves.elasticviews.ElasticCardView>
</RelativeLayout>
</LinearLayout>

页面效果——日间:

夜间模式效果:

切换主题方法

那么我们应该怎么切换主题呢?首先在AndroidManifest.xml文件中设置default主题为日间的:

1
2
3
4
5
6
7
8
9
10
<application
android:allowBackup="true"
android:icon="@drawable/app_logo"
android:label="@string/app_name"
android:debuggable="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Mariotheme.Day">

接着在我们控制切换主题的开关中设置监听事件。由于我的开关是写在fragment中的,所以是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SharePreference sp = new SharePreference(getContext());
if (sp.getThemeInt()==1){alter_theme.setChecked(true);}
else {alter_theme.setChecked(false);}
alter_theme.setOnCheckedChangeListener(new SwitchButton.OnCheckedChangeListener() {
@SuppressLint("WrongConstant")
@Override
public void onCheckedChanged(SwitchButton view, boolean isChecked) {

if (isChecked){
//如果按钮打开,则切换夜间模式
sp.setThemeState(1);//设置状态为1并保存
getActivity().setTheme(R.style.Mariotheme_Night);
getActivity().onBackPressed();
mediaPlayerManager.PlayRemindVoice(R.raw.rc_shense);
}else {
//如果按钮关闭则回退为日间模式
sp.setThemeState(2);//设置状态为2并保存
getActivity().setTheme(R.style.Mariotheme_Day);
getActivity().onBackPressed();

}
}
});

在activity中在setContentView()方法之前直接调用setTheme()方法即可,并且需要调用recreate()方法来进行UI的刷新,否则主题还是不会更新。值得注意的是,退出activity后主题会恢复为默认的日间主题,所以我们需要把开启夜间状态后进行持久化存储。可以使用onSaveInstanceState()应用状态的保存。

但是我个人更推荐使用SharePreference来对按钮以及主题的状态进行保存。