React Native 鸿蒙跨平台开发:实现一个模拟计算器
状态管理:使用 useState 管理计算器的各种状态事件处理:处理数字、运算符、等号等按钮的点击事件布局设计:使用 Flexbox 实现键盘网格布局性能优化:使用 memo 和 useCallback 优化性能错误处理:处理除零错误等异常情况扩展功能:添加科学计算、历史记录等扩展功能计算器组件在 React Native for Harmony 中表现良好,交互流畅,是一个很好的学习案例。

一、核心原理:计算器的设计与实现
1.1 计算器的设计理念
计算器是一个经典的交互式应用,主要用于:
- 基础运算:加、减、乘、除四则运算
- 连续运算:支持多个连续的运算操作
- 特殊运算:百分比、正负号切换等
- 结果显示:实时显示输入和计算结果
- 用户交互:提供直观的按钮操作界面
1.2 计算器的核心要素
一个完整的计算器需要考虑:
- 显示区域:显示输入的数字和计算结果
- 键盘区域:数字按钮和操作按钮
- 状态管理:管理当前输入、前一个操作数、运算符、错误状态等
- 运算逻辑:实现各种数学运算
- 错误处理:处理除零错误、溢出等异常情况,显示错误提示
- 用户反馈:提供按钮点击反馈和结果反馈
- 错误恢复:在错误状态下,任何按钮操作都能清除错误状态
1.3 实现原理
计算器的核心实现原理:
- 使用状态管理存储当前输入、前一个操作数、运算符
- 使用 Flexbox 布局实现键盘网格
- 使用 TouchableOpacity 实现按钮交互
- 实现完整的四则运算逻辑
- 支持连续运算和结果显示
- 使用 StyleSheet 实现样式定制
二、基础计算器实现
2.1 组件结构
计算器组件包含以下部分:
- 显示容器:显示输入和结果的容器
- 显示文本:显示当前数字或结果
- 键盘容器:包含所有按钮的容器
- 数字按钮:0-9 数字按钮
- 操作按钮:加、减、乘、除按钮
- 功能按钮:清除、百分比、正负号、等号按钮
2.2 完整代码实现
import React, { useState, useCallback, memo } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
SafeAreaView,
} from 'react-native';
// 计算器按钮组件 Props 类型
interface CalculatorButtonProps {
text: string;
onPress: () => void;
style?: any;
textStyle?: any;
}
// 计算器按钮组件
const CalculatorButton = memo<CalculatorButtonProps>(({
text,
onPress,
style,
textStyle,
}) => (
<TouchableOpacity
style={[styles.button, style]}
onPress={onPress}
activeOpacity={0.7}
>
<Text style={[styles.buttonText, textStyle]}>{text}</Text>
</TouchableOpacity>
));
CalculatorButton.displayName = 'CalculatorButton';
// 计算器组件
const Calculator = memo(() => {
const [display, setDisplay] = useState('0');
const [previousValue, setPreviousValue] = useState<number | null>(null);
const [operation, setOperation] = useState<string | null>(null);
const [waitingForOperand, setWaitingForOperand] = useState(false);
const [isError, setIsError] = useState(false);
// 处理数字按钮点击
const handleNumberPress = useCallback((num: string) => {
if (isError) {
setIsError(false);
setDisplay(num);
return;
}
setDisplay(prev => {
if (waitingForOperand) {
setWaitingForOperand(false);
return num;
}
return prev === '0' ? num : prev + num;
});
}, [waitingForOperand, isError]);
// 处理操作符按钮点击
const handleOperationPress = useCallback((nextOperation: string) => {
if (isError) {
setIsError(false);
setDisplay('0');
return;
}
const inputValue = parseFloat(display);
if (previousValue === null) {
setPreviousValue(inputValue);
} else if (operation) {
const currentValue = previousValue || 0;
try {
const newValue = performOperation(currentValue, inputValue, operation);
setDisplay(String(newValue));
setPreviousValue(newValue);
} catch (error) {
setIsError(true);
setDisplay('错误');
setPreviousValue(null);
setOperation(null);
return;
}
}
setWaitingForOperand(true);
setOperation(nextOperation);
}, [display, previousValue, operation, isError]);
// 处理等号按钮点击
const handleEqualPress = useCallback(() => {
if (isError) {
setIsError(false);
setDisplay('0');
return;
}
const inputValue = parseFloat(display);
if (previousValue === null || operation === null) return;
try {
const result = performOperation(previousValue || 0, inputValue, operation);
setDisplay(String(result));
setPreviousValue(null);
setOperation(null);
setWaitingForOperand(true);
} catch (error) {
setIsError(true);
setDisplay('错误');
setPreviousValue(null);
setOperation(null);
}
}, [display, previousValue, operation, isError]);
// 处理清除按钮点击
const handleClearPress = useCallback(() => {
setDisplay('0');
setPreviousValue(null);
setOperation(null);
setWaitingForOperand(false);
}, []);
// 处理小数点按钮点击
const handleDecimalPress = useCallback(() => {
if (isError) {
setIsError(false);
setDisplay('0.');
return;
}
setDisplay(prev => {
if (waitingForOperand) {
setWaitingForOperand(false);
return '0.';
}
return prev.includes('.') ? prev : prev + '.';
});
}, [waitingForOperand, isError]);
// 处理正负号切换
const handleToggleSign = useCallback(() => {
if (isError) {
return;
}
setDisplay(prev => String(-parseFloat(prev)));
}, [isError]);
// 处理百分比
const handlePercentage = useCallback(() => {
if (isError) {
return;
}
setDisplay(prev => String(parseFloat(prev) / 100));
}, [isError]);
return (
<SafeAreaView style={styles.container}>
{/* 标题区域 */}
<View style={styles.header}>
<Text style={styles.title}>React Native for Harmony</Text>
<Text style={styles.subtitle}>模拟计算器</Text>
</View>
{/* 计算器主体 */}
<View style={styles.calculatorContainer}>
{/* 显示区域 */}
<View style={styles.display}>
<Text style={[styles.displayText, isError && styles.errorText]}>{display}</Text>
</View>
{/* 键盘区域 */}
<View style={styles.keypad}>
{/* 第一行 */}
<CalculatorButton
text="C"
onPress={handleClearPress}
style={styles.clearButton}
textStyle={styles.clearButtonText}
/>
<CalculatorButton
text="±"
onPress={handleToggleSign}
style={styles.functionButton}
textStyle={styles.functionButtonText}
/>
<CalculatorButton
text="%"
onPress={handlePercentage}
style={styles.functionButton}
textStyle={styles.functionButtonText}
/>
<CalculatorButton
text="÷"
onPress={() => handleOperationPress('/')}
style={styles.operationButton}
textStyle={styles.operationButtonText}
/>
{/* 第二行 */}
<CalculatorButton
text="7"
onPress={() => handleNumberPress('7')}
/>
<CalculatorButton
text="8"
onPress={() => handleNumberPress('8')}
/>
<CalculatorButton
text="9"
onPress={() => handleNumberPress('9')}
/>
<CalculatorButton
text="×"
onPress={() => handleOperationPress('*')}
style={styles.operationButton}
textStyle={styles.operationButtonText}
/>
{/* 第三行 */}
<CalculatorButton
text="4"
onPress={() => handleNumberPress('4')}
/>
<CalculatorButton
text="5"
onPress={() => handleNumberPress('5')}
/>
<CalculatorButton
text="6"
onPress={() => handleNumberPress('6')}
/>
<CalculatorButton
text="-"
onPress={() => handleOperationPress('-')}
style={styles.operationButton}
textStyle={styles.operationButtonText}
/>
{/* 第四行 */}
<CalculatorButton
text="1"
onPress={() => handleNumberPress('1')}
/>
<CalculatorButton
text="2"
onPress={() => handleNumberPress('2')}
/>
<CalculatorButton
text="3"
onPress={() => handleNumberPress('3')}
/>
<CalculatorButton
text="+"
onPress={() => handleOperationPress('+')}
style={styles.operationButton}
textStyle={styles.operationButtonText}
/>
{/* 第五行 */}
<CalculatorButton
text="0"
onPress={() => handleNumberPress('0')}
style={styles.zeroButton}
/>
<CalculatorButton
text="."
onPress={handleDecimalPress}
/>
<CalculatorButton
text="="
onPress={handleEqualPress}
style={styles.equalButton}
textStyle={styles.equalButtonText}
/>
</View>
</View>
{/* 说明区域 */}
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>💡 功能说明</Text>
<Text style={styles.infoText}>• 基础运算:支持加、减、乘、除四则运算</Text>
<Text style={styles.infoText}>• 连续运算:支持多个连续的运算操作</Text>
<Text style={styles.infoText}>• 特殊运算:支持百分比、正负号切换</Text>
<Text style={styles.infoText}>• 实时显示:实时显示输入和计算结果</Text>
<Text style={styles.infoText}>• 错误处理:除零时显示红色错误提示</Text>
<Text style={styles.infoText}>• 错误恢复:任何按钮操作都能清除错误状态</Text>
<Text style={styles.infoText}>• 鸿蒙端完美兼容,交互流畅</Text>
</View>
</SafeAreaView>
);
});
Calculator.displayName = 'Calculator';
// 执行计算操作
function performOperation(a: number, b: number, operation: string): number {
switch (operation) {
case '+':
return a + b;
case '-':
return a - b;
case '*':
return a * b;
case '/':
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
default:
return b;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F7FA',
},
// ======== 标题区域 ========
header: {
padding: 20,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#EBEEF5',
},
title: {
fontSize: 24,
fontWeight: '700',
color: '#303133',
textAlign: 'center',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
fontWeight: '500',
color: '#909399',
textAlign: 'center',
},
// ======== 计算器容器 ========
calculatorContainer: {
backgroundColor: '#FFFFFF',
margin: 16,
borderRadius: 16,
padding: 16,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 4,
},
// ======== 显示区域 ========
display: {
backgroundColor: '#F5F7FA',
borderRadius: 12,
padding: 20,
marginBottom: 16,
alignItems: 'flex-end',
minHeight: 80,
justifyContent: 'flex-end',
},
displayText: {
fontSize: 36,
fontWeight: '600',
color: '#303133',
},
errorText: {
color: '#F56C6C',
},
// ======== 键盘区域 ========
keypad: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
button: {
width: '22%',
aspectRatio: 1,
backgroundColor: '#F5F7FA',
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 12,
},
buttonText: {
fontSize: 24,
fontWeight: '500',
color: '#303133',
},
// ======== 特殊按钮样式 ========
clearButton: {
backgroundColor: '#F56C6C',
},
clearButtonText: {
color: '#FFFFFF',
},
functionButton: {
backgroundColor: '#909399',
},
functionButtonText: {
color: '#FFFFFF',
},
operationButton: {
backgroundColor: '#409EFF',
},
operationButtonText: {
color: '#FFFFFF',
},
equalButton: {
backgroundColor: '#67C23A',
},
equalButtonText: {
color: '#FFFFFF',
},
zeroButton: {
width: '48%',
},
// ======== 信息卡片 ========
infoCard: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
margin: 16,
marginTop: 0,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 4,
},
infoTitle: {
fontSize: 16,
fontWeight: '600',
color: '#303133',
marginBottom: 12,
},
infoText: {
fontSize: 14,
color: '#606266',
lineHeight: 22,
marginBottom: 6,
},
});
export default Calculator;
三、核心实现要点
3.1 状态管理
使用状态管理存储计算器的各种状态:
const [display, setDisplay] = useState('0');
const [previousValue, setPreviousValue] = useState<number | null>(null);
const [operation, setOperation] = useState<string | null>(null);
const [waitingForOperand, setWaitingForOperand] = useState(false);
const [isError, setIsError] = useState(false);
状态说明:
display:当前显示的数字或结果previousValue:前一个操作数operation:当前操作符waitingForOperand:是否等待输入下一个操作数isError:是否处于错误状态(如除零错误)
3.2 数字输入处理
处理数字按钮的点击事件:
const handleNumberPress = useCallback((num: string) => {
setDisplay(prev => {
if (waitingForOperand) {
setWaitingForOperand(false);
return num;
}
return prev === '0' ? num : prev + num;
});
}, [waitingForOperand]);
处理逻辑:
- 如果等待输入下一个操作数,则替换当前显示
- 如果当前显示为 ‘0’,则替换为新数字
- 否则追加新数字到当前显示
3.3 运算符处理
处理运算符按钮的点击事件:
const handleOperationPress = useCallback((nextOperation: string) => {
const inputValue = parseFloat(display);
if (previousValue === null) {
setPreviousValue(inputValue);
} else if (operation) {
const currentValue = previousValue || 0;
const newValue = performOperation(currentValue, inputValue, operation);
setDisplay(String(newValue));
setPreviousValue(newValue);
}
setWaitingForOperand(true);
setOperation(nextOperation);
}, [display, previousValue, operation]);
处理逻辑:
- 如果没有前一个操作数,则保存当前值
- 如果有前一个操作数和运算符,则执行运算
- 设置等待输入下一个操作数的状态
- 保存当前运算符
3.4 等号处理
处理等号按钮的点击事件:
const handleEqualPress = useCallback(() => {
const inputValue = parseFloat(display);
if (previousValue === null || operation === null) return;
const result = performOperation(previousValue || 0, inputValue, operation);
setDisplay(String(result));
setPreviousValue(null);
setOperation(null);
setWaitingForOperand(true);
}, [display, previousValue, operation]);
处理逻辑:
- 如果没有前一个操作数或运算符,则不执行
- 执行运算并显示结果
- 清空前一个操作数和运算符
- 设置等待输入下一个操作数的状态
3.5 错误处理
处理除零错误等异常情况:
const [isError, setIsError] = useState(false);
// 在 performOperation 函数中抛出错误
function performOperation(a: number, b: number, operation: string): number {
switch (operation) {
case '/':
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
default:
return b;
}
}
// 在运算符处理中捕获错误
try {
const newValue = performOperation(currentValue, inputValue, operation);
setDisplay(String(newValue));
setPreviousValue(newValue);
} catch (error) {
setIsError(true);
setDisplay('错误');
setPreviousValue(null);
setOperation(null);
}
错误处理要点:
- 使用
isError状态管理错误状态 - 在 performOperation 函数中抛出错误
- 在运算符处理和等号处理中捕获错误
- 在显示区域显示红色错误文字
- 在错误状态下,任何按钮操作都会清除错误状态
3.6 键盘布局
使用 Flexbox 实现键盘网格布局:
<View style={styles.keypad}>
<CalculatorButton text="C" onPress={handleClearPress} />
<CalculatorButton text="7" onPress={() => handleNumberPress('7')} />
{/* 更多按钮 */}
</View>
布局设计要点:
- 使用
flexWrap: 'wrap'实现自动换行 - 每个按钮宽度为
22%,每行 4 个按钮 - 0 按钮宽度为
48%,占据两个按钮的位置 - 使用
justifyContent: 'space-between'控制按钮间距
四、性能优化
4.1 使用 memo 优化
所有组件都使用 memo 包装:
const CalculatorButton = memo<CalculatorButtonProps>(({ text, onPress, style, textStyle }) => {
// ...
});
const Calculator = memo(() => {
// ...
});
为什么使用 memo?
- 避免不必要的重新渲染
- 提升整体性能
- 在复杂应用中,避免父组件更新时重新渲染所有按钮
4.2 使用 useCallback 优化
使用 useCallback 缓存回调函数:
const handleNumberPress = useCallback((num: string) => {
setDisplay(prev => {
if (waitingForOperand) {
setWaitingForOperand(false);
return num;
}
return prev === '0' ? num : prev + num;
});
}, [waitingForOperand]);
为什么使用 useCallback?
- 避免每次渲染都创建新函数
- 减少子组件的重新渲染
- 提升整体性能
4.3 按钮组件优化
使用独立的按钮组件:
const CalculatorButton = memo<CalculatorButtonProps>(({ text, onPress, style, textStyle }) => (
<TouchableOpacity
style={[styles.button, style]}
onPress={onPress}
activeOpacity={0.7}
>
<Text style={[styles.buttonText, textStyle]}>{text}</Text>
</TouchableOpacity>
));
优化要点:
- 使用
memo避免不必要的重新渲染 - 使用
activeOpacity提供点击反馈 - 支持自定义样式和文本样式
五、常见问题与解决方案
5.1 连续运算不正确
问题现象: 连续运算时结果不正确
可能原因:
- 状态管理不正确
- 运算符处理逻辑错误
- 等待操作数状态设置错误
解决方案:
// 1. 确保状态管理正确
const [previousValue, setPreviousValue] = useState<number | null>(null);
const [operation, setOperation] = useState<string | null>(null);
const [waitingForOperand, setWaitingForOperand] = useState(false);
// 2. 确保运算符处理逻辑正确
if (previousValue === null) {
setPreviousValue(inputValue);
} else if (operation) {
const currentValue = previousValue || 0;
const newValue = performOperation(currentValue, inputValue, operation);
setDisplay(String(newValue));
setPreviousValue(newValue);
}
// 3. 确保等待操作数状态设置正确
setWaitingForOperand(true);
5.2 小数点输入错误
问题现象: 小数点输入不正确
可能原因:
- 没有检查是否已经包含小数点
- 等待操作数状态处理错误
解决方案:
const handleDecimalPress = useCallback(() => {
setDisplay(prev => {
if (waitingForOperand) {
setWaitingForOperand(false);
return '0.';
}
return prev.includes('.') ? prev : prev + '.';
});
}, [waitingForOperand]);
5.3 除零错误
问题现象: 除以零时显示错误
可能原因:
- 没有处理除零情况
- 显示结果为 Infinity 或 NaN
解决方案:
使用错误状态管理除零错误:
// 1. 添加错误状态
const [isError, setIsError] = useState(false);
// 2. 在 performOperation 函数中抛出错误
function performOperation(a: number, b: number, operation: string): number {
switch (operation) {
case '/':
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
default:
return b;
}
}
// 3. 在运算符处理中捕获错误
const handleOperationPress = useCallback((nextOperation: string) => {
if (isError) {
setIsError(false);
setDisplay('0');
return;
}
const inputValue = parseFloat(display);
if (previousValue === null) {
setPreviousValue(inputValue);
} else if (operation) {
const currentValue = previousValue || 0;
try {
const newValue = performOperation(currentValue, inputValue, operation);
setDisplay(String(newValue));
setPreviousValue(newValue);
} catch (error) {
setIsError(true);
setDisplay('错误');
setPreviousValue(null);
setOperation(null);
return;
}
}
setWaitingForOperand(true);
setOperation(nextOperation);
}, [display, previousValue, operation, isError]);
// 4. 在显示区域显示错误
<Text style={[styles.displayText, isError && styles.errorText]}>{display}</Text>
// 5. 在样式中添加错误文本样式
errorText: {
color: '#F56C6C',
},
5.4 按钮点击无响应
问题现象: 点击按钮没有响应
可能原因:
- onPress 事件未正确绑定
- 组件未正确渲染
- 状态更新未触发重新渲染
解决方案:
// 1. 确保 onPress 事件正确绑定
<CalculatorButton
text="7"
onPress={() => handleNumberPress('7')}
/>
// 2. 确保组件正确渲染
const Calculator = memo(() => {
// ...
});
// 3. 确保状态更新正确
setDisplay(prev => {
// ...
});
六、扩展用法
6.1 添加科学计算功能
添加三角函数、对数等科学计算功能:
const handleScientificOperation = useCallback((op: string) => {
const inputValue = parseFloat(display);
let result: number;
switch (op) {
case 'sin':
result = Math.sin(inputValue);
break;
case 'cos':
result = Math.cos(inputValue);
break;
case 'tan':
result = Math.tan(inputValue);
break;
case 'log':
result = Math.log10(inputValue);
break;
case 'ln':
result = Math.log(inputValue);
break;
default:
return;
}
setDisplay(String(result));
setWaitingForOperand(true);
}, [display]);
6.2 添加历史记录功能
添加计算历史记录功能:
const [history, setHistory] = useState<string[]>([]);
const handleEqualPress = useCallback(() => {
const inputValue = parseFloat(display);
if (previousValue === null || operation === null) return;
const result = performOperation(previousValue || 0, inputValue, operation);
const expression = `${previousValue} ${operation} ${inputValue} = ${result}`;
setHistory(prev => [...prev, expression]);
setDisplay(String(result));
setPreviousValue(null);
setOperation(null);
setWaitingForOperand(true);
}, [display, previousValue, operation]);
6.3 添加语音输入功能
添加语音输入数字和运算符的功能:
const handleVoiceInput = useCallback((text: string) => {
// 解析语音输入
const numbers = text.match(/\d+/g);
const operators = text.match(/[+\-*/]/g);
if (numbers && numbers.length > 0) {
handleNumberPress(numbers[0]);
}
if (operators && operators.length > 0) {
handleOperationPress(operators[0]);
}
}, []);
6.4 添加主题切换功能
添加亮色和暗色主题切换功能:
const [isDarkMode, setIsDarkMode] = useState(false);
const theme = isDarkMode ? darkTheme : lightTheme;
<View style={[styles.container, theme.container]}>
<View style={[styles.display, theme.display]}>
<Text style={[styles.displayText, theme.displayText]}>{display}</Text>
</View>
</View>
七、总结
计算器是一个经典的交互式应用,通过本篇文章,我们学习了:
- 状态管理:使用 useState 管理计算器的各种状态
- 事件处理:处理数字、运算符、等号等按钮的点击事件
- 布局设计:使用 Flexbox 实现键盘网格布局
- 性能优化:使用 memo 和 useCallback 优化性能
- 错误处理:处理除零错误等异常情况
- 扩展功能:添加科学计算、历史记录等扩展功能
计算器组件在 React Native for Harmony 中表现良好,交互流畅,是一个很好的学习案例。
@欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)