React Native for OpenHarmony 实战:Steam 资讯 App 主题设置实现

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_steam

主题系统是现代 App 的标配。用户可以根据自己的喜好选择深色或浅色主题,甚至自定义主题颜色。这篇文章来实现一个完整的主题系统,包括多主题定义、动态切换、样式管理和持久化存储。

为什么需要主题系统

在开发 Steam 资讯 App 的过程中,我们发现用户对界面外观的需求差异很大。有些用户喜欢在夜间使用深色主题来保护眼睛,有些用户则习惯在白天使用浅色主题。如果 App 只支持一种主题,就会让一部分用户感到不适。

一个好的主题系统应该具备以下特点:

  • 集中管理 - 所有颜色配置集中在一个地方,便于维护和修改
  • 易于扩展 - 添加新主题时只需要添加新的颜色配置
  • 实时切换 - 用户切换主题时,整个 App 立即响应
  • 自动适配 - 支持跟随系统主题自动切换

主题配置的设计

首先需要定义 App 支持的所有主题。每个主题包含一套完整的颜色配置,用于控制整个 App 的外观。

先定义主题颜色的接口:

tsx 复制代码
interface ThemeColors {
  background: string;      // 页面背景色
  surface: string;         // 卡片和容器背景
  surfaceLight: string;    // 较浅的表面色
  text: string;            // 主要文字颜色
  textSecondary: string;   // 次要文字颜色
  primary: string;         // 主色调
  accent: string;          // 辅助色
  border: string;          // 边框颜色
  success: string;         // 成功状态
  warning: string;         // 警告状态
  error: string;           // 错误状态
}

这个接口定义了主题所需的所有颜色属性。每个属性都有明确的用途:

  • background - 页面最底层的背景色,通常是最深的颜色
  • surface - 卡片、对话框等容器的背景色
  • surfaceLight - 用于分割线、输入框等较浅的表面
  • text - 主要内容文字,需要有足够的对比度
  • textSecondary - 辅助信息、描述文字等
  • primary - 主色调,用于按钮、链接等强调元素
  • accent - 辅助色,用于补充主色调
  • border - 边框和分割线的颜色
  • success / warning / error - 状态指示颜色

接口设计 - 通过 TypeScript 接口来定义主题结构,可以在编译时就发现错误。如果某个主题缺少某个颜色属性,TypeScript 会立即报错。这样可以避免运行时的颜色缺失问题。

然后创建具体的主题配置:

tsx 复制代码
const THEMES: Record<string, ThemeColors> = {
  dark: {
    background: '#171a21',
    surface: '#1b2838',
    surfaceLight: '#2a475e',
    text: '#ffffff',
    textSecondary: '#8f98a0',
    primary: '#66c0f4',
    accent: '#acdbf5',
    border: '#2a475e',
    success: '#a4d007',
    warning: '#f57c00',
    error: '#d32f2f',
  },
  light: {
    background: '#f5f5f5',
    surface: '#ffffff',
    surfaceLight: '#eeeeee',
    text: '#212121',
    textSecondary: '#757575',
    primary: '#1976d2',
    accent: '#42a5f5',
    border: '#e0e0e0',
    success: '#388e3c',
    warning: '#f57c00',
    error: '#d32f2f',
  },
};

这里定义了两个主题:深色和浅色。深色主题采用 Steam 的官方配色,浅色主题则采用更明亮的颜色。

配置管理 - 通过集中管理所有颜色,可以确保整个 App 的配色一致。如果要修改主题,只需要改这个配置对象就行。后续如果要添加新主题,比如高对比度主题,只需要在这里添加新的配置就可以。这种设计让主题系统非常灵活。

创建主题 Hook

为了方便在组件中使用主题颜色,创建一个自定义 Hook 来获取当前主题的颜色配置。这样做的好处是,所有组件都能通过统一的方式访问主题颜色,而不需要关心主题的具体实现细节。

tsx 复制代码
export const useTheme = () => {
  const {theme} = useApp();
  
  const getTheme = () => {
    if (theme === 'auto') {
      const isDark = Appearance.getColorScheme() === 'dark';
      return THEMES[isDark ? 'dark' : 'light'];
    }
    return THEMES[theme] || THEMES.dark;
  };
  
  return getTheme();
};

这个 Hook 的核心逻辑包括三个部分:

  • 获取全局主题状态 - 从 AppContext 中获取用户选择的主题
  • 自动模式处理 - 如果用户选择了"自动",则根据系统设置来决定使用深色还是浅色
  • 兜底处理 - 如果主题不存在或无效,默认使用深色主题

Hook 的优势 - 通过自定义 Hook,可以把主题逻辑封装起来。组件只需要调用 useTheme() 就能获得当前主题的颜色,不需要关心主题的具体实现。这样即使后续要修改主题逻辑,也只需要修改这个 Hook,不需要改动所有使用主题的组件。

使用这个 Hook 非常简单直观:

tsx 复制代码
const colors = useTheme();

<View style={{backgroundColor: colors.background}}>
  <Text style={{color: colors.text}}>Hello World</Text>
</View>

这样就能获得当前主题的颜色,并应用到组件上。当用户切换主题时,这些颜色会自动更新。

主题选择界面的实现

主题设置页面需要显示所有可用的主题,让用户选择。这个页面应该提供清晰的视觉反馈,让用户知道当前选择了哪个主题。

先定义主题选项的数据结构:

tsx 复制代码
const themeOptions = [
  {id: 'dark', label: '深色主题', icon: '🌙', description: '适合夜间使用'},
  {id: 'light', label: '浅色主题', icon: '☀️', description: '适合白天使用'},
  {id: 'auto', label: '自动', icon: '🔄', description: '根据系统设置自动切换'},
];

这个数组定义了所有可用的主题选项。每个选项包含:

  • id - 主题的唯一标识符,用于在状态中查找
  • label - 主题的显示名称
  • icon - 主题的图标,用 emoji 表示
  • description - 主题的描述,帮助用户理解

数据驱动 - 通过数组来定义主题选项,如果要添加新的主题,只需要在数组中添加一项就行。这样的设计让代码更灵活,也更容易维护。

然后实现主题选择的 UI:

tsx 复制代码
export const ThemeSettingsScreen = () => {
  const {theme, setTheme} = useApp();
  const colors = useTheme();
  
  return (
    <View style={[styles.container, {backgroundColor: colors.background}]}>
      <Header title="主题设置" showBack />
      <ScrollView style={styles.content}>
        <View style={styles.themeList}>
          {themeOptions.map(option => (
            <TouchableOpacity
              key={option.id}
              style={[
                styles.themeItem,
                {backgroundColor: colors.surface, borderColor: colors.border},
                theme === option.id && {borderColor: colors.primary, borderWidth: 2},
              ]}
              onPress={() => setTheme(option.id)}
            >
              <Text style={styles.themeIcon}>{option.icon}</Text>
              <View style={styles.themeInfo}>
                <Text style={[styles.themeName, {color: colors.text}]}>
                  {option.label}
                </Text>
                <Text style={[styles.themeDesc, {color: colors.textSecondary}]}>
                  {option.description}
                </Text>
              </View>
              {theme === option.id && (
                <Text style={[styles.checkmark, {color: colors.primary}]}>✓</Text>
              )}
            </TouchableOpacity>
          ))}
        </View>
      </ScrollView>
      <TabBar />
    </View>
  );
};

这个界面的关键实现细节:

  • 动态背景色 - 页面背景色从 useTheme() 获取,确保页面本身也遵循当前主题
  • 条件样式 - 当前选中的主题会显示不同的边框颜色和宽度
  • 视觉反馈 - 选中的主题项右侧显示对勾符号,让用户清楚地知道当前选择

用户体验 - 通过对勾符号和边框颜色的组合,用户可以一眼看出当前选择了哪个主题。这种视觉反馈很重要,能让用户更有信心地做出选择。

主题预览功能

为了让用户在选择主题前能看到效果,应该在设置页面中添加一个预览区域。这样用户可以直观地看到不同主题的外观。

tsx 复制代码
<View style={styles.previewSection}>
  <Text style={[styles.previewTitle, {color: colors.text}]}>主题预览</Text>
  <View style={[styles.previewBox, {backgroundColor: colors.surface, borderColor: colors.border}]}>
    <Text style={[styles.previewText, {color: colors.text}]}>主要文字</Text>
    <Text style={[styles.previewText, {color: colors.textSecondary}]}>次要文字</Text>
    <TouchableOpacity style={[styles.previewBtn, {backgroundColor: colors.primary}]}>
      <Text style={{color: colors.background}}>按钮</Text>
    </TouchableOpacity>
    <View style={[styles.previewBorder, {borderTopColor: colors.border}]} />
  </View>
</View>

预览区域展示了主题中最常用的几个颜色:

  • 主要文字 - 使用 colors.text 显示,这是最常见的文字颜色
  • 次要文字 - 使用 colors.textSecondary 显示,用于描述和辅助信息
  • 按钮 - 使用 colors.primary 作为背景色,这是最常见的交互元素
  • 分割线 - 使用 colors.border 作为边框颜色,用于视觉分隔

实时预览 - 预览区域会实时显示当前选中主题的效果。当用户切换主题时,预览区域会立即更新,让用户能够立即看到效果。这大大提高了用户体验。

主题切换的实时生效

当用户切换主题时,整个 App 的颜色应该立即改变。这需要在 AppContext 中实现响应式的主题切换。

在 AppContext 中,setTheme 函数的实现很简单:

tsx 复制代码
const setTheme = (theme: string) =>
  setState(prev => ({...prev, theme}));

但关键是,所有使用 useTheme() Hook 的组件都会自动重新渲染。这是因为 Hook 内部依赖了全局状态中的 theme 属性。当 theme 改变时,Hook 会返回新的颜色配置,组件会自动重新渲染。

响应式更新 - React 的响应式系统确保了当状态改变时,所有依赖这个状态的组件都会自动更新。这样用户切换主题后,整个 App 的颜色会立即改变,无需手动刷新。

主题设置的持久化存储

用户的主题选择应该被保存,这样下次打开 App 时还能使用之前选择的主题。可以使用 AsyncStorage 来实现持久化存储。

tsx 复制代码
import AsyncStorage from '@react-native-async-storage/async-storage';

const saveTheme = async (theme: string) => {
  try {
    await AsyncStorage.setItem('app_theme', theme);
  } catch (error) {
    console.error('Error saving theme:', error);
  }
};

const loadTheme = async () => {
  try {
    const saved = await AsyncStorage.getItem('app_theme');
    return saved || 'dark';
  } catch (error) {
    console.error('Error loading theme:', error);
    return 'dark';
  }
};

这两个函数分别用于保存和加载主题设置:

  • saveTheme - 将主题设置保存到本地存储,使用 setItem 方法
  • loadTheme - 从本地存储加载主题设置,如果没有保存过则返回默认值 'dark'

错误处理 - 使用 try-catch 来捕获可能的错误。如果保存或加载失败,应该有合理的兜底方案,比如使用默认主题。这样即使本地存储出现问题,App 也能正常运行。

然后在 AppProvider 中集成这些函数:

tsx 复制代码
export const AppProvider = ({children}: {children: ReactNode}) => {
  const [state, setState] = useState<AppState>({
    theme: 'dark',
    // ... 其他初始状态
  });
  
  useEffect(() => {
    // 应用启动时加载保存的主题设置
    loadTheme().then(theme => {
      setState(prev => ({...prev, theme}));
    });
  }, []);
  
  useEffect(() => {
    // 当主题改变时保存到本地
    saveTheme(state.theme);
  }, [state.theme]);
  
  // ... 其他代码
};

这个实现的流程是:

  • 应用启动 - 在 AppProvider 挂载时,调用 loadTheme() 加载保存的主题设置
  • 主题改变 - 当用户改变主题时,自动调用 saveTheme() 保存新的主题设置
  • 下次启动 - 下次打开 App 时,会自动加载之前保存的主题设置

持久化策略 - 通过 useEffect 监听主题的变化,当主题改变时自动保存。这样用户的设置能在应用关闭后保留,提供了无缝的用户体验。

系统主题检测

为了提供更好的用户体验,可以在"自动"模式下根据系统设置来自动切换主题。这样当用户改变系统主题时,App 会自动跟随。

tsx 复制代码
import {Appearance, useColorScheme} from 'react-native';

export const useTheme = () => {
  const {theme} = useApp();
  const systemTheme = useColorScheme();
  
  const getTheme = () => {
    if (theme === 'auto') {
      return THEMES[systemTheme === 'dark' ? 'dark' : 'light'];
    }
    return THEMES[theme] || THEMES.dark;
  };
  
  return getTheme();
};

这个实现的关键点:

  • 系统主题检测 - 使用 useColorScheme() Hook 获取系统当前的主题设置
  • 自动切换 - 如果用户选择了"自动"模式,则使用系统主题
  • 手动模式 - 如果用户手动选择了主题,则忽略系统设置

系统集成 - 通过 useColorScheme() Hook,可以获取系统当前的主题设置。这样 App 可以和系统主题保持一致,提供更好的整体体验。

在组件中应用主题

现在在任何组件中都可以使用 useTheme() Hook 来获取主题颜色。比如在 HomeScreen 中:

tsx 复制代码
export const HomeScreen = () => {
  const colors = useTheme();
  
  return (
    <View style={[styles.container, {backgroundColor: colors.background}]}>
      <Header title="Steam 资讯" />
      <ScrollView style={{backgroundColor: colors.background}}>
        <View style={[styles.section, {backgroundColor: colors.surface}]}>
          <Text style={[styles.title, {color: colors.text}]}>特惠推荐</Text>
          <Text style={[styles.subtitle, {color: colors.textSecondary}]}>
            今日特惠游戏
          </Text>
        </View>
      </ScrollView>
    </View>
  );
};

这样做的好处是:

  • 统一管理 - 所有的颜色都通过 useTheme() 来获取
  • 自动更新 - 当用户切换主题时,这些颜色会自动更新
  • 代码简洁 - 不需要在每个组件中硬编码颜色值

代码组织 - 通过统一使用 useTheme() 来获取颜色,代码变得更清晰。如果要修改主题颜色,只需要在 THEMES 配置中修改就行,不需要在代码中搜索和替换。

主题样式的完整定义

为了让主题系统完整运作,需要定义所有相关的样式。这些样式应该尽可能地使用主题颜色,而不是硬编码的颜色值。

tsx 复制代码
const styles = StyleSheet.create({
  container: {flex: 1},
  content: {flex: 1},
  themeList: {paddingVertical: 16, paddingHorizontal: 16},
  themeItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 12,
    paddingHorizontal: 16,
    borderRadius: 8,
    marginBottom: 12,
    borderWidth: 1,
  },
  themeIcon: {fontSize: 32, marginRight: 16},
  themeInfo: {flex: 1},
  themeName: {fontSize: 16, fontWeight: '600', marginBottom: 2},
  themeDesc: {fontSize: 12},
  checkmark: {fontSize: 20, fontWeight: 'bold'},
  previewSection: {paddingHorizontal: 16, paddingVertical: 24},
  previewTitle: {fontSize: 16, fontWeight: 'bold', marginBottom: 12},
  previewBox: {
    borderRadius: 8,
    padding: 16,
    borderWidth: 1,
  },
  previewText: {fontSize: 14, marginBottom: 8},
  previewBtn: {paddingVertical: 8, paddingHorizontal: 16, borderRadius: 6, marginTop: 8},
  previewBorder: {borderTopWidth: 1, marginTop: 12},
});

这些样式定义了主题设置页面的布局。关键点包括:

  • 灵活的布局 - 使用 flexbox 实现响应式布局
  • 可复用的样式 - 定义通用的样式类,可以在多个地方使用
  • 视觉层级 - 通过不同的字体大小和颜色来区分不同的信息层级

样式管理 - 虽然这些样式中的颜色值是硬编码的,但在实际使用时,这些颜色会被 useTheme() 返回的动态颜色覆盖。这样既保证了样式的清晰,又实现了主题的动态切换。

主题系统的扩展

当需要添加新的主题时,只需要在 THEMES 对象中添加新的配置就行。比如添加一个高对比度主题:

tsx 复制代码
const THEMES: Record<string, ThemeColors> = {
  // ... 现有主题
  highContrast: {
    background: '#000000',
    surface: '#1a1a1a',
    surfaceLight: '#333333',
    text: '#ffffff',
    textSecondary: '#cccccc',
    primary: '#ffff00',
    accent: '#00ff00',
    border: '#666666',
    success: '#00ff00',
    warning: '#ffff00',
    error: '#ff0000',
  },
};

然后在主题选项中添加新的选项:

tsx 复制代码
const themeOptions = [
  // ... 现有选项
  {id: 'highContrast', label: '高对比度', icon: '⚡', description: '高对比度主题'},
];

这样就完成了新主题的添加,无需修改任何其他代码。

可扩展性 - 这种设计让添加新主题变得非常简单。只需要定义新的颜色配置和选项,就能立即支持新主题。这正是好的架构设计的体现。

小结

主题系统展示了如何实现一个完整的主题管理方案:

  • 集中配置 - 通过配置对象集中管理所有主题颜色
  • 自定义 Hook - 创建 useTheme() Hook 简化组件中的主题使用
  • 全局状态 - 主题设置存储在全局状态中,确保整个 App 都能访问
  • 实时切换 - 用户切换主题时,整个 App 的颜色立即改变
  • 持久化存储 - 用户的主题选择被保存,下次打开 App 时自动恢复
  • 系统集成 - 支持自动模式,根据系统设置自动切换主题
  • 易于扩展 - 添加新的主题只需要在配置对象中添加就行

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
云边散步2 小时前
godot2D游戏教程系列一(1)
游戏
C_心欲无痕2 小时前
JavaScript 常见算法与手写函数实现
开发语言·javascript·算法
Web - Anonymous2 小时前
使用Vue3 + Elementplus + Day.js 实现日期选择器(包括日、周、月、年、自定义) - 附完整示例
前端·javascript·vue.js
冴羽2 小时前
2025 年 HTML 年度调查报告亮点速览!
前端·javascript·html
张元清2 小时前
浏览器硬导航优化:提升用户体验的关键
前端·javascript·面试
xkxnq2 小时前
第二阶段:Vue 组件化开发(第 23天)
前端·javascript·vue.js
晴栀ay2 小时前
JS的超集——TypeScript
前端·react.js·typescript
yyyao2 小时前
🔥🔥🔥 React18 源码学习 - Render 阶段(构造 Fiber 树)
react.js·源码阅读
有意义2 小时前
TypeScript 不是加法,是减法
react.js·typescript·前端框架