在这里插入图片描述

今天我们用 React Native 实现一个最大公约数和最小公倍数计算工具,使用辗转相除法,显示计算步骤。

状态设计

import React, { useState, useRef } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';

export const GcdLcm: React.FC = () => {
  const [num1, setNum1] = useState('');
  const [num2, setNum2] = useState('');
  const [result, setResult] = useState<{ gcd: number; lcm: number; steps: string[] } | null>(null);
  
  const buttonAnim = useRef(new Animated.Value(1)).current;
  const resultAnim = useRef(new Animated.Value(0)).current;
  const gcdAnim = useRef(new Animated.Value(0)).current;
  const lcmAnim = useRef(new Animated.Value(0)).current;

状态设计包含两个输入数字、计算结果、动画值。

两个输入数字num1num2 都是字符串类型,存储用户输入的数字。

计算结果result 是一个对象或 null

  • gcd:最大公约数(Greatest Common Divisor)
  • lcm:最小公倍数(Least Common Multiple)
  • steps:计算步骤数组

四个动画值

  • buttonAnim:按钮的缩放动画
  • resultAnim:结果卡片的缩放和透明度动画
  • gcdAnim:最大公约数的缩放动画
  • lcmAnim:最小公倍数的缩放动画

为什么 GCD 和 LCM 分别有动画值?因为要让它们依次出现,先显示结果卡片,再同时显示 GCD 和 LCM,营造"计算完成"的效果。

最大公约数算法

  const gcd = (a: number, b: number): number => {
    return b === 0 ? a : gcd(b, a % b);
  };

递归实现辗转相除法(欧几里得算法)。

递归终止条件:如果 b === 0,返回 a

递归计算gcd(b, a % b),用 b 和 a 除以 b 的余数继续计算。

为什么用辗转相除法?因为辗转相除法是计算最大公约数最高效的算法,时间复杂度 O(log n)。比暴力枚举(从小到大试除)快得多。

算法原理:如果 d 是 a 和 b 的公约数,那么 d 也是 b 和 a%b 的公约数。反之亦然。所以 gcd(a, b) = gcd(b, a%b)。不断递归,直到 b 为 0,此时 a 就是最大公约数。

举例:计算 gcd(48, 18)

  • gcd(48, 18) = gcd(18, 48 % 18) = gcd(18, 12)
  • gcd(18, 12) = gcd(12, 18 % 12) = gcd(12, 6)
  • gcd(12, 6) = gcd(6, 12 % 6) = gcd(6, 0)
  • gcd(6, 0) = 6

计算函数

  const calculate = () => {
    Animated.sequence([
      Animated.timing(buttonAnim, { toValue: 0.9, duration: 100, useNativeDriver: true }),
      Animated.spring(buttonAnim, { toValue: 1, friction: 3, useNativeDriver: true }),
    ]).start();
    
    const a = parseInt(num1);
    const b = parseInt(num2);
    if (isNaN(a) || isNaN(b) || a <= 0 || b <= 0) return;

计算按钮点击时,触发动画,验证输入。

按钮动画:序列动画,先缩小到 90%(100ms),再弹回到 100%。营造"按下"的感觉。

解析数字parseInt() 把字符串转成整数。

验证输入

  • isNaN(a)isNaN(b):不是数字
  • a <= 0b <= 0:不是正整数

如果验证失败,直接返回,不执行计算。

为什么要求正整数?因为最大公约数和最小公倍数只对正整数有意义。负数、0、小数的最大公约数定义不明确。

生成计算步骤

    const steps: string[] = [];
    let x = a, y = b;
    while (y !== 0) {
      steps.push(`${x} = ${y} × ${Math.floor(x / y)} + ${x % y}`);
      const temp = y;
      y = x % y;
      x = temp;
    }

用循环模拟辗转相除法,记录每一步。

初始化x = a, y = b,复制输入的两个数。

循环条件y !== 0,当 y 为 0 时停止。

记录步骤

  • ${x} = ${y} × ${Math.floor(x / y)} + ${x % y}
  • 表示"x = y × 商 + 余数"
  • 比如 48 = 18 × 2 + 12

更新变量

  • temp = y:保存 y
  • y = x % y:y 变成余数
  • x = temp:x 变成原来的 y

为什么要记录步骤?因为步骤能帮助用户理解算法。很多人知道辗转相除法,但不知道具体怎么算。显示步骤让用户看到"每一步做了什么",增加工具的教育价值。

举例:计算 gcd(48, 18)

  • 第 1 步:48 = 18 × 2 + 12,x = 18, y = 12
  • 第 2 步:18 = 12 × 1 + 6,x = 12, y = 6
  • 第 3 步:12 = 6 × 2 + 0,x = 6, y = 0
  • 循环结束,x = 6 就是最大公约数

计算结果和动画

    const gcdResult = gcd(a, b);
    const lcmResult = (a * b) / gcdResult;
    
    resultAnim.setValue(0);
    gcdAnim.setValue(0);
    lcmAnim.setValue(0);
    
    Animated.sequence([
      Animated.spring(resultAnim, { toValue: 1, friction: 5, useNativeDriver: true }),
      Animated.parallel([
        Animated.spring(gcdAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
        Animated.spring(lcmAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
      ]),
    ]).start();
    
    setResult({ gcd: gcdResult, lcm: lcmResult, steps });
  };

计算最大公约数和最小公倍数,触发动画。

计算最大公约数:调用 gcd(a, b) 函数。

计算最小公倍数:用公式 LCM = (a × b) / GCD

为什么用这个公式?因为 GCD × LCM = a × b。这是数学定理,可以推导出 LCM = (a × b) / GCD。比如 gcd(12, 18) = 6,lcm(12, 18) = 12 × 18 / 6 = 36。

重置动画值:把三个动画值都设为 0。

序列动画

  1. 结果卡片从 0 到 1(弹簧动画)
  2. GCD 和 LCM 同时从 0 到 1(并行弹簧动画)

为什么用序列 + 并行动画?因为要先显示结果卡片,再显示 GCD 和 LCM。如果全部并行,会同时出现,没有层次感。如果全部序列,会依次出现,太慢。序列 + 并行结合,先显示卡片,再同时显示两个结果,节奏刚好。

界面渲染:头部和输入

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerIcon}>🔢</Text>
        <Text style={styles.headerTitle}>最大公约数/最小公倍数</Text>
      </View>

      <View style={styles.inputSection}>
        <View style={styles.inputWrapper}>
          <TextInput style={styles.input} value={num1} onChangeText={setNum1} keyboardType="numeric" placeholder="第一个数" placeholderTextColor="#666" />
        </View>
        <View style={styles.inputWrapper}>
          <TextInput style={styles.input} value={num2} onChangeText={setNum2} keyboardType="numeric" placeholder="第二个数" placeholderTextColor="#666" />
        </View>
      </View>

      <Animated.View style={{ transform: [{ scale: buttonAnim }] }}>
        <TouchableOpacity style={styles.btn} onPress={calculate} activeOpacity={0.8}>
          <Text style={styles.btnText}>🧮 计算</Text>
        </TouchableOpacity>
      </Animated.View>

头部显示标题,输入区域包含两个输入框,按钮触发计算。

头部

  • 图标:🔢 数字
  • 标题:最大公约数/最小公倍数

输入区域

  • 两个输入框并排,各占 50% 宽度
  • keyboardType="numeric":弹出数字键盘
  • textAlign: 'center':居中对齐
  • 占位符:“第一个数”、“第二个数”

按钮

  • 应用缩放动画,点击时缩小再弹回
  • 图标:🧮 算盘
  • 文字:计算

为什么两个输入框并排?因为要输入两个数字,并排布局让用户一眼看出"需要输入两个数"。如果上下排列,用户可能以为只需要输入一个数。

结果显示

      {result && (
        <Animated.View style={[styles.result, { transform: [{ scale: resultAnim }], opacity: resultAnim }]}>
          <View style={styles.resultRow}>
            <Animated.View style={[styles.resultItem, { transform: [{ scale: gcdAnim }] }]}>
              <Text style={styles.resultLabel}>最大公约数 (GCD)</Text>
              <Text style={styles.resultValue}>{result.gcd}</Text>
            </Animated.View>
            <Animated.View style={[styles.resultItem, { transform: [{ scale: lcmAnim }] }]}>
              <Text style={styles.resultLabel}>最小公倍数 (LCM)</Text>
              <Text style={styles.resultValue}>{result.lcm}</Text>
            </Animated.View>
          </View>

结果卡片显示最大公约数和最小公倍数。

条件渲染:只有 result 不为 null 时才显示。

结果卡片动画

  • 缩放:从 0 到 1
  • 透明度:从 0 到 1

结果行:横向布局,两个结果项并排。

结果项

  • 标签:灰色小字,“最大公约数 (GCD)” 或 “最小公倍数 (LCM)”
  • 数值:蓝色大字,字号 36,加粗
  • 应用缩放动画

为什么两个结果并排?因为 GCD 和 LCM 是对等的,并排布局让用户一眼看出"这是两个结果"。如果上下排列,用户可能以为只有一个结果。

计算步骤

          {result.steps.length > 0 && (
            <View style={styles.steps}>
              <Text style={styles.stepsTitle}>📝 辗转相除法步骤:</Text>
              {result.steps.map((step, i) => (
                <Text key={i} style={styles.stepText}>{step}</Text>
              ))}
            </View>
          )}
        </Animated.View>
      )}

计算步骤显示辗转相除法的每一步。

条件渲染:只有 steps 数组不为空时才显示。

标题:📝 辗转相除法步骤

遍历步骤:用 map 遍历 steps 数组,生成步骤文本。

步骤文本

  • 白色文字
  • fontFamily: 'monospace':等宽字体,让数字对齐

为什么用等宽字体?因为步骤是数学公式,用等宽字体让数字对齐,更容易阅读。比如:

48 = 18 × 2 + 12
18 = 12 × 1 + 6
12 =  6 × 2 + 0

等宽字体让等号对齐,看起来更整齐。

公式说明

      <View style={styles.info}>
        <Text style={styles.infoTitle}>💡 公式</Text>
        <Text style={styles.infoText}>• GCD: 辗转相除法 (欧几里得算法)</Text>
        <Text style={styles.infoText}>• LCM = (a × b) / GCD(a, b)</Text>
        <Text style={styles.infoText}>• GCD(a, b) × LCM(a, b) = a × b</Text>
      </View>
    </ScrollView>
  );
};

公式说明区域显示算法和公式。

三条公式

  1. GCD 用辗转相除法(欧几里得算法)
  2. LCM = (a × b) / GCD(a, b)
  3. GCD(a, b) × LCM(a, b) = a × b

为什么显示公式?因为公式能帮助用户理解算法。很多人知道最大公约数和最小公倍数,但不知道怎么计算。显示公式让用户学习数学知识,增加工具的教育价值。

鸿蒙 ArkTS 对比:辗转相除法

@State num1: string = ''
@State num2: string = ''
@State result: { gcd: number, lcm: number, steps: string[] } | null = null

gcd(a: number, b: number): number {
  return b === 0 ? a : this.gcd(b, a % b)
}

calculate() {
  const a = parseInt(this.num1)
  const b = parseInt(this.num2)
  if (isNaN(a) || isNaN(b) || a <= 0 || b <= 0) return
  
  const steps: string[] = []
  let x = a, y = b
  while (y !== 0) {
    steps.push(`${x} = ${y} × ${Math.floor(x / y)} + ${x % y}`)
    const temp = y
    y = x % y
    x = temp
  }
  
  const gcdResult = this.gcd(a, b)
  const lcmResult = (a * b) / gcdResult
  this.result = { gcd: gcdResult, lcm: lcmResult, steps }
}

ArkTS 中的辗转相除法逻辑完全一样。核心是递归计算 GCD,用公式计算 LCM,用循环记录步骤。parseInt()isNaN()Math.floor() 都是标准 JavaScript API,跨平台通用。

为什么算法跨平台通用?因为辗转相除法是纯数学算法,不涉及 UI、动画、平台 API。只要语言支持递归和取余运算,就能实现辗转相除法。

样式定义

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23', padding: 20 },
  header: { alignItems: 'center', marginBottom: 24 },
  headerIcon: { fontSize: 50, marginBottom: 8 },
  headerTitle: { fontSize: 24, fontWeight: '700', color: '#fff', textAlign: 'center' },
  inputSection: { flexDirection: 'row', marginBottom: 20 },
  inputWrapper: { flex: 1, backgroundColor: '#1a1a3e', borderRadius: 12, marginHorizontal: 4, borderWidth: 1, borderColor: '#3a3a6a' },
  input: { padding: 16, fontSize: 20, color: '#fff', textAlign: 'center' },
  btn: { backgroundColor: '#4A90D9', padding: 18, borderRadius: 16, alignItems: 'center', marginBottom: 24 },
  btnText: { color: '#fff', fontSize: 18, fontWeight: '700' },
  result: { backgroundColor: '#1a1a3e', padding: 20, borderRadius: 20, marginBottom: 20, borderWidth: 1, borderColor: '#3a3a6a' },
  resultRow: { flexDirection: 'row' },
  resultItem: { flex: 1, alignItems: 'center', padding: 16 },
  resultLabel: { fontSize: 12, color: '#888', marginBottom: 8, textAlign: 'center' },
  resultValue: { fontSize: 36, fontWeight: '700', color: '#4A90D9' },
  steps: { marginTop: 20, paddingTop: 20, borderTopWidth: 1, borderTopColor: '#3a3a6a' },
  stepsTitle: { fontSize: 14, color: '#888', marginBottom: 12 },
  stepText: { fontSize: 14, color: '#fff', fontFamily: 'monospace', marginBottom: 6 },
  info: { backgroundColor: '#1a1a3e', padding: 16, borderRadius: 16, borderWidth: 1, borderColor: '#3a3a6a' },
  infoTitle: { fontSize: 16, fontWeight: '600', marginBottom: 12, color: '#fff' },
  infoText: { fontSize: 14, color: '#888', marginBottom: 6 },
});

容器用深蓝黑色背景。输入区域横向布局,两个输入框各占 50%。结果行横向布局,两个结果项各占 50%。结果数值字号 36,蓝色加粗。步骤用等宽字体,让数字对齐。

小结

这个最大公约数工具展示了辗转相除法的实现。用递归计算 GCD,用公式计算 LCM,用循环记录步骤。序列 + 并行动画让结果依次出现,营造计算完成的效果。


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

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐