React Native for OpenHarmony 实战:数学练习实现

今天我们用 React Native 实现一个数学练习工具,支持加减乘除四则运算,三种难度。

状态设计

tsx 复制代码
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Animated } from 'react-native';

type Operation = '+' | '-' | '×' | '÷';

export const MathPractice: React.FC = () => {
  const [num1, setNum1] = useState(0);
  const [num2, setNum2] = useState(0);
  const [operation, setOperation] = useState<Operation>('+');
  const [answer, setAnswer] = useState('');
  const [result, setResult] = useState<'correct' | 'wrong' | null>(null);
  const [score, setScore] = useState({ correct: 0, wrong: 0 });
  const [difficulty, setDifficulty] = useState<'easy' | 'medium' | 'hard'>('easy');
  
  const questionAnim = useRef(new Animated.Value(1)).current;
  const inputAnim = useRef(new Animated.Value(1)).current;
  const scoreAnim = useRef(new Animated.Value(1)).current;
  const shakeAnim = useRef(new Animated.Value(0)).current;

状态设计包含题目、答案、结果、计分、难度。

题目状态

  • num1num2:两个操作数
  • operation:运算符,类型是 '+' | '-' | '×' | '÷'

答案和结果

  • answer:用户输入的答案,字符串类型
  • result:判断结果,'correct'(正确)、'wrong'(错误)或 null(未判断)

计分score 对象包含正确次数和错误次数。

难度difficulty 有三种:'easy'(简单)、'medium'(中等)、'hard'(困难)。

四个动画值

  • questionAnim:题目的缩放和淡入动画
  • inputAnim:输入框的缩放动画(代码中定义但未使用)
  • scoreAnim:计分板的缩放动画
  • shakeAnim:题目的摇晃动画

为什么用字面量类型定义运算符和难度 ?因为 TypeScript 的字面量类型让代码更安全。operation 只能是这 4 个运算符之一,difficulty 只能是这 3 种难度之一。如果写错,TypeScript 会报错。

动画函数

tsx 复制代码
  const animateQuestion = () => {
    questionAnim.setValue(0);
    Animated.spring(questionAnim, { toValue: 1, friction: 5, useNativeDriver: true }).start();
  };

  const animateCorrect = () => {
    Animated.sequence([
      Animated.timing(scoreAnim, { toValue: 1.2, duration: 150, useNativeDriver: true }),
      Animated.spring(scoreAnim, { toValue: 1, friction: 3, useNativeDriver: true }),
    ]).start();
  };

  const animateWrong = () => {
    Animated.sequence([
      Animated.timing(shakeAnim, { toValue: 1, duration: 50, useNativeDriver: true }),
      Animated.timing(shakeAnim, { toValue: -1, duration: 50, useNativeDriver: true }),
      Animated.timing(shakeAnim, { toValue: 1, duration: 50, useNativeDriver: true }),
      Animated.timing(shakeAnim, { toValue: 0, duration: 50, useNativeDriver: true }),
    ]).start();
  };

三个动画函数:题目动画、正确动画、错误动画。

题目动画 :重置动画值为 0,然后弹簧动画到 1。friction: 5 让弹簧有明显的回弹效果,题目从无到有、从小到大地出现。

正确动画 :序列动画,计分板放大到 120%(150ms),再弹回到 100%。friction: 3 让弹簧有明显的回弹效果,庆祝答对。

错误动画:序列动画,题目左右摇晃。从 0 到 1(向右)、到 -1(向左)、到 1(向右)、回到 0,总共 200ms。摇晃 3 次,提示答错。

为什么正确动画放大计分板,错误动画摇晃题目?因为正确时,用户关注的是分数增加,放大计分板强调这个变化。错误时,用户关注的是题目,摇晃题目提示"这道题答错了"。

生成题目

tsx 复制代码
  const generateQuestion = () => {
    animateQuestion();
    const ops: Operation[] = ['+', '-', '×', '÷'];
    const op = ops[Math.floor(Math.random() * ops.length)];
    let n1, n2;
    const max = difficulty === 'easy' ? 10 : difficulty === 'medium' ? 50 : 100;
    
    if (op === '÷') {
      n2 = Math.floor(Math.random() * (max / 2)) + 1;
      n1 = n2 * (Math.floor(Math.random() * 10) + 1);
    } else if (op === '×') {
      n1 = Math.floor(Math.random() * Math.sqrt(max)) + 1;
      n2 = Math.floor(Math.random() * Math.sqrt(max)) + 1;
    } else {
      n1 = Math.floor(Math.random() * max) + 1;
      n2 = Math.floor(Math.random() * max) + 1;
      if (op === '-' && n1 < n2) [n1, n2] = [n2, n1];
    }
    
    setNum1(n1);
    setNum2(n2);
    setOperation(op);
    setAnswer('');
    setResult(null);
  };

  useEffect(() => { generateQuestion(); }, [difficulty]);

生成题目函数根据难度和运算符生成合适的数字。

触发动画:先触发题目动画,让新题目有出现效果。

随机运算符:从 4 个运算符中随机选择一个。

难度对应的最大值

  • 简单:10
  • 中等:50
  • 困难:100

除法特殊处理

  • n2 = Math.floor(Math.random() * (max / 2)) + 1:除数是 1 到 max/2 的随机数
  • n1 = n2 * (Math.floor(Math.random() * 10) + 1):被除数是除数的倍数
  • 这样保证结果是整数,不会出现小数

为什么除法要保证整数结果?因为小数除法对小学生太难,而且输入小数不方便。整数除法更适合练习。

乘法特殊处理

  • Math.floor(Math.random() * Math.sqrt(max)) + 1:两个数都是 1 到 √max 的随机数
  • 这样保证乘积不超过 max

为什么乘法用平方根?因为如果两个数都是 1 到 max,乘积可能很大(比如 100 × 100 = 10000)。用平方根限制,乘积最大是 max(比如 10 × 10 = 100)。

减法特殊处理

  • 如果 n1 < n2,交换两个数
  • 这样保证结果是正数,不会出现负数

为什么减法要保证正数?因为负数对小学生太难。正数减法更适合练习。

重置状态:清空答案,清除结果。

难度变化时重新生成useEffect(() => { generateQuestion(); }, [difficulty]) 监听难度变化,切换难度时自动生成新题目。

计算正确答案

tsx 复制代码
  const getCorrectAnswer = () => {
    switch (operation) {
      case '+': return num1 + num2;
      case '-': return num1 - num2;
      case '×': return num1 * num2;
      case '÷': return num1 / num2;
    }
  };

根据运算符计算正确答案。用 switch 语句处理 4 种运算。

检查答案

tsx 复制代码
  const checkAnswer = () => {
    const correct = getCorrectAnswer();
    const userAnswer = parseFloat(answer);
    if (userAnswer === correct) {
      setResult('correct');
      setScore({ ...score, correct: score.correct + 1 });
      animateCorrect();
      setTimeout(generateQuestion, 1000);
    } else {
      setResult('wrong');
      setScore({ ...score, wrong: score.wrong + 1 });
      animateWrong();
    }
  };

检查答案函数比较用户答案和正确答案。

获取正确答案 :调用 getCorrectAnswer() 计算正确答案。

解析用户答案parseFloat(answer) 把字符串转成数字。

答对

  • 结果设为 'correct'
  • 正确次数加 1
  • 触发正确动画
  • 1 秒后自动生成新题目

答错

  • 结果设为 'wrong'
  • 错误次数加 1
  • 触发错误动画
  • 不自动生成新题目,让用户看到正确答案

为什么答对后自动生成新题目,答错后不自动生成?因为答对说明用户掌握了,可以继续下一题。答错说明用户不会,需要看正确答案学习,不应该立即跳到下一题。

摇晃插值

tsx 复制代码
  const shake = shakeAnim.interpolate({ inputRange: [-1, 0, 1], outputRange: ['-5deg', '0deg', '5deg'] });

插值把动画值 -1 到 1 映射到旋转角度 -5 度到 5 度。题目左右旋转,营造"摇晃"效果。

界面渲染:头部和计分板

tsx 复制代码
  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerIcon}>🧮</Text>
        <Text style={styles.headerTitle}>数学练习</Text>
      </View>

      <Animated.View style={[styles.scoreBoard, { transform: [{ scale: scoreAnim }] }]}>
        <View style={styles.scoreItem}>
          <Text style={styles.scoreEmoji}>✅</Text>
          <Text style={styles.scoreCorrect}>{score.correct}</Text>
        </View>
        <View style={styles.scoreItem}>
          <Text style={styles.scoreEmoji}>❌</Text>
          <Text style={styles.scoreWrong}>{score.wrong}</Text>
        </View>
      </Animated.View>

头部显示标题,计分板显示正确和错误次数。

计分板:应用缩放动画,答对时放大再缩回。

两个计分项

  • 正确:✅ 对勾 + 绿色数字
  • 错误:❌ 叉号 + 红色数字

为什么用绿色和红色?因为绿色代表"对",红色代表"错",全世界通用。用户看到颜色就知道是正确还是错误。

题目和输入框

tsx 复制代码
      <Animated.View style={[styles.question, {
        transform: [{ scale: questionAnim }, { rotate: shake }],
        opacity: questionAnim,
      }]}>
        <Text style={styles.questionText}>{num1} {operation} {num2} = ?</Text>
      </Animated.View>

      <Animated.View style={[styles.inputWrapper, { transform: [{ scale: inputAnim }] }]}>
        <TextInput
          style={[styles.input, result === 'correct' && styles.inputCorrect, result === 'wrong' && styles.inputWrong]}
          value={answer}
          onChangeText={setAnswer}
          keyboardType="numeric"
          placeholder="答案"
          placeholderTextColor="#666"
        />
      </Animated.View>

      {result === 'wrong' && <Text style={styles.correctAnswer}>正确答案: {getCorrectAnswer()}</Text>}

题目

  • 应用缩放、旋转、透明度动画
  • 显示"num1 operation num2 = ?"

输入框

  • keyboardType="numeric":弹出数字键盘
  • 根据结果应用不同样式:
    • 正确:绿色背景和边框
    • 错误:红色背景和边框

正确答案:只在答错时显示,红色文字。

为什么输入框用不同颜色?因为颜色能快速传达结果。绿色表示"对",红色表示"错",用户看到颜色就知道答案是否正确。

按钮和难度选择

tsx 复制代码
      <View style={styles.btnRow}>
        <TouchableOpacity style={styles.btn} onPress={checkAnswer} activeOpacity={0.8}>
          <Text style={styles.btnText}>✅ 确认</Text>
        </TouchableOpacity>
        <TouchableOpacity style={[styles.btn, styles.btnSecondary]} onPress={generateQuestion} activeOpacity={0.8}>
          <Text style={styles.btnText}>⏭️ 跳过</Text>
        </TouchableOpacity>
      </View>

      <View style={styles.difficulty}>
        {([
          { key: 'easy', label: '简单', icon: '😊' },
          { key: 'medium', label: '中等', icon: '🤔' },
          { key: 'hard', label: '困难', icon: '😈' },
        ] as const).map(d => (
          <TouchableOpacity
            key={d.key}
            style={[styles.diffBtn, difficulty === d.key && styles.diffBtnActive]}
            onPress={() => setDifficulty(d.key)}
            activeOpacity={0.7}
          >
            <Text style={styles.diffIcon}>{d.icon}</Text>
            <Text style={[styles.diffText, difficulty === d.key && styles.diffTextActive]}>{d.label}</Text>
          </TouchableOpacity>
        ))}
      </View>
    </View>
  );
};

两个按钮

  • 确认:蓝色背景,点击检查答案
  • 跳过:灰色背景,点击生成新题目

为什么需要跳过按钮?因为用户可能遇到不会的题目,想跳过。跳过按钮让用户有选择权,不会卡在一道题上。

难度选择:三个按钮,点击切换难度。

难度数组 :用 as const 断言为常量,让 TypeScript 推断出精确的类型。

激活状态:当前难度的按钮用蓝色背景,白色文字。

为什么用不同的 emoji?因为 emoji 能传达难度的"感觉"。😊 笑脸表示"轻松",🤔 思考表示"需要动脑",😈 恶魔表示"很难"。

鸿蒙 ArkTS 对比:题目生成

typescript 复制代码
@State num1: number = 0
@State num2: number = 0
@State operation: string = '+'
@State answer: string = ''
@State result: string | null = null
@State score: { correct: number, wrong: number } = { correct: 0, wrong: 0 }
@State difficulty: string = 'easy'

generateQuestion() {
  const ops = ['+', '-', '×', '÷']
  const op = ops[Math.floor(Math.random() * ops.length)]
  let n1, n2
  const max = this.difficulty === 'easy' ? 10 : this.difficulty === 'medium' ? 50 : 100
  
  if (op === '÷') {
    n2 = Math.floor(Math.random() * (max / 2)) + 1
    n1 = n2 * (Math.floor(Math.random() * 10) + 1)
  } else if (op === '×') {
    n1 = Math.floor(Math.random() * Math.sqrt(max)) + 1
    n2 = Math.floor(Math.random() * Math.sqrt(max)) + 1
  } else {
    n1 = Math.floor(Math.random() * max) + 1
    n2 = Math.floor(Math.random() * max) + 1
    if (op === '-' && n1 < n2) [n1, n2] = [n2, n1]
  }
  
  this.num1 = n1
  this.num2 = n2
  this.operation = op
  this.answer = ''
  this.result = null
}

checkAnswer() {
  const correct = this.getCorrectAnswer()
  const userAnswer = parseFloat(this.answer)
  if (userAnswer === correct) {
    this.result = 'correct'
    this.score.correct += 1
    setTimeout(() => this.generateQuestion(), 1000)
  } else {
    this.result = 'wrong'
    this.score.wrong += 1
  }
}

ArkTS 中的题目生成和检查逻辑完全一样,核心是随机数生成、特殊处理、答案比较。Math.random()Math.sqrt()parseFloat() 都是标准 JavaScript API,跨平台通用。

样式定义

tsx 复制代码
const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23', padding: 20, alignItems: 'center', justifyContent: 'center' },
  header: { alignItems: 'center', marginBottom: 20 },
  headerIcon: { fontSize: 40, marginBottom: 4 },
  headerTitle: { fontSize: 24, fontWeight: '700', color: '#fff' },
  scoreBoard: { flexDirection: 'row', marginBottom: 30, backgroundColor: '#1a1a3e', borderRadius: 16, padding: 16 },
  scoreItem: { flexDirection: 'row', alignItems: 'center', marginHorizontal: 20 },
  scoreEmoji: { fontSize: 24, marginRight: 8 },
  scoreCorrect: { color: '#2ecc71', fontSize: 28, fontWeight: '700' },
  scoreWrong: { color: '#e74c3c', fontSize: 28, fontWeight: '700' },
  question: { marginBottom: 24, backgroundColor: '#1a1a3e', borderRadius: 20, padding: 30 },
  questionText: { color: '#fff', fontSize: 48, fontWeight: '700' },
  inputWrapper: { marginBottom: 16 },
  input: { backgroundColor: '#1a1a3e', color: '#fff', fontSize: 32, padding: 16, borderRadius: 16, width: 200, textAlign: 'center', borderWidth: 2, borderColor: '#3a3a6a' },
  inputCorrect: { backgroundColor: 'rgba(46, 204, 113, 0.2)', borderColor: '#2ecc71' },
  inputWrong: { backgroundColor: 'rgba(231, 76, 60, 0.2)', borderColor: '#e74c3c' },
  correctAnswer: { color: '#e74c3c', marginBottom: 16, fontSize: 16 },
  btnRow: { flexDirection: 'row', marginBottom: 30 },
  btn: { backgroundColor: '#4A90D9', paddingVertical: 14, paddingHorizontal: 24, borderRadius: 12, marginHorizontal: 8 },
  btnSecondary: { backgroundColor: '#666' },
  btnText: { color: '#fff', fontSize: 16, fontWeight: '700' },
  difficulty: { flexDirection: 'row' },
  diffBtn: { padding: 14, marginHorizontal: 6, backgroundColor: '#1a1a3e', borderRadius: 12, alignItems: 'center', minWidth: 80 },
  diffBtnActive: { backgroundColor: '#4A90D9' },
  diffIcon: { fontSize: 24, marginBottom: 4 },
  diffText: { color: '#888', fontSize: 12 },
  diffTextActive: { color: '#fff', fontWeight: '600' },
});

容器用深蓝黑色背景,居中对齐。题目字号 48,很大,醒目。输入框字号 32,居中对齐,宽度 200。正确输入框用半透明绿色背景和绿色边框,错误输入框用半透明红色背景和红色边框。

小结

这个数学练习工具展示了题目生成和答案检查的实现。根据运算符特殊处理数字生成,除法保证整数结果,乘法用平方根限制,减法保证正数。三种难度控制数字范围,答对自动下一题,答错显示正确答案。


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

相关推荐
CDwenhuohuo2 小时前
安卓app巨坑 nvue后者页面要写画笔绘制功能nvue canvas
前端·javascript·vue.js
弓.长.2 小时前
React Native 鸿蒙跨平台开发:长按菜单效果
react native·react.js·harmonyos
Never_Satisfied2 小时前
在JavaScript / HTML中,HTML元素自定义属性使用指南
开发语言·javascript·html
前端 贾公子2 小时前
husky 9.0升级指南
javascript
Amumu121382 小时前
React Router 6介绍
前端·react.js·前端框架
弓.长.2 小时前
React Native 鸿蒙跨平台开发:实现一个模拟计算器
react native·react.js·harmonyos
南村群童欺我老无力.2 小时前
Flutter 框架跨平台鸿蒙开发 - 打造表情包制作器应用
开发语言·javascript·flutter·华为·harmonyos
摘星编程2 小时前
React Native for OpenHarmony 实战:MediaPlayer 播放器详解
javascript·react native·react.js
TAEHENGV3 小时前
React Native for OpenHarmony 实战:反应测试实现
javascript·react native·react.js