RN for OpenHarmony英雄联盟助手App实战:设置实现

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

设置页面是几乎每个 App 都有的功能,它让用户可以自定义 App 的行为和外观。一个好的设置页面应该结构清晰、操作简单,让用户能快速找到想要修改的选项。

这篇文章我们来实现设置页面,重点讨论主题切换 的实现、动态样式 的处理、以及设置页面的交互模式设计。

设置页面的设计原则

在开始写代码之前,先思考一下设置页面应该怎么设计:

分组展示:把相关的设置项放在一起,用分组标题区分。比如"显示"、"存储"、"关于"等。

操作类型:设置项通常有几种操作类型:

  • 开关型:用 Switch 组件,比如"深色模式"
  • 跳转型:点击后跳转到子页面,比如"关于"
  • 操作型:点击后执行某个操作,比如"清除缓存"
  • 展示型:只显示信息,不可操作,比如"版本号"

视觉一致性:所有设置项使用相同的卡片样式,形成统一的视觉语言。

主题系统的实现

在看设置页面之前,先了解一下主题系统是怎么实现的。主题切换是设置页面最重要的功能之一。

tsx 复制代码
import React, {createContext, useContext, useState, ReactNode, useMemo} from 'react';

// 主题类型
type ThemeType = 'dark' | 'light';

// 完整颜色配置
interface ThemeColors {
  primary: string;
  primaryDark: string;
  primaryLight: string;
  background: string;
  backgroundLight: string;
  backgroundCard: string;
  backgroundModal: string;
  textPrimary: string;
  textSecondary: string;
  textMuted: string;
  textGold: string;
  success: string;
  warning: string;
  error: string;
  info: string;
  border: string;
  borderLight: string;
  borderGold: string;
  white: string;
  black: string;
  transparent: string;
  overlay: string;
  // 职业颜色
  fighter: string;
  mage: string;
  assassin: string;
  marksman: string;
  support: string;
  tank: string;
  // 难度颜色
  difficultyLow: string;
  difficultyMedium: string;
  difficultyHigh: string;
}

ThemeColors 接口定义了主题中所有可用的颜色。这个接口非常详细,包含了:

颜色类别 包含的颜色 用途
主色调 primary, primaryDark, primaryLight 品牌色、强调色
背景色 background, backgroundCard, backgroundModal 页面、卡片、弹窗背景
文字色 textPrimary, textSecondary, textMuted, textGold 不同层级的文字
状态色 success, warning, error, info 成功、警告、错误、信息
边框色 border, borderLight, borderGold 普通边框、浅色边框、金色边框
职业色 fighter, mage, assassin... 英雄职业的标识色
难度色 difficultyLow, difficultyMedium, difficultyHigh 英雄难度的标识色

深色主题与浅色主题

tsx 复制代码
// 暗色主题
const darkTheme: ThemeColors = {
  primary: '#C89B3C',
  primaryDark: '#785A28',
  primaryLight: '#F0E6D2',
  background: '#010A13',
  backgroundLight: '#0A1428',
  backgroundCard: '#1E2328',
  backgroundModal: '#0A323C',
  textPrimary: '#F0E6D2',
  textSecondary: '#A09B8C',
  textMuted: '#5B5A56',
  textGold: '#C89B3C',
  // ... 其他颜色
};

// 亮色主题
const lightTheme: ThemeColors = {
  primary: '#C89B3C',
  primaryDark: '#785A28',
  primaryLight: '#F0E6D2',
  background: '#F5F5F5',
  backgroundLight: '#FFFFFF',
  backgroundCard: '#FFFFFF',
  backgroundModal: '#FFFFFF',
  textPrimary: '#1A1A1A',
  textSecondary: '#666666',
  textMuted: '#999999',
  textGold: '#C89B3C',
  // ... 其他颜色
};

两套主题的设计思路

  • 深色主题:背景色很深(#010A13 接近纯黑),文字色很浅(#F0E6D2 接近米白),符合英雄联盟游戏的暗黑风格
  • 浅色主题:背景色很浅(#F5F5F5 浅灰),文字色很深(#1A1A1A 深灰),适合白天使用

注意 :两套主题的 primary(主色调)保持一致,都是金色 #C89B3C。这是品牌色,不应该随主题变化。

ThemeContext 的实现

tsx 复制代码
// Context 类型
interface ThemeContextType {
  theme: ThemeType;
  colors: ThemeColors;
  toggleTheme: () => void;
  setTheme: (theme: ThemeType) => void;
  isDark: boolean;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// Provider
export function ThemeProvider({children}: {children: ReactNode}) {
  const [theme, setThemeState] = useState<ThemeType>('dark');

  const value = useMemo(() => ({
    theme,
    colors: theme === 'dark' ? darkTheme : lightTheme,
    isDark: theme === 'dark',
    toggleTheme: () => setThemeState(prev => (prev === 'dark' ? 'light' : 'dark')),
    setTheme: (newTheme: ThemeType) => setThemeState(newTheme),
  }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

Context 提供的能力

属性/方法 类型 用途
theme 'dark' | 'light' 当前主题名称
colors ThemeColors 当前主题的颜色配置
toggleTheme () => void 切换主题(深色↔浅色)
setTheme (theme) => void 设置指定主题
isDark boolean 是否是深色主题

useMemo 的作用

tsx 复制代码
const value = useMemo(() => ({...}), [theme]);

useMemo 缓存 context value 对象,只有当 theme 变化时才重新创建。这可以避免不必要的重渲染------如果每次渲染都创建新对象,所有消费这个 context 的组件都会重渲染。

设置页面的实现

tsx 复制代码
import React, {useState, useMemo} from 'react';
import {View, Text, ScrollView, TouchableOpacity, Switch, StyleSheet, Alert} from 'react-native';
import {useTheme} from '../../context/ThemeContext';
import {useNavigation} from '../../context/NavigationContext';

export function SettingsPage() {
  const {theme, colors, toggleTheme} = useTheme();
  const {navigate} = useNavigation();
  const [cacheSize] = useState('0 B');

依赖说明

  • useTheme:获取主题相关的状态和方法
  • useNavigation:获取导航方法,用于跳转到子页面
  • cacheSize:缓存大小,当前是静态值,实际项目中应该动态计算

动态样式的实现

tsx 复制代码
  const styles = useMemo(() => StyleSheet.create({
    container: {flex: 1, backgroundColor: colors.background, padding: 16},
    section: {marginBottom: 24},
    sectionTitle: {fontSize: 14, color: colors.textMuted, marginBottom: 8, textTransform: 'uppercase', letterSpacing: 1},
    settingItem: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: colors.backgroundCard, padding: 16, borderRadius: 8, marginBottom: 8, borderWidth: 1, borderColor: colors.border},
    settingLabel: {fontSize: 16, color: colors.textPrimary},
    settingValue: {fontSize: 14, color: colors.textSecondary},
    settingAction: {fontSize: 14, color: colors.error},
    arrow: {fontSize: 20, color: colors.textMuted},
  }), [colors]);

为什么样式要放在组件内部?

通常我们把 StyleSheet.create 放在组件外部,这样样式对象只创建一次。但在这个页面,样式依赖于主题颜色,而主题颜色可能会变化。

把样式放在组件内部,配合 useMemo,可以实现:

  1. 主题切换时样式自动更新 :当 colors 变化时,样式会重新创建
  2. 性能优化useMemo 确保只有 colors 变化时才重新创建样式

sectionTitle 的样式设计

tsx 复制代码
sectionTitle: {
  fontSize: 14, 
  color: colors.textMuted, 
  marginBottom: 8, 
  textTransform: 'uppercase', 
  letterSpacing: 1
}
  • textTransform: 'uppercase':文字转大写,增加标题感
  • letterSpacing: 1:字母间距加大,提升可读性
  • color: colors.textMuted:使用最淡的文字颜色,不抢设置项的风头

这种设计让分组标题低调但清晰,用户能看到分组,但注意力集中在设置项上。

清除缓存的交互

tsx 复制代码
  const handleClearCache = () => {
    Alert.alert('清除缓存', '确定要清除所有缓存数据吗?', [
      {text: '取消', style: 'cancel'},
      {text: '确定', onPress: () => {
        Alert.alert('提示', '缓存已清除');
      }},
    ]);
  };

为什么需要确认对话框?

清除缓存是一个破坏性操作,执行后无法撤销。用确认对话框可以:

  1. 防止误操作:用户可能不小心点到
  2. 告知后果:让用户知道这个操作会做什么
  3. 给用户选择权:可以取消操作

Alert.alert 的参数

tsx 复制代码
Alert.alert(
  '清除缓存',           // 标题
  '确定要清除所有缓存数据吗?',  // 内容
  [                      // 按钮数组
    {text: '取消', style: 'cancel'},
    {text: '确定', onPress: () => {...}},
  ]
);

style: 'cancel' 会让按钮显示为取消样式(在 iOS 上是蓝色普通文字,在 Android 上可能有不同表现)。

显示设置区域

tsx 复制代码
  return (
    <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>显示</Text>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>深色模式</Text>
          <Switch 
            value={theme === 'dark'} 
            onValueChange={toggleTheme} 
            trackColor={{false: colors.border, true: colors.primary}} 
            thumbColor={colors.white} 
          />
        </View>
      </View>

Switch 组件的属性

属性 说明
value theme === 'dark' 开关状态,深色主题时为开
onValueChange toggleTheme 切换时调用的函数
trackColor {false: ..., true: ...} 轨道颜色,关闭时灰色,打开时金色
thumbColor colors.white 滑块颜色,始终为白色

trackColor 的设计

  • 关闭时 (false):使用边框色 colors.border,低调不显眼
  • 打开时 (true):使用主色调 colors.primary(金色),表示激活状态

这种设计让用户一眼就能看出开关状态

存储设置区域

tsx 复制代码
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>存储</Text>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>缓存大小</Text>
          <Text style={styles.settingValue}>{cacheSize}</Text>
        </View>
        <TouchableOpacity style={styles.settingItem} onPress={handleClearCache}>
          <Text style={styles.settingLabel}>清除缓存</Text>
          <Text style={styles.settingAction}>清除</Text>
        </TouchableOpacity>
      </View>

两种设置项的对比

设置项 组件 右侧内容 可交互
缓存大小 View 数值文字
清除缓存 TouchableOpacity 操作文字

"清除"文字的样式

tsx 复制代码
settingAction: {fontSize: 14, color: colors.error}

使用 colors.error(红色)表示这是一个危险操作。红色在用户心中有"警告"的含义,提醒用户谨慎操作。

更多设置区域

tsx 复制代码
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>更多</Text>
        <TouchableOpacity style={styles.settingItem} onPress={() => navigate('SpellList')}>
          <Text style={styles.settingLabel}>召唤师技能</Text>
          <Text style={styles.arrow}>›</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.settingItem} onPress={() => navigate('Tools')}>
          <Text style={styles.settingLabel}>实用工具</Text>
          <Text style={styles.arrow}>›</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.settingItem} onPress={() => navigate('About')}>
          <Text style={styles.settingLabel}>关于</Text>
          <Text style={styles.arrow}>›</Text>
        </TouchableOpacity>
      </View>

跳转型设置项的设计

右侧显示 箭头,这是一个通用的 UI 约定,表示"点击后会跳转到另一个页面"。用户看到箭头就知道这个设置项可以点击,而且会打开新页面。

为什么用文字箭头而不是图标?

  • 简单:不需要额外的图标资源
  • 轻量:文字渲染比图片更快
  • 一致:在所有平台上显示效果相同

当然,也可以用 Icon 组件显示更精美的箭头图标。

关于信息区域

tsx 复制代码
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>关于</Text>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>版本</Text>
          <Text style={styles.settingValue}>1.0.0</Text>
        </View>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>数据来源</Text>
          <Text style={styles.settingValue}>Riot Games</Text>
        </View>
      </View>
    </ScrollView>
  );
}

展示型设置项

这两个设置项只显示信息,不可交互。使用 View 而不是 TouchableOpacity,用户点击不会有任何反馈。

版本号的用途

显示版本号有几个作用:

  1. 用户反馈:用户报告问题时可以提供版本号
  2. 更新提示:可以和服务器版本对比,提示用户更新
  3. 调试定位:开发者可以根据版本号定位问题

数据来源的声明

显示"Riot Games"是一种版权声明,表明数据来自官方,同时也是对 Riot Games 的致敬。

扩展:主题持久化

当前实现中,主题状态存储在内存中,App 重启后会恢复默认值。可以用 AsyncStorage 持久化:

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

export function ThemeProvider({children}: {children: ReactNode}) {
  const [theme, setThemeState] = useState<ThemeType>('dark');
  const [isLoaded, setIsLoaded] = useState(false);

  // 加载保存的主题
  useEffect(() => {
    AsyncStorage.getItem('theme').then(savedTheme => {
      if (savedTheme === 'dark' || savedTheme === 'light') {
        setThemeState(savedTheme);
      }
      setIsLoaded(true);
    });
  }, []);

  // 切换主题时保存
  const toggleTheme = useCallback(() => {
    setThemeState(prev => {
      const newTheme = prev === 'dark' ? 'light' : 'dark';
      AsyncStorage.setItem('theme', newTheme);
      return newTheme;
    });
  }, []);

  // 等待加载完成再渲染
  if (!isLoaded) return null;

  // ...
}

扩展:计算真实缓存大小

当前缓存大小是硬编码的 "0 B",可以实现真实的缓存计算:

tsx 复制代码
const [cacheSize, setCacheSize] = useState('计算中...');

useEffect(() => {
  calculateCacheSize().then(size => {
    setCacheSize(formatBytes(size));
  });
}, []);

// 格式化字节数
const formatBytes = (bytes: number): string => {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};

// 计算缓存大小(示例)
const calculateCacheSize = async (): Promise<number> => {
  // 这里需要根据实际的缓存策略来计算
  // 比如图片缓存、API 响应缓存等
  return 0;
};

扩展:更多设置选项

可以添加更多实用的设置选项:

tsx 复制代码
// 语言设置
<TouchableOpacity style={styles.settingItem} onPress={() => navigate('LanguageSettings')}>
  <Text style={styles.settingLabel}>语言</Text>
  <View style={styles.settingRight}>
    <Text style={styles.settingValue}>简体中文</Text>
    <Text style={styles.arrow}>›</Text>
  </View>
</TouchableOpacity>

// 通知设置
<View style={styles.settingItem}>
  <Text style={styles.settingLabel}>推送通知</Text>
  <Switch value={notificationEnabled} onValueChange={setNotificationEnabled} />
</View>

// 数据版本
<TouchableOpacity style={styles.settingItem} onPress={checkDataUpdate}>
  <Text style={styles.settingLabel}>数据版本</Text>
  <View style={styles.settingRight}>
    <Text style={styles.settingValue}>{dataVersion}</Text>
    {hasUpdate && <View style={styles.updateDot} />}
  </View>
</TouchableOpacity>

扩展:跟随系统主题

可以添加"跟随系统"选项,让 App 主题自动跟随系统设置:

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

type ThemeMode = 'dark' | 'light' | 'system';

export function ThemeProvider({children}: {children: ReactNode}) {
  const systemColorScheme = useColorScheme();
  const [themeMode, setThemeMode] = useState<ThemeMode>('system');

  // 计算实际主题
  const actualTheme = themeMode === 'system' 
    ? (systemColorScheme || 'dark') 
    : themeMode;

  // ...
}

useColorScheme 是 React Native 提供的 Hook,返回系统当前的颜色方案('dark' 或 'light')。

小结

设置页面展示了几个重要的实现模式:

  1. 主题系统:用 Context 管理主题状态,提供颜色配置和切换方法
  2. 动态样式:把 StyleSheet 放在组件内部,配合 useMemo 实现主题响应
  3. 设置项类型:开关型、跳转型、操作型、展示型,各有不同的交互方式
  4. 确认对话框:对破坏性操作使用 Alert 确认,防止误操作
  5. 视觉语义:用颜色传达含义(红色=危险,箭头=跳转)

设置页面虽然看起来简单,但涉及到的交互模式和设计细节很多,是一个很好的学习案例。

下一篇我们来实现关于页面,展示 App 的基本信息。


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

相关推荐
山上三树2 小时前
对比用户态线程与内核态轻量级进程
linux
阿甘正赚.2 小时前
Linux初学
linux·运维·服务器
物随心转2 小时前
线程阻塞调用与同步调用的区别
linux
虚神界熊孩儿2 小时前
Linux下修改docker和harbor默认网段的方法
linux·docker·harbor
Jay Chou why did2 小时前
ARM寄存器
linux
乌日尼乐2 小时前
【Linux】iptables使用详解(RT)
linux
wdfk_prog3 小时前
[Linux]学习笔记系列 -- bits
linux·笔记·学习
Xの哲學3 小时前
Linux epoll 深度剖析: 从设计哲学到底层实现
linux·服务器·网络·算法·边缘计算
iYun在学C3 小时前
驱动程序(注册字符设备)
linux·嵌入式硬件