RN for OpenHarmony 实战 TodoList 项目:深色浅色主题切换

案例开源地址:https://atomgit.com/lqjmac/rn_openharmony_todolist

晚上看手机,眼睛疼

白天用手机,亮堂堂的屏幕没问题。晚上关了灯躺在床上刷手机,那个白色背景简直是在用手电筒照眼睛。

深色模式就是为这个场景设计的。深色背景配浅色文字,屏幕亮度低,眼睛舒服多了。现在几乎所有主流 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 在组件挂载时读取保存的主题设置。

第二个 useEffectdarkMode 变化时保存设置。

AsyncStorageReact 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

相关推荐
cn_mengbei16 小时前
从零到一:基于Qt on HarmonyOS的鸿蒙PC原生应用开发实战与性能优化指南
qt·性能优化·harmonyos
俩毛豆16 小时前
华为的“天工计划”是什么
华为·harmonyos·鸿蒙·搜索·小艺
小贵子的博客16 小时前
Ant Design Vue <a-table>
前端·javascript·vue.js·anti-design-vue
天天进步201516 小时前
【Nanobrowser源码分析4】交互篇: 从指令到动作:模拟点击、滚动与输入的底层实现
开发语言·javascript·ecmascript
FIT2CLOUD飞致云16 小时前
应用升级为智能体,模板中心上线,MaxKB开源企业级智能体平台v2.5.0版本发布
人工智能·ai·开源·1panel·maxkb
console.log('npc')16 小时前
vue2中子组件父组件的修改参数
开发语言·前端·javascript
Van_captain17 小时前
rn_for_openharmony常用组件_Chip纸片
javascript·开源·harmonyos
奋斗吧程序媛17 小时前
vue3 Study(1)
前端·javascript·vue.js
QQ129584550417 小时前
ThingsBoard - APP首页修改为手工选择组织
前端·javascript·物联网·iot