在这里插入图片描述

计算器是每个开发者入门时都会接触的经典项目,它涵盖了状态管理、用户交互、布局设计等核心概念。今天我们用 React Native 来实现一个带有 3D 效果和流畅动画的计算器,同时确保代码能够在 OpenHarmony 平台上正常运行。

为什么选择计算器作为第一个项目

说实话,计算器看起来简单,但要做好并不容易。它需要处理连续输入、运算符优先级、小数点、正负号等各种边界情况。更重要的是,计算器的 UI 布局非常适合练习 Flexbox,按钮的交互也能让我们熟悉 React Native 的动画系统。

在 OpenHarmony 环境下开发时,我们需要特别注意避免使用一些还未适配的第三方库。好在计算器这种纯逻辑+UI 的应用,完全可以用 React Native 的内置组件来实现。

组件结构设计

我们的计算器组件采用函数式组件的写法,使用 useState 来管理状态,useRef 来存储动画值。先来看看整体的引入和状态定义:

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

export const Calculator: React.FC = () => {
  const [display, setDisplay] = useState('0');
  const [firstNum, setFirstNum] = useState<string>('');
  const [operator, setOperator] = useState<string>('');
  const [waitingForSecond, setWaitingForSecond] = useState(false);
  const scaleAnims = useRef<{ [key: string]: Animated.Value }>({}).current;

这里定义了四个状态变量:

  • display 用于显示当前输入或计算结果
  • firstNum 保存第一个操作数
  • operator 记录当前选择的运算符
  • waitingForSecond 标记是否正在等待输入第二个操作数

scaleAnims 是一个对象,用来存储每个按钮的缩放动画值。为什么要用 useRef 而不是 useState?因为动画值的变化不需要触发组件重新渲染,用 useRef 可以避免不必要的性能开销。

鸿蒙 ArkTS 对比:状态管理

在鸿蒙原生开发中,使用 ArkTS 的 @State 装饰器来实现类似的状态管理:

@Entry
@Component
struct Calculator {
  @State display: string = '0'
  @State firstNum: string = ''
  @State operator: string = ''
  @State waitingForSecond: boolean = false

ArkTS 中的 @State 装饰器和 React 的 useState 作用类似,都是用来声明响应式状态。当状态变化时,UI 会自动更新。不同的是,ArkTS 使用装饰器语法,而 React 使用 Hook 函数。

React Native for OpenHarmony 在底层会将 React 的状态管理机制映射到鸿蒙的渲染引擎上,所以我们写的 useState 代码最终也能在鸿蒙设备上正确响应状态变化。

动画系统实现

为了让按钮按下时有反馈感,我们实现了一个简单但有效的缩放动画:

  const getScaleAnim = (key: string) => {
    if (!scaleAnims[key]) {
      scaleAnims[key] = new Animated.Value(1);
    }
    return scaleAnims[key];
  };

  const animatePress = (key: string) => {
    const anim = getScaleAnim(key);
    Animated.sequence([
      Animated.timing(anim, { toValue: 0.85, duration: 50, useNativeDriver: true }),
      Animated.spring(anim, { toValue: 1, friction: 3, tension: 100, useNativeDriver: true }),
    ]).start();
  };

getScaleAnim 函数采用懒加载的方式创建动画值,只有当某个按钮第一次被使用时才会创建对应的 Animated.Value。这样做的好处是避免一开始就创建大量的动画对象。

animatePress 函数使用了 Animated.sequence 来串联两个动画:先快速缩小到 0.85 倍,然后用弹簧动画恢复原状。frictiontension 参数控制弹簧的阻尼和张力,数值越小弹性越大。

鸿蒙 ArkTS 对比:动画实现

在鸿蒙原生开发中,实现类似的缩放动画可以使用 animateTo 函数:

@State scaleValue: number = 1

Button(btn)
  .scale({ x: this.scaleValue, y: this.scaleValue })
  .onClick(() => {
    animateTo({ duration: 50 }, () => {
      this.scaleValue = 0.85
    })
    setTimeout(() => {
      animateTo({ 
        duration: 200,
        curve: Curve.EaseOut 
      }, () => {
        this.scaleValue = 1
      })
    }, 50)
  })

ArkTS 的 animateTo 是一个显式动画函数,通过修改状态值来驱动动画。而 React Native 的 Animated API 则是创建一个动画值对象,通过 timingspring 方法来控制动画过程。

两种方式各有优劣:ArkTS 的写法更直观,直接修改状态;React Native 的 Animated 则提供了更丰富的动画组合能力,比如 sequenceparallelstagger 等。

核心计算逻辑

计算器的核心是处理数字输入和运算:

  const handleNumber = (num: string) => {
    if (waitingForSecond) {
      setDisplay(num);
      setWaitingForSecond(false);
    } else {
      setDisplay(display === '0' ? num : display + num);
    }
  };

  const handleOperator = (op: string) => {
    setFirstNum(display);
    setOperator(op);
    setWaitingForSecond(true);
  };

handleNumber 的逻辑是这样的:如果正在等待第二个操作数,直接用新数字替换显示内容;否则就追加到现有数字后面。注意 display === '0' 这个判断,它确保了初始状态下输入数字会替换掉默认的 0,而不是变成 “01”、“02” 这样的字符串。

handleOperator 则是保存当前显示的数字作为第一个操作数,记录运算符,然后设置等待标志。

接下来是实际的计算函数:

  const calculate = () => {
    const num1 = parseFloat(firstNum);
    const num2 = parseFloat(display);
    let result = 0;
    switch (operator) {
      case '+': result = num1 + num2; break;
      case '-': result = num1 - num2; break;
      case '×': result = num1 * num2; break;
      case '÷': result = num2 !== 0 ? num1 / num2 : 0; break;
    }
    setDisplay(String(result));
    setFirstNum('');
    setOperator('');
  };

  const clear = () => {
    setDisplay('0');
    setFirstNum('');
    setOperator('');
  };

calculate 函数用 parseFloat 把字符串转成数字,然后根据运算符执行对应的计算。除法时做了除零保护,虽然简单粗暴地返回 0,但至少不会崩溃。

鸿蒙 ArkTS 对比:计算逻辑

计算逻辑在 ArkTS 中的实现几乎一样,因为这是纯 JavaScript/TypeScript 逻辑:

calculate() {
  let num1 = parseFloat(this.firstNum)
  let num2 = parseFloat(this.display)
  let result: number = 0
  
  switch (this.operator) {
    case '+': result = num1 + num2; break
    case '-': result = num1 - num2; break
    case '×': result = num1 * num2; break
    case '÷': result = num2 !== 0 ? num1 / num2 : 0; break
  }
  
  this.display = result.toString()
  this.firstNum = ''
  this.operator = ''
}

这也是 React Native for OpenHarmony 的优势之一:业务逻辑代码几乎不需要修改,可以直接复用。只有 UI 层和平台相关的 API 需要适配。

按钮布局定义

计算器的按钮采用二维数组来定义,这样渲染时可以直接遍历:

  const buttons = [
    ['C', '±', '%', '÷'],
    ['7', '8', '9', '×'],
    ['4', '5', '6', '-'],
    ['1', '2', '3', '+'],
    ['0', '.', '='],
  ];

这个布局模仿了 iOS 计算器的设计,第一行是功能键,中间三行是数字和运算符,最后一行的 0 按钮会占据两个位置的宽度。

按钮渲染逻辑

每个按钮的渲染逻辑封装在 renderButton 函数中:

  const renderButton = (btn: string) => {
    const isOp = ['+', '-', '×', '÷', '='].includes(btn);
    const isZero = btn === '0';
    const isFunc = ['C', '±', '%'].includes(btn);
    const anim = getScaleAnim(btn);

    return (
      <Animated.View
        key={btn}
        style={[
          styles.btnWrapper,
          isZero && styles.btnZeroWrapper,
          { transform: [{ scale: anim }] },
        ]}
      >
        <TouchableOpacity
          style={[
            styles.btn,
            isZero && styles.btnZero,
            isOp && styles.btnOp,
            isFunc && styles.btnFunc,
          ]}
          onPress={() => {
            animatePress(btn);
            if (btn === 'C') clear();
            else if (btn === '=') calculate();
            else if (['+', '-', '×', '÷'].includes(btn)) handleOperator(btn);
            else if (btn === '±') setDisplay(String(-parseFloat(display)));
            else if (btn === '%') setDisplay(String(parseFloat(display) / 100));
            else handleNumber(btn);
          }}
          activeOpacity={0.8}
        >
          <Text style={[styles.btnText, isFunc && styles.btnFuncText]}>{btn}</Text>
        </TouchableOpacity>
      </Animated.View>
    );
  };

这里用了几个布尔变量来判断按钮类型,然后根据类型应用不同的样式。Animated.View 包裹 TouchableOpacity,这样缩放动画会作用在整个按钮上。

鸿蒙 ArkTS 对比:按钮渲染

在 ArkTS 中,按钮渲染使用 ForEach 循环和 Button 组件:

build() {
  Column() {
    ForEach(this.buttons, (row: string[], rowIndex: number) => {
      Row() {
        ForEach(row, (btn: string) => {
          Button(btn)
            .width(btn === '0' ? 160 : 75)
            .height(75)
            .fontSize(32)
            .fontColor(this.isFunc(btn) ? Color.Black : Color.White)
            .backgroundColor(this.getButtonColor(btn))
            .borderRadius(40)
            .onClick(() => this.handleClick(btn))
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
    })
  }
}

ArkTS 使用声明式 UI 语法,通过链式调用设置组件属性。React Native 则是通过 style 属性传入样式对象。两种方式都能实现相同的效果,只是语法风格不同。

主界面渲染

组件的 return 部分负责渲染整个界面:

  return (
    <View style={styles.container}>
      <View style={styles.displayContainer}>
        <View style={styles.display3D}>
          <Text style={styles.operatorText}>{operator ? `${firstNum} ${operator}` : ''}</Text>
          <Text style={styles.displayText} numberOfLines={1} adjustsFontSizeToFit>
            {display}
          </Text>
        </View>
      </View>
      <View style={styles.buttons}>
        {buttons.map((row, i) => (
          <View key={i} style={styles.row}>
            {row.map(btn => renderButton(btn))}
          </View>
        ))}
      </View>
    </View>
  );
};

显示区域分为两部分:上面显示当前的运算表达式(第一个数和运算符),下面显示当前输入或结果。numberOfLines={1}adjustsFontSizeToFit 确保数字太长时会自动缩小字体而不是换行。

样式设计

最后是样式定义,这里实现了 3D 效果和暗色主题:

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0a0a0a' },
  displayContainer: { flex: 1, justifyContent: 'flex-end', padding: 20 },
  display3D: {
    backgroundColor: '#1a1a1a',
    borderRadius: 20,
    padding: 20,
    shadowColor: '#ff9500',
    shadowOffset: { width: 0, height: 0 },
    shadowOpacity: 0.3,
    shadowRadius: 20,
    elevation: 10,
    borderWidth: 1,
    borderColor: '#333',
  },
  operatorText: { fontSize: 24, color: '#ff9500', textAlign: 'right', marginBottom: 10 },
  displayText: { fontSize: 64, color: '#fff', textAlign: 'right', fontWeight: '300' },
  buttons: { padding: 10, paddingBottom: 30 },
  row: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 12 },
  btnWrapper: { width: 75, height: 75 },
  btnZeroWrapper: { width: 160 },
  btn: {
    flex: 1,
    borderRadius: 40,
    backgroundColor: '#333',
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.5,
    shadowRadius: 8,
    elevation: 8,
    borderWidth: 1,
    borderTopColor: '#555',
    borderLeftColor: '#555',
    borderRightColor: '#222',
    borderBottomColor: '#222',
  },
  btnZero: { borderRadius: 40 },
  btnOp: {
    backgroundColor: '#ff9500',
    shadowColor: '#ff9500',
    shadowOpacity: 0.5,
    borderTopColor: '#ffb340',
    borderLeftColor: '#ffb340',
    borderRightColor: '#cc7700',
    borderBottomColor: '#cc7700',
  },
  btnFunc: { backgroundColor: '#a5a5a5' },
  btnText: { fontSize: 32, color: '#fff', fontWeight: '500' },
  btnFuncText: { color: '#000' },
});

3D 效果主要通过两种方式实现:

  1. 阴影shadowColorshadowOffsetshadowOpacityshadowRadius 组合使用,给元素添加投影。显示区域用橙色阴影营造发光效果,按钮用黑色阴影增加立体感。

  2. 边框渐变:通过设置四个方向不同颜色的边框(上左亮、右下暗),模拟光源从左上方照射的效果。

鸿蒙 ArkTS 对比:样式实现

在 ArkTS 中实现类似的 3D 效果:

Column() {
  Text(this.display)
    .fontSize(64)
    .fontColor(Color.White)
    .textAlign(TextAlign.End)
}
.backgroundColor('#1a1a1a')
.borderRadius(20)
.padding(20)
.shadow({
  radius: 20,
  color: '#ff9500',
  offsetX: 0,
  offsetY: 0
})
.border({
  width: 1,
  color: '#333333'
})

ArkTS 的 shadow 属性和 React Native 的阴影属性功能类似,都可以设置阴影半径、颜色和偏移。React Native for OpenHarmony 在渲染时会将这些样式属性转换为鸿蒙原生的渲染指令。

值得注意的是,React Native 的 elevation 属性主要用于 Android 平台,在 OpenHarmony 上会被映射为对应的阴影效果。如果发现阴影显示不一致,可以优先使用 shadow* 系列属性。

小结

通过这个计算器项目,我们实践了 React Native 开发中的几个核心概念:状态管理、动画系统、样式布局。同时也对比了 React Native 和鸿蒙原生 ArkTS 在实现上的异同。

React Native for OpenHarmony 的优势在于:可以用熟悉的 React 语法开发鸿蒙应用,业务逻辑代码可以跨平台复用,学习成本相对较低。而了解 ArkTS 的实现方式,也有助于我们更好地理解底层原理,在遇到问题时能够更快地定位和解决。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐