RN for OpenHarmony英雄联盟助手App实战:背景故事实现

案例开源地址:https://atomgit.com/nutpi/rn_openharmony_lol
每个英雄都有自己的故事。盖伦是德玛西亚的守护者,亚索是被冤枉的剑客,金克丝是疯狂的爆破专家……这些背景故事让英雄不再只是一堆技能数值,而是有血有肉的角色。
背景故事页要做的就是把这些故事以最佳的阅读体验呈现出来。这篇文章我们来实现这个页面,重点聊聊长文本的排版、阅读体验优化、以及 ImageBackground 的进阶用法。
阅读体验的设计考量
在开始写代码之前,先想想什么样的阅读体验是好的:
- 舒适的字号和行高:太小看不清,太大一屏显示不了几行
- 合适的行宽:一行太长眼睛追踪困难,太短又要频繁换行
- 足够的对比度:文字和背景要有足够的对比度
- 视觉层次:标题、正文要有明显区分
带着这些考量,我们来实现页面。
页面结构
import React, {useEffect, useState} from 'react';
import {
View,
Text,
ScrollView,
ImageBackground,
StyleSheet,
Dimensions,
} from 'react-native';
import {colors} from '../../styles/colors';
import {useApp} from '../../context/AppContext';
import {useNavigation} from '../../context/NavigationContext';
import {championApi} from '../../api';
import {getChampionSplashUrl} from '../../utils/image';
import {Loading} from '../../components/common';
import type {ChampionDetail} from '../../models/Champion';
const {width: SCREEN_WIDTH} = Dimensions.get('window');
导入的组件说明:
ScrollView:故事内容可能很长,需要滚动查看ImageBackground:在头部显示英雄原画作为背景Dimensions:获取屏幕宽度,用于设置头部图片尺寸
export function ChampionStoryPage() {
const {state} = useApp();
const {params} = useNavigation();
const [champion, setChampion] = useState<ChampionDetail | null>(null);
const [loading, setLoading] = useState(true);
const championId = params.championId as string;
状态设计:
这个页面的状态和之前几个页面一样简单:
champion:存储英雄详情数据loading:控制加载状态背景故事在
champion.lore字段中,是一个纯文本字符串。
数据加载
useEffect(() => {
async function loadDetail() {
if (!state.version || !championId) return;
setLoading(true);
const detail = await championApi.getChampionDetail(
state.version,
championId,
);
setChampion(detail);
setLoading(false);
}
loadDetail();
}, [state.version, championId]);
为什么不从上一页传递故事内容?
背景故事通常有几百甚至上千字,通过路由参数传递这么长的文本不太合适。而且英雄详情数据会被缓存,重新请求的开销很小。
另外,独立加载数据让这个页面可以作为独立入口使用(比如从搜索结果直接跳转)。
头部设计
头部使用英雄原画作为背景,叠加英雄名称和称号:
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
{/* 头部 */}
<ImageBackground
source={{uri: getChampionSplashUrl(championId, 0)}}
style={styles.header}>
<View style={styles.headerOverlay}>
<Text style={styles.name}>{champion.name}</Text>
<Text style={styles.title}>{champion.title}</Text>
</View>
</ImageBackground>
ImageBackground 的结构:
ImageBackground 本质上是一个带背景图的 View,它的子元素会显示在图片上方。我们在里面放了一个半透明遮罩(headerOverlay),遮罩里是文字。
这种"图片 + 遮罩 + 文字"的组合是很常见的设计模式,能确保文字在任何背景图上都清晰可读。
为什么用默认皮肤的原画?
getChampionSplashUrl(championId, 0)中的0表示默认皮肤。背景故事讲的是英雄的"本体",用默认皮肤最合适。如果用其他皮肤(比如"星之守护者"系列),视觉上会和故事内容不搭。
头部样式详解
header: {
width: SCREEN_WIDTH,
height: 200,
justifyContent: 'flex-end',
},
headerOverlay: {
backgroundColor: 'rgba(0, 0, 0, 0.6)',
padding: 16,
},
name: {
fontSize: 24,
fontWeight: 'bold',
color: colors.textGold,
marginBottom: 4,
},
title: {
fontSize: 16,
color: colors.textSecondary,
},
头部高度的选择:
height: 200是一个平衡点:
- 太矮(如 120):原画显示不完整,视觉冲击力不够
- 太高(如 300):占用太多屏幕空间,故事内容要滚动很多才能看到
200px 大约是屏幕高度的 1/4,既能展示原画的精华部分,又不会喧宾夺主。
justifyContent: ‘flex-end’ 的作用:
让遮罩和文字靠底部显示。这样文字会出现在原画的下半部分,通常是英雄身体的位置,不会遮挡英雄的脸。
遮罩透明度的选择:
rgba(0, 0, 0, 0.6)是 60% 透明度的黑色。这个值是经过测试的:
- 太透明(如 0.3):文字在浅色背景上看不清
- 太不透明(如 0.8):原画几乎看不见,失去了背景图的意义
故事内容区域
{/* 故事内容 */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>📜 背景故事</Text>
<View style={styles.storyCard}>
<Text style={styles.storyText}>{champion.lore}</Text>
</View>
</View>
<View style={styles.bottomSpace} />
</ScrollView>
);
Emoji 作为图标:
📜是一个卷轴的 Emoji,和"背景故事"的主题很搭。在不引入图标库的情况下,Emoji 是一个简单有效的方案。
storyCard 的设计:
把故事内容放在一个卡片里,和页面背景区分开。卡片有自己的背景色、圆角和边框,形成一个独立的阅读区域。
故事文本样式
这是整个页面最重要的部分——文本的排版直接影响阅读体验:
storyText: {
fontSize: 15,
color: colors.textSecondary,
lineHeight: 26,
textAlign: 'justify',
},
字号选择:
fontSize: 15是一个适中的大小:
- 14px:稍微有点小,长时间阅读会累
- 15px:刚好,大多数用户都能舒适阅读
- 16px:也可以,但一行能显示的字数会减少
如果你的目标用户年龄偏大,可以考虑用 16px 或更大。
行高设置:
lineHeight: 26配合fontSize: 15,行高是字号的 1.73 倍。为什么要这么大的行高?因为中文阅读和英文不同:
- 中文没有单词间的空格,一行文字是连续的
- 较大的行高能让眼睛更容易追踪到下一行
- 也让段落看起来更"透气",不会有压迫感
两端对齐:
textAlign: 'justify'让文字两端对齐。这是中文排版的常见做法,让每行的左右边缘都整齐,视觉上更美观。注意:两端对齐在英文中可能会导致单词间距不均匀,但在中文中效果很好。
文字颜色:
用
textSecondary而不是textPrimary。纯白色文字在深色背景上对比度太高,长时间阅读会刺眼。稍微灰一点的颜色更舒适。
内容区域样式
content: {
padding: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.textPrimary,
marginBottom: 12,
},
storyCard: {
backgroundColor: colors.backgroundCard,
borderRadius: 8,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
},
卡片内边距:
padding: 16让文字不会贴着卡片边缘。这个间距和页面的整体 padding 一致,保持视觉统一。
标题和正文的区分:
标题用
textPrimary(更亮),正文用textSecondary(稍暗)。这种对比让用户一眼就能区分标题和内容。
长文本的处理
有些英雄的背景故事非常长,可能有上千字。我们需要确保长文本能正确显示:
<Text style={styles.storyText}>{champion.lore}</Text>
React Native 的 Text 组件特性:
Text 组件会自动处理文本换行,不需要我们手动处理。只要设置好宽度(通过父容器的 padding 间接设置),文本就会在合适的位置换行。
段落分隔:
拳头 API 返回的故事文本通常是一整段,没有分段。如果你想要更好的阅读体验,可以尝试按句号分段:
const paragraphs = champion.lore.split('。').filter(p => p.trim()); {paragraphs.map((p, i) => ( <Text key={i} style={styles.storyText}> {p}。 </Text> ))}不过这种处理可能会破坏原有的语义,需要谨慎使用。
阅读进度指示
如果故事很长,用户可能想知道自己读到哪里了。可以添加一个阅读进度条:
const [scrollProgress, setScrollProgress] = useState(0);
<ScrollView
onScroll={(event) => {
const {contentOffset, contentSize, layoutMeasurement} = event.nativeEvent;
const progress = contentOffset.y / (contentSize.height - layoutMeasurement.height);
setScrollProgress(Math.min(Math.max(progress, 0), 1));
}}
scrollEventThrottle={16}>
{/* ... */}
</ScrollView>
<View style={[styles.progressBar, {width: `${scrollProgress * 100}%`}]} />
scrollEventThrottle 的作用:
这个属性控制 onScroll 事件的触发频率。16 表示每 16 毫秒最多触发一次(约 60fps)。值越小越流畅,但也越耗性能。
为什么当前没有实现?
大多数英雄的背景故事在 2-3 屏内就能读完,进度条的意义不大。如果你的内容更长(比如小说阅读 App),进度条会更有价值。
字体大小调节
有些用户可能想要更大或更小的字体。可以添加字体大小调节功能:
const [fontSize, setFontSize] = useState(15);
<View style={styles.fontSizeControl}>
<TouchableOpacity onPress={() => setFontSize(f => Math.max(f - 1, 12))}>
<Text>A-</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setFontSize(f => Math.min(f + 1, 20))}>
<Text>A+</Text>
</TouchableOpacity>
</View>
<Text style={[styles.storyText, {fontSize, lineHeight: fontSize * 1.73}]}>
{champion.lore}
</Text>
字号范围限制:
Math.max(f - 1, 12)确保字号不会小于 12px(太小看不清)Math.min(f + 1, 20)确保字号不会大于 20px(太大一屏显示不了几行)
行高联动:
字号变化时,行高也要按比例变化,保持 1.73 倍的比例。
夜间模式适配
背景故事页是阅读密集型页面,夜间模式尤其重要。我们的主题系统已经支持深色/浅色切换,这个页面会自动适配:
// 深色模式
storyText: {
color: colors.textSecondary, // 浅灰色文字
}
storyCard: {
backgroundColor: colors.backgroundCard, // 深灰色背景
}
// 浅色模式
storyText: {
color: colors.textSecondary, // 深灰色文字
}
storyCard: {
backgroundColor: colors.backgroundCard, // 浅灰色背景
}
颜色变量的好处:
因为我们用的是颜色变量(
colors.textSecondary)而不是硬编码的颜色值,主题切换时颜色会自动变化,不需要额外处理。
完整样式回顾
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
header: {
width: SCREEN_WIDTH,
height: 200,
justifyContent: 'flex-end',
},
headerOverlay: {
backgroundColor: 'rgba(0, 0, 0, 0.6)',
padding: 16,
},
name: {
fontSize: 24,
fontWeight: 'bold',
color: colors.textGold,
marginBottom: 4,
},
title: {
fontSize: 16,
color: colors.textSecondary,
},
content: {
padding: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.textPrimary,
marginBottom: 12,
},
storyCard: {
backgroundColor: colors.backgroundCard,
borderRadius: 8,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
},
storyText: {
fontSize: 15,
color: colors.textSecondary,
lineHeight: 26,
textAlign: 'justify',
},
bottomSpace: {
height: 20,
},
});
小结
背景故事页的核心是提供舒适的阅读体验。我们通过以下方式实现:
- 头部设计:用英雄原画作为背景,配合半透明遮罩显示英雄信息
- 文本排版:选择合适的字号(15px)和行高(26px),使用两端对齐
- 卡片容器:把故事内容放在卡片里,形成独立的阅读区域
- 颜色对比:使用稍暗的文字颜色,减少长时间阅读的视觉疲劳
下一篇我们来实现使用技巧页,展示英雄的使用建议和对抗技巧。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)