写在前面
深色模式这几年特别火,iOS 13 和 Android 10 都加入了系统级的深色模式支持。作为一个现代 App,不支持深色模式好像有点说不过去。
我自己是深色模式的重度用户,晚上躺床上刷手机的时候,浅色界面简直亮瞎眼。所以每次装新 App,第一件事就是去设置里找有没有深色模式。如果没有,好感度直接减一半。
这篇文章记录一下主题设置页面的实现过程。页面本身和语言设置差不多,就是一个选择列表。但深色模式的实现比多语言复杂一些,涉及到整个 App 的样式切换。

支持哪些主题
先定义支持的主题选项:
tsx
const themes = [
{id: 'system', name: '跟随系统', icon: '📱', desc: '自动适应系统深浅色模式'},
{id: 'light', name: '浅色模式', icon: '☀️', desc: '始终使用浅色主题'},
{id: 'dark', name: '深色模式', icon: '🌙', desc: '始终使用深色主题'},
];
三个选项:
跟随系统:系统是深色模式,App 就是深色;系统是浅色模式,App 就是浅色。这是最省心的选项,大部分用户会选这个。
浅色模式:不管系统是什么模式,App 始终是浅色。有些用户就是喜欢浅色,或者觉得深色模式看着累。
深色模式:不管系统是什么模式,App 始终是深色。晚上用手机的用户会喜欢这个。
每个选项有四个属性:id 是标识符,name 是显示名称,icon 是图标,desc 是描述文字。描述文字能帮助用户理解每个选项的含义。
引入需要的依赖
tsx
import React from 'react';
不需要本地状态,主题设置存在全局状态里。
tsx
import {View, Text, StyleSheet, TouchableOpacity, Alert} from 'react-native';
和语言设置页面用的组件一样。设置类页面的结构都差不多,可以复用很多代码。
tsx
import {useApp} from '../store/AppContext';
import {Header} from '../components/Header';
获取全局状态
tsx
export const ThemeScreen = () => {
const {settings, updateSettings, goBack} = useApp();
和语言设置一样,从全局状态里取 settings、updateSettings 和 goBack。
选择主题的逻辑
tsx
const handleSelect = (id: string) => {
updateSettings('theme', id);
Alert.alert('设置成功', '主题设置已更新', [{text: '确定', onPress: goBack}]);
};
逻辑很简单:更新全局状态里的 theme 字段,弹个提示,返回上一页。
实际项目中,切换主题后应该立即生效,整个 App 的颜色都要变。这需要配合主题系统来实现,后面会详细讲。
页面结构
tsx
return (
<View style={styles.container}>
<Header title="主题设置" />
<View style={styles.content}>
{themes.map(theme => (
// 主题选项
))}
</View>
<Text style={styles.tip}>
💡 深色模式可以在夜间使用时减少眼睛疲劳,同时节省电量
</Text>
</View>
);
比语言设置多了一个底部提示,告诉用户深色模式的好处。
渲染主题选项
tsx
{themes.map(theme => (
<TouchableOpacity
key={theme.id}
style={styles.item}
onPress={() => handleSelect(theme.id)}
>
<View style={styles.iconWrap}>
<Text style={styles.icon}>{theme.icon}</Text>
</View>
左边是图标,用一个圆形背景包裹,看起来更精致。
tsx
<View style={styles.info}>
<Text style={styles.themeName}>{theme.name}</Text>
<Text style={styles.themeDesc}>{theme.desc}</Text>
</View>
中间是主题名称和描述。描述用小号灰色字,补充说明这个选项的作用。
tsx
{settings.theme === theme.id && <Text style={styles.check}>✓</Text>}
</TouchableOpacity>
))}
右边是选中标记,和语言设置一样的逻辑。
样式定义
typescript
const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#f5f5f5'},
content: {flex: 1, backgroundColor: '#fff', marginTop: 12},
基础布局,灰色背景上放白色列表。
typescript
item: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0'
},
每个选项横向排列,底部有分割线。
typescript
iconWrap: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#f5f5f5',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12
},
icon: {fontSize: 24},
图标用圆形灰色背景包裹,44x44 的大小,比语言设置的国旗更精致一些。
typescript
info: {flex: 1},
themeName: {fontSize: 16, color: '#333', fontWeight: '500'},
themeDesc: {fontSize: 12, color: '#999', marginTop: 2},
名称用 16 号字加粗,描述用 12 号灰色字。
typescript
check: {fontSize: 20, color: '#3498db', fontWeight: 'bold'},
tip: {fontSize: 13, color: '#999', padding: 16, lineHeight: 20},
});
底部提示用灰色小字,不要太醒目。
深色模式的实现方案
上面只是做了主题设置的 UI,真正的深色模式实现需要更多工作。聊一下常见的方案:
方案一:React Native 内置的 useColorScheme
React Native 提供了 useColorScheme hook,可以获取系统当前的颜色模式:
tsx
import {useColorScheme} from 'react-native';
const colorScheme = useColorScheme(); // 'light' | 'dark' | null
这个 hook 会返回
'light'、'dark'或null。null表示系统不支持或者无法确定。
配合我们的主题设置,可以这样判断当前应该用什么主题:
tsx
const systemTheme = useColorScheme();
const currentTheme = settings.theme === 'system'
? (systemTheme || 'light')
: settings.theme;
如果用户选了"跟随系统",就用系统主题;否则用用户选择的主题。
方案二:styled-components 的 ThemeProvider
如果用 styled-components,可以用 ThemeProvider 来管理主题:
tsx
const lightTheme = {
background: '#ffffff',
text: '#333333',
primary: '#3498db',
};
const darkTheme = {
background: '#1a1a1a',
text: '#ffffff',
primary: '#5dade2',
};
<ThemeProvider theme={currentTheme === 'dark' ? darkTheme : lightTheme}>
<App />
</ThemeProvider>
然后在组件里通过
props.theme获取当前主题的颜色值。
方案三:React Context 自己实现
不用第三方库,自己用 Context 实现也可以:
tsx
const ThemeContext = React.createContext({
theme: 'light',
colors: lightColors,
});
// 在组件里使用
const {colors} = useContext(ThemeContext);
<View style={{backgroundColor: colors.background}}>
这个方案灵活性最高,但需要自己处理的东西也最多。
我们这个项目为了简化,没有真正实现深色模式切换,只是保存了主题设置。实际项目中建议用方案一或方案三。
深色模式的设计要点
做深色模式不是简单地把白色换成黑色,有很多细节要注意:
1. 不要用纯黑色
纯黑色(#000000)在 OLED 屏幕上和其他颜色对比太强烈,看着不舒服。建议用深灰色,比如 #1a1a1a 或 #121212。
Google 的 Material Design 推荐深色模式的背景色是 #121212,这个颜色经过了大量测试,视觉效果比较好。
2. 文字颜色要降低对比度
浅色模式下文字是黑色(#333333),深色模式下不要用纯白色(#ffffff),用稍微暗一点的白色(#e0e0e0)。
对比度太高会让眼睛疲劳,特别是在暗环境下。
3. 主题色可能需要调整
有些颜色在浅色背景上好看,在深色背景上可能不好看。比如我们的主题蓝色 #3498db,在深色背景上可能需要调亮一点。
4. 图片和图标
有些图片在深色背景上可能看不清,需要准备深色模式专用的图片。图标如果是深色的,在深色背景上也会看不清。
一个技巧是给图标加个浅色的背景或者边框。
5. 阴影效果
浅色模式下常用的阴影效果,在深色模式下基本看不出来。可以用边框或者微妙的颜色差异来代替。
跟随系统的实现
"跟随系统"选项需要监听系统主题变化:
tsx
import {useColorScheme, Appearance} from 'react-native';
// 方式一:用 hook(推荐)
const colorScheme = useColorScheme();
// 方式二:监听变化
useEffect(() => {
const subscription = Appearance.addChangeListener(({colorScheme}) => {
console.log('系统主题变了:', colorScheme);
});
return () => subscription.remove();
}, []);
useColorScheme会自动响应系统主题变化,组件会重新渲染。如果需要在变化时执行一些逻辑(比如保存到本地),可以用Appearance.addChangeListener。
主题切换的动画
切换主题时,如果颜色瞬间变化会很突兀。可以加个过渡动画:
tsx
// 简单的方案:用 LayoutAnimation
import {LayoutAnimation} from 'react-native';
const handleSelect = (id: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
updateSettings('theme', id);
};
LayoutAnimation会让布局变化有个平滑的过渡效果。不过它对颜色变化的支持有限,效果可能不太明显。
更好的方案是用 react-native-reanimated 做颜色插值动画,但实现起来比较复杂。
省电效果
深色模式在 OLED 屏幕上确实能省电,因为 OLED 显示黑色时像素是关闭的,不耗电。
但在 LCD 屏幕上,深色模式不省电,甚至可能更耗电(因为背光始终亮着,显示深色需要液晶遮挡更多光线)。
所以底部提示里说"节省电量",严格来说只对 OLED 屏幕成立。不过现在大部分中高端手机都是 OLED 屏幕了,这么说问题不大。
小结
主题设置页面的 UI 和语言设置很像,就是一个选择列表。但深色模式的实现比多语言复杂。
几个关键点:
- 提供三个选项:跟随系统、浅色、深色
- 每个选项有图标和描述,帮助用户理解
- 底部提示深色模式的好处
关于深色模式实现:
- 用
useColorScheme获取系统主题 - 不要用纯黑色,用深灰色
- 文字颜色要降低对比度
- 图片和图标可能需要适配
- 切换时可以加过渡动画
深色模式是个细活,做好了用户体验会很好,做不好会很丑。如果时间有限,可以先只支持浅色模式,后面再加深色模式。
下一篇写隐私政策页面,这是本系列的最后一篇,敬请期待。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net