在这里插入图片描述

今天我们用 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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐