React Native for OpenHarmony 实战:字数统计实现
本文介绍了一个基于React Native实现的字数统计工具,具有以下特点: 功能丰富:可统计总字符数、中英文字符、数字、标点符号等8种指标 动画效果:采用3D翻转卡片式入场动画和输入框脉冲动画 状态设计:使用单一数据源原则,所有统计结果实时计算得出 性能优化:动画使用原生驱动,确保60fps流畅运行 跨平台兼容:核心统计逻辑与鸿蒙ArkTS通用 实现要点: 使用Animated API实现卡片序

今天我们用 React Native 实现一个字数统计工具,不仅能统计总字符数,还能分析中文、英文、数字、标点符号的数量,并且带有炫酷的 3D 翻转入场动画。
状态设计
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, StyleSheet, ScrollView, Animated } from 'react-native';
export const WordCounter: React.FC = () => {
const [text, setText] = useState('');
const cardAnims = useRef(Array(8).fill(0).map(() => new Animated.Value(0))).current;
const pulseAnim = useRef(new Animated.Value(1)).current;
状态设计非常简洁:
文本内容 text:存储用户输入的文本,初始为空字符串。所有统计指标都从这个状态计算得出。
卡片动画数组 cardAnims:8 个统计卡片各有一个动画值,初始都是 0(隐藏状态)。用 Array(8).fill(0).map() 创建 8 个独立的 Animated.Value,这样每个卡片可以独立控制动画时机。
脉冲动画 pulseAnim:输入框的呼吸动画,初始值 1 表示正常大小。这个动画会循环播放,让输入框看起来"活着"。
为什么不把统计结果存在状态里?因为统计结果可以从 text 实时计算,不需要额外的状态。这符合 React 的"单一数据源"原则,避免状态同步问题。
入场动画
useEffect(() => {
// 卡片入场动画
cardAnims.forEach((anim, i) => {
setTimeout(() => {
Animated.spring(anim, { toValue: 1, friction: 5, useNativeDriver: true }).start();
}, i * 80);
});
组件挂载时触发入场动画,8 个卡片依次翻转出现。
延迟启动:setTimeout 让每个卡片延迟 i * 80 毫秒启动。第一个卡片立即启动(0ms),第二个延迟 80ms,第三个延迟 160ms,以此类推。这样形成"波浪"效果,比同时出现更有层次感。
弹簧动画:Animated.spring 创建弹性动画,friction: 5 设置摩擦力。弹簧动画比线性动画更自然,有"回弹"的感觉。
原生驱动:useNativeDriver: true 让动画在原生层执行,性能更好,60fps 不掉帧。
为什么用 forEach 而不是 for 循环?因为 forEach 的回调函数会创建闭包,捕获当前的 i 值。如果用 for 循环,setTimeout 的回调会共享同一个 i,导致所有卡片同时启动。
脉冲动画
// 脉冲动画
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, { toValue: 1.02, duration: 1000, useNativeDriver: true }),
Animated.timing(pulseAnim, { toValue: 1, duration: 1000, useNativeDriver: true }),
])
).start();
}, []);
脉冲动画让输入框缓慢放大(1 → 1.02)再缩小(1.02 → 1),每个方向 1 秒,总共 2 秒一个周期。
循环播放:Animated.loop 让动画无限循环。不需要手动重启,也不需要监听动画结束事件。
序列动画:Animated.sequence 让两个动画依次执行。先放大,再缩小,形成"呼吸"效果。
时长选择:1 秒的时长比较舒缓,不会让用户感到烦躁。如果太快(比如 200ms),会显得很急促;如果太慢(比如 3 秒),又不够明显。
缩放幅度:1.02 表示放大 2%,这是一个很微妙的变化。太大会干扰用户输入,太小又看不出效果。
这个动画的作用是吸引用户注意力,提示"这里可以输入"。当页面刚打开时,用户可能不知道从哪里开始,脉冲动画就像一个无声的引导。
统计指标计算
const stats = [
{ icon: '📝', value: text.length, label: '总字符', color: '#4A90D9' },
{ icon: '✂️', value: text.replace(/\s/g, '').length, label: '不含空格', color: '#00b894' },
{ icon: '📖', value: text.trim() ? text.trim().split(/\s+/).length : 0, label: '单词数', color: '#e17055' },
{ icon: '📄', value: text ? text.split('\n').length : 0, label: '行数', color: '#6c5ce7' },
{ icon: '🀄', value: (text.match(/[\u4e00-\u9fa5]/g) || []).length, label: '中文字符', color: '#fd79a8' },
{ icon: '🔤', value: (text.match(/[a-zA-Z]/g) || []).length, label: '英文字母', color: '#00cec9' },
{ icon: '🔢', value: (text.match(/[0-9]/g) || []).length, label: '数字', color: '#fdcb6e' },
{ icon: '❗', value: (text.match(/[^\w\s\u4e00-\u9fa5]/g) || []).length, label: '标点符号', color: '#a29bfe' },
];
这个数组定义了 8 个统计指标,每个指标包含图标、数值、标签、颜色。
总字符:text.length 是最简单的统计,包括所有字符(空格、换行、标点等)。
不含空格:text.replace(/\s/g, '') 删除所有空白字符(空格、制表符、换行),然后计算长度。\s 匹配所有空白字符,g 表示全局替换。这个指标在某些场景下更有意义,比如统计有效内容。
单词数:先用 trim() 删除首尾空白,避免空字符串被算成一个单词。然后用 split(/\s+/) 按空白字符分割,\s+ 表示一个或多个空白字符。如果文本为空,返回 0 而不是 1。
行数:split('\n') 按换行符分割,数组长度就是行数。注意空文本也算 1 行,所以要先判断 text 是否为空。
中文字符:/[\u4e00-\u9fa5]/g 匹配所有中文字符。Unicode 范围 \u4e00-\u9fa5 包含常用汉字。match() 返回匹配数组,如果没有匹配返回 null,所以用 || [] 提供默认值。
英文字母:/[a-zA-Z]/g 匹配所有英文字母,不区分大小写。
数字:/[0-9]/g 匹配所有数字字符。
标点符号:/[^\w\s\u4e00-\u9fa5]/g 匹配所有非单词字符、非空白字符、非中文字符。\w 包含字母、数字、下划线,^ 表示取反。这样就能匹配到标点符号、特殊字符等。
每个指标都有独特的颜色,让卡片更有辨识度。颜色选择遵循"彩虹"原则,相邻卡片的颜色有明显区别。
鸿蒙 ArkTS 对比:统计计算
@State text: string = ''
get stats() {
return [
{ icon: '📝', value: this.text.length, label: '总字符', color: '#4A90D9' },
{ icon: '✂️', value: this.text.replace(/\s/g, '').length, label: '不含空格', color: '#00b894' },
{ icon: '📖', value: this.text.trim() ? this.text.trim().split(/\s+/).length : 0, label: '单词数', color: '#e17055' },
{ icon: '📄', value: this.text ? this.text.split('\n').length : 0, label: '行数', color: '#6c5ce7' },
{ icon: '🀄', value: (this.text.match(/[\u4e00-\u9fa5]/g) || []).length, label: '中文字符', color: '#fd79a8' },
{ icon: '🔤', value: (this.text.match(/[a-zA-Z]/g) || []).length, label: '英文字母', color: '#00cec9' },
{ icon: '🔢', value: (this.text.match(/[0-9]/g) || []).length, label: '数字', color: '#fdcb6e' },
{ icon: '❗', value: (this.text.match(/[^\w\s\u4e00-\u9fa5]/g) || []).length, label: '标点符号', color: '#a29bfe' },
]
}
ArkTS 中用 get 定义计算属性,当 text 变化时自动重新计算。统计逻辑完全一样,正则表达式、字符串方法都是 JavaScript 标准 API,跨平台通用。
界面渲染:头部和输入框
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerIcon}>📊</Text>
<Text style={styles.headerTitle}>字数统计</Text>
<Text style={styles.headerSubtitle}>实时统计文本信息</Text>
</View>
<Animated.View style={[styles.inputCard, { transform: [{ scale: pulseAnim }] }]}>
<TextInput
style={styles.input}
value={text}
onChangeText={setText}
multiline
placeholder="在此输入或粘贴文本..."
placeholderTextColor="#666"
/>
</Animated.View>
头部区域和之前的工具类似,包含图标、标题、副标题。📊 图标表示"统计"的概念。
输入框动画:用 Animated.View 包裹输入框卡片,应用脉冲动画。注意动画应用在卡片上,而不是输入框本身,这样不会影响输入框的内部布局。
多行输入:multiline 让输入框支持换行,minHeight: 180 确保有足够的空间。用户可以输入长文本,也可以粘贴文章。
实时更新:onChangeText={setText} 直接传入 setter 函数,每次输入都会更新状态,触发统计指标重新计算。这就是"实时统计"的实现原理。
占位符:提示用户"在此输入或粘贴文本",明确告诉用户这个工具的用法。
统计卡片渲染
<View style={styles.statsGrid}>
{stats.map((stat, i) => (
<Animated.View
key={i}
style={[styles.statCard, {
transform: [
{ scale: cardAnims[i] },
{ perspective: 1000 },
{ rotateY: cardAnims[i].interpolate({ inputRange: [0, 1], outputRange: ['90deg', '0deg'] }) },
],
opacity: cardAnims[i],
}]}
>
<Text style={styles.statIcon}>{stat.icon}</Text>
<Text style={[styles.statValue, { color: stat.color }]}>{stat.value}</Text>
<Text style={styles.statLabel}>{stat.label}</Text>
</Animated.View>
))}
</View>
</ScrollView>
);
};
统计卡片用网格布局,每行两个卡片。map 遍历 stats 数组,为每个指标生成一个卡片。
3D 翻转动画:
scale:从 0 到 1,卡片从无到有perspective:设置透视距离,让 3D 效果更明显rotateY:从 90 度到 0 度,卡片从侧面翻转到正面
插值映射:cardAnims[i].interpolate() 把动画值(0 到 1)映射成旋转角度(90deg 到 0deg)。当动画值是 0 时,卡片侧面朝向用户(90 度),完全看不见;当动画值是 1 时,卡片正面朝向用户(0 度),完全显示。
透明度动画:opacity 也从 0 到 1,配合缩放和旋转,形成"淡入 + 翻转 + 放大"的组合效果。
卡片内容:
- 顶部是 emoji 图标,28 号字体,很醒目
- 中间是数值,32 号粗体,用指标的专属颜色
- 底部是标签,14 号灰色文字
这种布局让用户一眼就能看到关键信息(数值),图标和标签起辅助说明作用。
鸿蒙 ArkTS 对比:卡片渲染
Grid() {
ForEach(this.stats, (stat: StatItem, index: number) => {
GridItem() {
Column() {
Text(stat.icon)
.fontSize(28)
.margin({ bottom: 8 })
Text(stat.value.toString())
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor(stat.color)
Text(stat.label)
.fontSize(14)
.fontColor('#888888')
.margin({ top: 8 })
}
.width('100%')
.padding(20)
.backgroundColor('#1a1a3e')
.borderRadius(16)
.border({ width: 1, color: '#3a3a6a' })
}
.rotate({ y: this.cardRotations[index] })
.opacity(this.cardOpacities[index])
})
}
.columnsTemplate('1fr 1fr')
.rowsGap(8)
.columnsGap(8)
鸿蒙用 Grid 组件实现网格布局,columnsTemplate('1fr 1fr') 定义两列等宽。ForEach 遍历数据,GridItem 包裹每个卡片。
3D 旋转用 rotate({ y: angle }) 实现,配合 animateTo 函数可以实现和 React Native 类似的动画效果。
样式定义:容器和头部
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0f0f23', padding: 20 },
header: { alignItems: 'center', marginBottom: 24 },
headerIcon: { fontSize: 50, marginBottom: 8 },
headerTitle: { fontSize: 28, fontWeight: '700', color: '#fff' },
headerSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },
容器用深蓝色背景,和其他工具保持一致。头部元素居中对齐,形成清晰的视觉层次。
样式定义:输入框
inputCard: {
backgroundColor: '#1a1a3e',
borderRadius: 20,
padding: 4,
marginBottom: 20,
borderWidth: 1,
borderColor: '#3a3a6a',
},
input: {
backgroundColor: '#252550',
padding: 16,
borderRadius: 16,
minHeight: 180,
fontSize: 16,
color: '#fff',
},
输入框用双层结构:外层是卡片容器,内层是实际的输入框。这样可以在卡片和输入框之间留出 4 像素的间隙,形成"内嵌"效果。
输入框的最小高度 180 像素,比一般的输入框高,因为字数统计通常处理较长的文本。
样式定义:统计卡片
statsGrid: { flexDirection: 'row', flexWrap: 'wrap' },
statCard: {
width: '48%',
margin: '1%',
backgroundColor: '#1a1a3e',
padding: 20,
borderRadius: 16,
alignItems: 'center',
borderWidth: 1,
borderColor: '#3a3a6a',
},
statIcon: { fontSize: 28, marginBottom: 8 },
statValue: { fontSize: 32, fontWeight: '700' },
statLabel: { fontSize: 14, color: '#888', marginTop: 8 },
});
网格容器用 flexWrap: 'wrap' 让卡片自动换行。每个卡片宽度 48%,左右各留 1% 的外边距,这样两个卡片加起来正好 100%(48% + 1% + 1% + 48% + 1% + 1% = 100%)。
卡片内容居中对齐,图标、数值、标签垂直排列。数值用 32 号粗体,是卡片的视觉焦点。
为什么用百分比而不是固定像素?因为不同设备的屏幕宽度不同,用百分比可以自适应。在小屏幕上,卡片会变窄;在大屏幕上,卡片会变宽,但始终保持两列布局。
小结
这个字数统计工具展示了 React Native 中实时计算和动画的结合。8 个统计指标通过正则表达式实时计算,3D 翻转入场动画让界面更生动。所有动画都用 useNativeDriver: true 在原生层执行,保持 60fps 流畅运行。在 OpenHarmony 平台上,正则表达式和字符串方法是 JavaScript 标准 API,跨平台兼容性很好。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)