React Native for OpenHarmony 实战:小费计算实现
今天我们用 React Native 实现一个小费计算工具,支持快速选择小费比例,计算分摊金额。
状态设计
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';
export const TipCalculator: React.FC = () => {
const [bill, setBill] = useState('');
const [tipPercent, setTipPercent] = useState('15');
const [people, setPeople] = useState('1');
const resultAnim = useRef(new Animated.Value(1)).current;
const peopleAnim = useRef(new Animated.Value(1)).current;
const pulseAnim = useRef(new Animated.Value(1)).current;
状态设计包含账单金额、小费比例、用餐人数、动画值。
账单金额:bill 是字符串类型,存储用户输入的账单金额。
小费比例:tipPercent 是字符串类型,存储小费百分比。默认值 '15',表示 15%。
用餐人数:people 是字符串类型,存储用餐人数。默认值 '1',表示 1 人。
三个动画值:
resultAnim:结果卡片的缩放动画peopleAnim:人数的缩放动画pulseAnim:每人支付的脉冲动画(循环放大缩小)
为什么小费比例默认 15%?因为 15% 是国际上最常见的小费比例。在美国、加拿大等国家,15% 是标准小费。
脉冲动画
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, { toValue: 1.02, duration: 1500, useNativeDriver: true }),
Animated.timing(pulseAnim, { toValue: 1, duration: 1500, useNativeDriver: true }),
])
).start();
}, []);
组件挂载时,启动每人支付的脉冲动画。
循环动画:Animated.loop() 让动画无限循环。
序列动画:
- 放大到 102%(1.5 秒)
- 缩回到 100%(1.5 秒)
为什么用脉冲动画?因为"每人支付"是用户最关心的信息。多人用餐时,用户想知道"我要付多少钱"。脉冲动画让这个数字更醒目。
输入变化动画
useEffect(() => {
Animated.sequence([
Animated.timing(resultAnim, { toValue: 0.95, duration: 100, useNativeDriver: true }),
Animated.spring(resultAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
]).start();
}, [bill, tipPercent, people]);
监听账单、小费比例、人数变化,触发结果卡片的动画。
依赖数组:[bill, tipPercent, people] 表示当这三个值任意一个变化时,执行动画。
序列动画:
- 缩小到 95%(100ms)
- 弹回到 100%(弹簧动画)
为什么监听三个输入?因为这三个值都会影响结果。账单变化,小费和总计都变化。小费比例变化,小费和总计变化。人数变化,每人支付变化。动画让用户感知到"结果更新了"。
人数动画
const animatePeople = () => {
Animated.sequence([
Animated.timing(peopleAnim, { toValue: 1.2, duration: 100, useNativeDriver: true }),
Animated.spring(peopleAnim, { toValue: 1, friction: 3, useNativeDriver: true }),
]).start();
};
人数变化时触发的动画函数。
序列动画:
- 放大到 120%(100ms)
- 弹回到 100%(弹簧动画)
为什么人数变化要单独动画?因为人数是通过按钮修改的,不是输入框。单独动画让用户看到"人数变化了",增加交互反馈。
计算函数
const calculate = () => {
const b = parseFloat(bill) || 0;
const t = parseFloat(tipPercent) || 0;
const p = parseInt(people) || 1;
const tip = b * t / 100;
const total = b + tip;
const perPerson = total / p;
return { tip: tip.toFixed(2), total: total.toFixed(2), perPerson: perPerson.toFixed(2) };
};
const result = calculate();
const tipOptions = [10, 15, 18, 20, 25];
计算小费、总计、每人支付。
解析输入:
parseFloat(bill) || 0:账单金额,默认 0parseFloat(tipPercent) || 0:小费比例,默认 0parseInt(people) || 1:用餐人数,默认 1
计算小费:tip = b * t / 100。比如账单 100,小费 15%,小费 100 × 15 / 100 = 15。
计算总计:total = b + tip。比如账单 100,小费 15,总计 100 + 15 = 115。
计算每人支付:perPerson = total / p。比如总计 115,2 人,每人支付 115 / 2 = 57.5。
保留 2 位小数:toFixed(2) 让结果更易读。
小费选项:[10, 15, 18, 20, 25] 是常见的小费比例。
为什么这些比例?因为这是国际上最常见的小费比例:
- 10%:服务一般
- 15%:标准小费
- 18%:服务良好
- 20%:服务优秀
- 25%:服务非常好
界面渲染:头部和账单输入
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerIcon}>💰</Text>
<Text style={styles.headerTitle}>小费计算</Text>
</View>
<View style={styles.inputCard}>
<Text style={styles.label}>💵 账单金额</Text>
<View style={styles.inputWrapper}>
<TextInput style={styles.input} value={bill} onChangeText={setBill} keyboardType="numeric" placeholder="0.00" placeholderTextColor="#666" />
</View>
</View>
头部显示标题,账单输入卡片包含标签和输入框。
头部:
- 图标:💰 钱袋
- 标题:小费计算
账单输入卡片:
- 标签:💵 账单金额
- 输入框:
keyboardType="numeric"弹出数字键盘,textAlign: 'center'居中对齐,字号 28
为什么输入框字号 28?因为账单金额是重要信息,大字号让用户看得更清楚。
小费比例选择
<View style={styles.inputCard}>
<Text style={styles.label}>📊 小费比例</Text>
<View style={styles.tipOptions}>
{tipOptions.map(t => (
<TouchableOpacity key={t} style={[styles.tipBtn, tipPercent === String(t) && styles.tipBtnActive]} onPress={() => setTipPercent(String(t))} activeOpacity={0.7}>
<Text style={[styles.tipText, tipPercent === String(t) && styles.tipTextActive]}>{t}%</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.customInput}>
<TextInput style={styles.inputSmall} value={tipPercent} onChangeText={setTipPercent} keyboardType="numeric" placeholder="自定义 %" placeholderTextColor="#666" />
</View>
</View>
小费比例卡片包含快速选择按钮和自定义输入框。
快速选择按钮:
- 遍历
tipOptions数组,生成 5 个按钮 - 当前选中的按钮用蓝色背景(
tipBtnActive) - 按钮文字显示小费比例,比如"15%"
- 横向布局,每个按钮占相同宽度(
flex: 1)
自定义输入框:
- 在快速选择按钮下面
- 字号 16,居中对齐
- 占位符"自定义 %"
为什么提供自定义输入框?因为有些用户想输入特殊的小费比例,比如 12% 或 22%。快速选择按钮覆盖常见情况,自定义输入框覆盖特殊情况。
用餐人数
<View style={styles.inputCard}>
<Text style={styles.label}>👥 用餐人数</Text>
<View style={styles.peopleRow}>
<TouchableOpacity style={styles.peopleBtn} onPress={() => { animatePeople(); setPeople(String(Math.max(1, parseInt(people) - 1))); }} activeOpacity={0.7}>
<Text style={styles.peopleBtnText}>−</Text>
</TouchableOpacity>
<Animated.Text style={[styles.peopleCount, { transform: [{ scale: peopleAnim }] }]}>{people}</Animated.Text>
<TouchableOpacity style={styles.peopleBtn} onPress={() => { animatePeople(); setPeople(String(parseInt(people) + 1)); }} activeOpacity={0.7}>
<Text style={styles.peopleBtnText}>+</Text>
</TouchableOpacity>
</View>
</View>
用餐人数卡片包含减号按钮、人数显示、加号按钮。
减号按钮:
- 点击时触发人数动画
- 人数减 1,最小值 1(
Math.max(1, parseInt(people) - 1)) - 圆形按钮,蓝色背景,白色文字
人数显示:
- 字号 40,白色,加粗
- 应用缩放动画
- 左右各有 40 的间距
加号按钮:
- 点击时触发人数动画
- 人数加 1
- 圆形按钮,蓝色背景,白色文字
为什么用按钮而不是输入框?因为人数通常是小整数(1-10),用按钮比输入框更方便。点击加减按钮比输入数字更快。
为什么最小值是 1?因为至少有 1 个人用餐。0 人没有意义。
结果显示
<Animated.View style={[styles.result, { transform: [{ scale: resultAnim }] }]}>
<View style={styles.resultRow}>
<Text style={styles.resultLabel}>小费</Text>
<Text style={styles.resultValue}>¥ {result.tip}</Text>
</View>
<View style={styles.resultRow}>
<Text style={styles.resultLabel}>总计</Text>
<Text style={styles.resultValue}>¥ {result.total}</Text>
</View>
<Animated.View style={[styles.resultMain, { transform: [{ scale: pulseAnim }] }]}>
<Text style={styles.resultLabelMain}>每人支付</Text>
<Text style={styles.resultValueMain}>¥ {result.perPerson}</Text>
</Animated.View>
</Animated.View>
</ScrollView>
);
};
结果卡片显示小费、总计、每人支付。
结果卡片动画:应用缩放动画,输入变化时缩小再弹回。
小费行:
- 标签:小费
- 金额:白色文字,字号 20
总计行:
- 标签:总计
- 金额:白色文字,字号 20
每人支付:
- 标签:每人支付
- 金额:蓝色文字,字号 40,应用脉冲动画
- 居中对齐
为什么每人支付字号最大?因为这是用户最关心的信息。多人用餐时,用户想知道"我要付多少钱"。大字号 + 蓝色 + 脉冲动画,让这个数字成为视觉焦点。
鸿蒙 ArkTS 对比:小费计算
@State bill: string = ''
@State tipPercent: string = '15'
@State people: string = '1'
calculate() {
const b = parseFloat(this.bill) || 0
const t = parseFloat(this.tipPercent) || 0
const p = parseInt(this.people) || 1
const tip = b * t / 100
const total = b + tip
const perPerson = total / p
return { tip: tip.toFixed(2), total: total.toFixed(2), perPerson: perPerson.toFixed(2) }
}
build() {
Column() {
Text('小费计算')
.fontSize(28)
.fontWeight(FontWeight.Bold)
Column() {
Text('💵 账单金额')
TextInput({ text: this.bill, placeholder: '0.00' })
.type(InputType.Number)
.fontSize(28)
.textAlign(TextAlign.Center)
.onChange((value: string) => {
this.bill = value
})
}
Column() {
Text('📊 小费比例')
Row() {
ForEach([10, 15, 18, 20, 25], (t: number) => {
Button(`${t}%`)
.backgroundColor(this.tipPercent === String(t) ? '#4A90D9' : '#252550')
.onClick(() => {
this.tipPercent = String(t)
})
})
}
TextInput({ text: this.tipPercent, placeholder: '自定义 %' })
.type(InputType.Number)
.onChange((value: string) => {
this.tipPercent = value
})
}
Column() {
Text('👥 用餐人数')
Row() {
Button('−')
.onClick(() => {
this.people = String(Math.max(1, parseInt(this.people) - 1))
})
Text(this.people)
.fontSize(40)
.fontWeight(FontWeight.Bold)
Button('+')
.onClick(() => {
this.people = String(parseInt(this.people) + 1)
})
}
}
Column() {
Row() {
Text('小费')
Text(`¥ ${this.calculate().tip}`)
}
Row() {
Text('总计')
Text(`¥ ${this.calculate().total}`)
}
Column() {
Text('每人支付')
Text(`¥ ${this.calculate().perPerson}`)
.fontSize(40)
.fontColor('#4A90D9')
.fontWeight(FontWeight.Bold)
}
}
}
}
ArkTS 中的小费计算逻辑完全一样。核心是公式:小费 = 账单 × 比例 / 100,总计 = 账单 + 小费,每人支付 = 总计 / 人数。parseFloat()、parseInt()、toFixed()、Math.max() 都是标准 JavaScript API,跨平台通用。
样式定义
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0f0f23', padding: 20 },
header: { alignItems: 'center', marginBottom: 24 },
headerIcon: { fontSize: 50, marginBottom: 8 },
headerTitle: { fontSize: 28, fontWeight: '700', color: '#fff' },
inputCard: { backgroundColor: '#1a1a3e', borderRadius: 16, padding: 16, marginBottom: 16, borderWidth: 1, borderColor: '#3a3a6a' },
label: { fontSize: 14, color: '#888', marginBottom: 12 },
inputWrapper: { backgroundColor: '#252550', borderRadius: 12 },
input: { padding: 16, fontSize: 28, textAlign: 'center', color: '#fff' },
inputSmall: { padding: 12, fontSize: 16, textAlign: 'center', color: '#fff' },
customInput: { backgroundColor: '#252550', borderRadius: 10, marginTop: 10 },
tipOptions: { flexDirection: 'row', marginBottom: 8 },
tipBtn: { flex: 1, padding: 12, backgroundColor: '#252550', borderRadius: 10, marginHorizontal: 3, alignItems: 'center' },
tipBtnActive: { backgroundColor: '#4A90D9' },
tipText: { color: '#888', fontWeight: '600' },
tipTextActive: { color: '#fff' },
peopleRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
peopleBtn: { width: 56, height: 56, backgroundColor: '#4A90D9', borderRadius: 28, justifyContent: 'center', alignItems: 'center' },
peopleBtnText: { color: '#fff', fontSize: 28, fontWeight: '600' },
peopleCount: { fontSize: 40, fontWeight: '700', marginHorizontal: 40, color: '#fff' },
result: { backgroundColor: '#1a1a3e', padding: 20, borderRadius: 20, borderWidth: 1, borderColor: '#3a3a6a' },
resultRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#3a3a6a' },
resultLabel: { fontSize: 16, color: '#888' },
resultValue: { fontSize: 20, color: '#fff', fontWeight: '600' },
resultMain: { paddingTop: 20, alignItems: 'center' },
resultLabelMain: { fontSize: 14, color: '#888', marginBottom: 8 },
resultValueMain: { fontSize: 40, color: '#4A90D9', fontWeight: '700' },
});
容器用深蓝黑色背景。输入卡片用深蓝色背景,圆角 16。小费比例按钮横向布局,每个按钮占相同宽度。人数按钮圆形,宽高 56,圆角 28。每人支付字号 40,蓝色。
小结
这个小费计算工具展示了实时计算和交互反馈的实现。用公式计算小费、总计、每人支付,监听输入变化触发动画。快速选择按钮让用户不需要手动输入小费比例。加减按钮让用户方便调整人数。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)