在这里插入图片描述

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

符文是英雄联盟中非常重要的系统,它能为英雄提供额外的属性和特殊效果。不同的符文搭配可以让同一个英雄有完全不同的玩法。符文系统页面要展示五大符文系(精密、主宰、巫术、坚决、启迪),让用户可以浏览每个符文系下的所有符文。

这篇文章我们来实现符文列表页,重点是符文数据结构的理解、动态颜色主题的应用、以及 TypeScript 常量映射的使用技巧。

符文系统的数据结构

在开始写代码之前,先理解一下符文系统的数据结构。这对于后续的开发非常重要。

层级关系

符文系统有三个层级:

符文系 (RunePath)
  └── 符文槽位 (RuneSlot)
        └── 符文 (Rune)

符文系:共有 5 个,分别是精密、主宰、巫术、坚决、启迪。每个符文系有自己的主题色和图标。

符文槽位:每个符文系有 4 个槽位。第一个槽位是基石符文(最强的),后面三个是普通符文。

符文:每个槽位有 3-4 个符文可选,玩家需要从中选择一个。

类型定义

我们用 TypeScript 接口来描述这个结构:

// 符文系
export interface RunePath {
  id: number;
  key: string;
  icon: string;
  name: string;
  slots: RuneSlot[];
}

// 符文槽位
export interface RuneSlot {
  runes: Rune[];
}

// 符文
export interface Rune {
  id: number;
  key: string;
  icon: string;
  name: string;
  shortDesc: string;
  longDesc: string;
}

字段说明

  • id:数字 ID,拳头内部使用的唯一标识
  • key:英文标识,如 “Precision”、“Domination”
  • icon:图标路径,用于拼接完整的图标 URL
  • name:英文名称(API 返回的是英文,我们需要自己做中文映射)
  • slots:符文槽位数组,每个槽位包含多个可选符文

常量映射的设计

API 返回的符文名称是英文的,我们需要转成中文。同时,每个符文系有自己的主题色,也需要定义。

// 符文系中文名
export const RunePathNames = {
  Precision: '精密',
  Domination: '主宰',
  Sorcery: '巫术',
  Resolve: '坚决',
  Inspiration: '启迪',
} as const;

// 符文系颜色
export const RunePathColors = {
  Precision: '#C8AA6E',
  Domination: '#D44242',
  Sorcery: '#9FAAFC',
  Resolve: '#A1D586',
  Inspiration: '#49AAB8',
} as const;

as const 的作用

as const 是 TypeScript 的常量断言,它会把对象的类型收窄为字面量类型。比如 RunePathNames.Precision 的类型会是 '精密' 而不是 string。这在某些场景下能提供更精确的类型检查。

颜色的选择

每个符文系的颜色都有其含义:

  • 精密(金色):代表精准、高效,适合射手和需要持续输出的英雄
  • 主宰(红色):代表杀戮、侵略,适合刺客和需要爆发的英雄
  • 巫术(蓝紫色):代表魔法、神秘,适合法师
  • 坚决(绿色):代表坚韧、防御,适合坦克和战士
  • 启迪(青色):代表创新、灵活,提供各种实用效果

工具函数

// 获取符文系中文名
export function getRunePathName(key: string): string {
  return RunePathNames[key as keyof typeof RunePathNames] || key;
}

// 获取符文系颜色
export function getRunePathColor(key: string): string {
  return RunePathColors[key as keyof typeof RunePathColors] || '#C89B3C';
}

这两个函数根据符文系的 key 返回对应的中文名和颜色。如果遇到未知的 key(比如拳头新增了符文系),会返回原始 key 或默认颜色,避免报错。

keyof typeof 是 TypeScript 的类型操作符:

  • typeof RunePathNames 获取对象的类型
  • keyof 获取这个类型的所有键的联合类型

页面实现

导入依赖

import React, {useEffect, useState, useMemo} from 'react';
import {View, Text, ScrollView, Image, TouchableOpacity, StyleSheet} from 'react-native';
import {useTheme} from '../../context/ThemeContext';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {runeApi} from '../../api';
import {getRuneIconUrl} from '../../utils/image';
import {getRunePathName, getRunePathColor} from '../../models/Rune';
import {Loading} from '../../components/common';

这个页面用到了主题系统(useTheme),因为我们需要支持深色/浅色模式切换。符文系的颜色是固定的,但页面的背景色、文字颜色需要跟随主题变化。

组件结构与状态

export function RuneListPage() {
  const {colors} = useTheme();
  const {state, setRunes} = useApp();
  const {navigate} = useNavigation();
  const [loading, setLoading] = useState(true);

状态说明

  • colors:当前主题的颜色配置
  • state.runes:全局状态中缓存的符文数据
  • setRunes:更新符文数据的方法
  • loading:加载状态

动态样式

  const styles = useMemo(() => StyleSheet.create({
    container: {flex: 1, backgroundColor: colors.background, padding: 16},
    pathCard: {
      flexDirection: 'row', 
      alignItems: 'center', 
      backgroundColor: colors.backgroundCard, 
      borderRadius: 12, 
      padding: 16, 
      marginBottom: 12, 
      borderWidth: 1, 
      borderColor: colors.border, 
      borderLeftWidth: 4
    },
    pathIcon: {width: 48, height: 48},
    pathInfo: {flex: 1, marginLeft: 16},
    pathName: {fontSize: 18, fontWeight: '600', marginBottom: 4},
    pathDesc: {fontSize: 14, color: colors.textSecondary},
    arrow: {fontSize: 20, color: colors.textMuted},
    bottomSpace: {height: 20},
  }), [colors]);

为什么用 useMemo?

样式对象依赖 colors,当主题切换时 colors 会变化。用 useMemo 包裹可以:

  1. 只在 colors 变化时重新创建样式对象
  2. 避免每次渲染都创建新的样式对象

borderLeftWidth: 4 的设计

每个符文系卡片左边有一条 4px 的彩色边框,颜色是符文系的主题色。这种设计叫"色带指示器",能在不占用太多空间的情况下传达信息。

数据加载

  useEffect(() => {
    async function loadRunes() {
      if (!state.version) return;
      if (state.runes.length === 0) {
        setLoading(true);
        const runes = await runeApi.getRuneList(state.version);
        setRunes(runes);
      }
      setLoading(false);
    }
    loadRunes();
  }, [state.version]);

  if (loading) return <Loading fullScreen />;

缓存策略

if (state.runes.length === 0) 判断全局状态中是否已有符文数据。如果有,直接使用缓存,不再请求。这和装备列表页的策略一样。

符文数据在一个游戏版本内不会变化,缓存是安全的。

列表渲染

  return (
    <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
      {state.runes.map(path => {
        const pathColor = getRunePathColor(path.key);
        return (
          <TouchableOpacity
            key={path.id}
            style={[styles.pathCard, {borderLeftColor: pathColor}]}
            onPress={() => navigate('RuneDetail', {pathId: path.id})}>
            <Image 
              source={{uri: getRuneIconUrl(path.icon)}} 
              style={[styles.pathIcon, {tintColor: pathColor}]} 
            />
            <View style={styles.pathInfo}>
              <Text style={[styles.pathName, {color: pathColor}]}>
                {getRunePathName(path.key)}
              </Text>
              <Text style={styles.pathDesc}>{path.slots.length} 层符文</Text>
            </View>
            <Text style={styles.arrow}>→</Text>
          </TouchableOpacity>
        );
      })}
      <View style={styles.bottomSpace} />
    </ScrollView>
  );
}

动态颜色的应用

每个符文系卡片有三处使用了动态颜色:

  1. 左边框borderLeftColor: pathColor
  2. 图标tintColor: pathColor(给图标着色)
  3. 名称color: pathColor

这样每个符文系都有自己独特的视觉标识,用户一眼就能区分。

tintColor 的作用

tintColor 是 React Native Image 组件的属性,它会给图片着色。原理是把图片中的非透明像素都染成指定的颜色。这对于单色图标非常有用——我们只需要一套白色或黑色的图标,然后用 tintColor 动态改变颜色。

slots.length 的展示

显示"4 层符文"这样的信息,让用户知道点进去会看到多少内容。这是一个小细节,但能提升用户体验。

符文描述的清理

符文的描述文本也是带 HTML 标签的,需要清理:

// 清理符文描述
export function cleanRuneDesc(desc: string): string {
  if (!desc) return '';
  return desc
    .replace(/<br>/gi, '\n')
    .replace(/<[^>]+>/g, '')
    .trim();
}

这个函数和之前的 stripHtml 类似,把 <br> 转成换行符,移除其他 HTML 标签。

图标 URL 的生成

符文图标的 URL 格式和英雄、装备不太一样:

export function getRuneIconUrl(iconPath: string): string {
  return `https://ddragon.leagueoflegends.com/cdn/img/${iconPath}`;
}

API 返回的 icon 字段已经包含了相对路径(如 perk-images/Styles/Precision/Precision.png),我们只需要拼接上 CDN 的基础 URL。

为什么用 ScrollView 而不是 FlatList?

符文系只有 5 个,数据量很小,用 ScrollView 就够了。FlatList 的优势是虚拟化渲染,适合长列表。对于只有几个元素的列表,FlatList 反而会增加不必要的复杂度。

选择组件的原则:

  • < 20 个元素:ScrollView
  • 20-100 个元素:FlatList
  • > 100 个元素:FlatList + 性能优化(如 getItemLayout)

交互设计

点击符文系卡片后,跳转到符文详情页,传入 pathId 参数:

onPress={() => navigate('RuneDetail', {pathId: path.id})}

符文详情页会根据这个 ID 找到对应的符文系,展示其下所有的符文。

卡片右边的箭头(→)是一个视觉提示,告诉用户这个卡片是可点击的,点击后会进入下一级页面。

主题适配

这个页面完全支持深色/浅色模式切换:

  • 背景色:跟随主题变化
  • 卡片背景:跟随主题变化
  • 边框颜色:跟随主题变化
  • 文字颜色:跟随主题变化
  • 符文系颜色:固定不变(这是符文系的标识色)

符文系的颜色不跟随主题变化是有意为之的。这些颜色是游戏中的标准色,玩家已经形成了认知(比如"红色是主宰系"),改变会造成困惑。

小结

符文系统页面展示了几个重要的技术点:

  1. 数据结构理解:符文系统有三层嵌套结构,需要先理解再开发
  2. 常量映射:用 TypeScript 常量对象做中英文映射和颜色映射
  3. 动态颜色:每个符文系有自己的主题色,通过内联样式动态应用
  4. tintColor:给图标动态着色,一套图标多种颜色
  5. 主题适配:页面背景跟随主题,符文系颜色保持固定

下一篇我们来实现符文详情页,展示每个符文系下的所有符文及其效果。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐