React Native for OpenHarmony 实战:计算器实现
本文介绍了使用 React Native 开发跨平台计算器应用的关键技术点,重点对比了 React Native 与鸿蒙 ArkTS 在状态管理、动画系统和计算逻辑上的实现差异。文章首先分析了计算器项目的学习价值,包括状态管理、用户交互和布局设计等核心概念;然后详细讲解了组件结构设计、动画系统实现(使用 Animated API 实现按钮缩放效果)和核心计算逻辑;最后通过代码示例对比了 React

计算器是每个开发者入门时都会接触的经典项目,它涵盖了状态管理、用户交互、布局设计等核心概念。今天我们用 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 倍,然后用弹簧动画恢复原状。friction 和 tension 参数控制弹簧的阻尼和张力,数值越小弹性越大。
鸿蒙 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 则是创建一个动画值对象,通过 timing 或 spring 方法来控制动画过程。
两种方式各有优劣:ArkTS 的写法更直观,直接修改状态;React Native 的 Animated 则提供了更丰富的动画组合能力,比如 sequence、parallel、stagger 等。
核心计算逻辑
计算器的核心是处理数字输入和运算:
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 效果主要通过两种方式实现:
-
阴影:
shadowColor、shadowOffset、shadowOpacity、shadowRadius组合使用,给元素添加投影。显示区域用橙色阴影营造发光效果,按钮用黑色阴影增加立体感。 -
边框渐变:通过设置四个方向不同颜色的边框(上左亮、右下暗),模拟光源从左上方照射的效果。
鸿蒙 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
更多推荐


所有评论(0)