晚上看手机,眼睛疼
白天用手机,亮堂堂的屏幕没问题。晚上关了灯躺在床上刷手机,那个白色背景简直是在用手电筒照眼睛。
深色模式就是为这个场景设计的。深色背景配浅色文字,屏幕亮度低,眼睛舒服多了。现在几乎所有主流 App 都支持深色模式,用户也习惯了在设置里找这个开关。
我们的 TodoList 应用默认是深色模式,用户可以通过导航栏的开关切换到浅色模式。切换是即时的,整个界面的颜色会立刻变化。
主题状态的管理
主题用一个布尔状态控制:
tsx
const [darkMode, setDarkMode] = useState(true);
true 是深色模式,false 是浅色模式。默认 true,因为深色模式更护眼,也更符合现代 App 的审美。
为什么用布尔值而不是字符串(比如 'dark' 和 'light')?因为只有两种模式,布尔值更简单。如果以后要支持更多主题(比如"跟随系统"、"自动"),可以改成字符串或枚举。
主题颜色的定义
根据 darkMode 的值,返回不同的颜色:
tsx
const theme = {
bg: darkMode ? '#0f0f23' : '#f5f5f5',
card: darkMode ? '#1a1a2e' : '#ffffff',
text: darkMode ? '#ffffff' : '#333333',
subText: darkMode ? '#888888' : '#666666',
border: darkMode ? '#2a2a4a' : '#e0e0e0',
accent: '#6c5ce7',
};
这是一个对象,包含六种颜色。每种颜色根据 darkMode 返回深色或浅色的值。
bg 背景色
深色模式是 #0f0f23,一种很深的蓝黑色,不是纯黑。纯黑 #000000 在 OLED 屏幕上会有"拖影"问题,而且和其他元素对比太强烈。深蓝黑色更柔和。
浅色模式是 #f5f5f5,一种很浅的灰色,不是纯白。纯白 #ffffff 太刺眼,浅灰色更舒服。
card 卡片背景色
深色模式是 #1a1a2e,比背景色稍浅一点。卡片要和背景有区分,但不能差太多。
浅色模式是 #ffffff,纯白色。在浅灰色背景上,白色卡片很清晰。
text 主文字颜色
深色模式是 #ffffff,白色。在深色背景上,白色文字对比度高,容易阅读。
浅色模式是 #333333,深灰色。不用纯黑 #000000,因为纯黑在白色背景上对比太强,看久了累。深灰色更柔和。
subText 次要文字颜色
深色模式是 #888888,中灰色。比主文字浅,用于不太重要的信息。
浅色模式是 #666666,也是灰色,但比深色模式的稍深一点,保证在浅色背景上能看清。
border 边框颜色
深色模式是 #2a2a4a,深紫灰色。边框不需要太明显,能看出分隔就行。
浅色模式是 #e0e0e0,浅灰色。
accent 强调色
#6c5ce7,紫色。这个颜色在两种模式下都不变。
强调色是品牌色,应该保持一致。不管深色还是浅色模式,用户看到紫色就知道这是重要的元素。
主题的应用
定义了主题颜色,怎么用到界面上?
背景色
tsx
<SafeAreaView style={[styles.container, {backgroundColor: theme.bg}]}>
最外层容器用 theme.bg 作为背景色。
卡片
tsx
<View style={[styles.taskCard, {backgroundColor: theme.card, borderColor: theme.border}]}>
任务卡片用 theme.card 作为背景,theme.border 作为边框颜色。
文字
tsx
<Text style={[styles.taskTitle, {color: theme.text}]}>{item.title}</Text>
<Text style={[styles.dueDate, {color: theme.subText}]}>📅 {item.dueDate}</Text>
主要文字用 theme.text,次要文字用 theme.subText。
样式数组
注意我们用的是样式数组 style={[styles.xxx, {color: theme.xxx}]}。
styles.xxx 是静态样式,定义在 StyleSheet.create 里,不会变。
{color: theme.xxx} 是动态样式,根据主题变化。
这种写法把静态和动态样式分开,代码更清晰。
主题切换开关
导航栏右侧有一个开关:
tsx
<View style={styles.themeSwitch}>
<Text style={{color: theme.subText}}>🌙</Text>
<Switch value={darkMode} onValueChange={setDarkMode} trackColor={{false: '#767577', true: theme.accent}} thumbColor={darkMode ? '#fff' : '#f4f3f4'} />
</View>
月亮图标
🌙 emoji 暗示这是深色模式的开关。月亮代表夜晚,夜晚用深色模式。
Switch 组件
value={darkMode} 绑定状态,开关的位置反映当前模式。
onValueChange={setDarkMode} 切换时更新状态。用户拨动开关,darkMode 变化,theme 对象重新计算,整个界面颜色更新。
trackColor
trackColor={``{false: '#767577', true: theme.accent}} 设置轨道颜色。
关闭时(浅色模式)轨道是灰色,打开时(深色模式)轨道是紫色。紫色轨道暗示"深色模式已开启"。
thumbColor
thumbColor={darkMode ? '#fff' : '#f4f3f4'} 设置滑块颜色。
深色模式下滑块是白色,浅色模式下是浅灰色。这个差异很微妙,但能让开关在不同背景下都清晰可见。
状态栏的适配
切换主题时,状态栏也要跟着变:
tsx
<StatusBar barStyle={darkMode ? 'light-content' : 'dark-content'} backgroundColor={theme.bg} />
barStyle
'light-content' 是浅色内容(白色文字和图标),适合深色背景。
'dark-content' 是深色内容(黑色文字和图标),适合浅色背景。
如果状态栏样式和背景不匹配,文字会看不清。深色背景配浅色文字,浅色背景配深色文字。
backgroundColor
backgroundColor={theme.bg} 设置状态栏背景色,和应用背景一致。
这个属性主要在 Android 上有效。iOS 的状态栏是透明的。
设置页的主题切换
除了导航栏的开关,设置页也有主题切换:
tsx
<View style={[styles.settingsCard, {backgroundColor: theme.card, borderColor: theme.border}]}>
<Text style={[styles.settingsCardTitle, {color: theme.text}]}>外观</Text>
<View style={styles.settingItem}>
<View style={styles.settingInfo}>
<Text style={[styles.settingLabel, {color: theme.text}]}>深色模式</Text>
<Text style={[styles.settingDesc, {color: theme.subText}]}>切换深色/浅色主题</Text>
</View>
<Switch value={darkMode} onValueChange={setDarkMode} trackColor={{false: '#767577', true: theme.accent}} thumbColor={darkMode ? '#fff' : '#f4f3f4'} />
</View>
</View>
两个开关控制同一个状态 darkMode,所以它们是同步的。在导航栏切换,设置页的开关也会变;在设置页切换,导航栏的开关也会变。
为什么要两个地方都有开关?导航栏的开关方便快速切换,设置页的开关让用户知道"这是一个设置项"。
主题切换的即时性
切换主题是即时的,不需要重启应用。
这是因为 theme 对象是在组件内部计算的:
tsx
const theme = {
bg: darkMode ? '#0f0f23' : '#f5f5f5',
...
};
darkMode 变化时,组件重新渲染,theme 重新计算,所有使用 theme 的地方都会更新。
这是 React 响应式的魅力。状态变化自动触发 UI 更新,不需要手动刷新。
主题的持久化
当前的实现,关闭应用再打开,主题会重置为默认的深色模式。如果想保存用户的选择:
tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
// 读取保存的主题
useEffect(() => {
AsyncStorage.getItem('darkMode').then(value => {
if (value !== null) {
setDarkMode(JSON.parse(value));
}
});
}, []);
// 保存主题设置
useEffect(() => {
AsyncStorage.setItem('darkMode', JSON.stringify(darkMode));
}, [darkMode]);
第一个 useEffect 在组件挂载时读取保存的主题设置。
第二个 useEffect 在 darkMode 变化时保存设置。
AsyncStorage 是 React Native 的本地存储,类似于 Web 的 localStorage。
我们的演示项目没有实现持久化,每次打开都是深色模式。真实应用应该保存用户的选择。
跟随系统主题
很多用户希望 App 的主题跟随系统设置。系统是深色模式,App 也是深色模式。
React Native 提供了 useColorScheme hook:
tsx
import {useColorScheme} from 'react-native';
const systemColorScheme = useColorScheme(); // 'dark' | 'light' | null
const [themeMode, setThemeMode] = useState<'system' | 'dark' | 'light'>('system');
const darkMode = themeMode === 'system'
? systemColorScheme === 'dark'
: themeMode === 'dark';
themeMode 有三个值:'system'(跟随系统)、'dark'(强制深色)、'light'(强制浅色)。
当 themeMode 是 'system' 时,根据 systemColorScheme 决定是否深色模式。
这样用户可以选择"跟随系统",系统切换主题时 App 自动跟着切换。
我们的演示项目没有实现这个功能,只有手动切换。
主题切换的动画
当前的主题切换是瞬间的,颜色突然变化。可以加一个过渡动画让切换更柔和:
tsx
const bgAnim = useRef(new Animated.Value(darkMode ? 0 : 1)).current;
useEffect(() => {
Animated.timing(bgAnim, {
toValue: darkMode ? 0 : 1,
duration: 300,
useNativeDriver: false,
}).start();
}, [darkMode]);
const animatedBg = bgAnim.interpolate({
inputRange: [0, 1],
outputRange: ['#0f0f23', '#f5f5f5'],
});
<Animated.View style={[styles.container, {backgroundColor: animatedBg}]}>
背景色在 300 毫秒内从一个颜色过渡到另一个颜色。
但这种方式有局限性。useNativeDriver: false 意味着动画在 JS 线程执行,性能不如原生动画。而且要给每个颜色都加动画,代码会很复杂。
大多数应用的主题切换都是瞬间的,用户也习惯了。加动画是锦上添花,不是必须的。
图片和图标的适配
我们的应用用 emoji 作为图标,不需要特别处理。
如果用图片或 SVG 图标,可能需要为深色模式准备不同的版本。比如一个黑色的图标在深色背景上看不见,需要换成白色版本。
tsx
<Image source={darkMode ? require('./icon-white.png') : require('./icon-black.png')} />
或者用 tintColor 给图标着色:
tsx
<Image source={require('./icon.png')} style={{tintColor: theme.text}} />
小结
主题切换用一个布尔状态 darkMode 控制,根据它计算 theme 对象包含各种颜色。界面元素通过 theme.xxx 获取颜色,darkMode 变化时自动更新。Switch 组件让用户切换主题,StatusBar 跟着适配。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

