React Native for OpenHarmony 实战:汇率换算实现
amount:用户输入的金额,用字符串存储方便和 TextInput 绑定。默认值 ‘100’ 让用户打开就能看到换算结果from:源货币代码,默认人民币to:目标货币代码,默认美元resultAnim:结果区域的缩放动画,每次输入变化时触发,给用户"结果更新了"的反馈rotateAnim:交换按钮的旋转动画,点击时旋转 180 度glowAnim:结果区域的发光动画,让界面更有活力为什么amou

出国旅游、海淘购物、外贸业务,都离不开汇率换算。今天我们用 React Native 实现一个汇率换算工具,支持多种常用货币之间的互相转换,界面上用国旗图标让货币选择更直观。
汇率数据设计
在实际应用中,汇率数据通常从 API 获取。但为了避免依赖第三方服务,我们这里使用静态数据。这样做的好处是:代码可以离线运行,不需要网络权限,在 OpenHarmony 上也不用担心网络库的兼容性问题。
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';
const rates: { [key: string]: { name: string; rate: number; flag: string } } = {
CNY: { name: '人民币', rate: 1, flag: '🇨🇳' },
USD: { name: '美元', rate: 0.14, flag: '🇺🇸' },
EUR: { name: '欧元', rate: 0.13, flag: '🇪🇺' },
JPY: { name: '日元', rate: 21.0, flag: '🇯🇵' },
GBP: { name: '英镑', rate: 0.11, flag: '🇬🇧' },
KRW: { name: '韩元', rate: 186.0, flag: '🇰🇷' },
HKD: { name: '港币', rate: 1.09, flag: '🇭🇰' },
TWD: { name: '台币', rate: 4.5, flag: '🇹🇼' },
};
这个数据结构设计得很巧妙。我们用人民币(CNY)作为基准货币,汇率设为 1。其他货币的汇率表示"1 人民币等于多少该货币"。比如 USD: { rate: 0.14 } 表示 1 人民币约等于 0.14 美元。
每种货币包含三个属性:
name:中文名称,用于显示rate:相对于人民币的汇率flag:国旗 emoji,让界面更直观
使用对象而不是数组的好处是:可以通过货币代码(如 rates['USD'])直接访问,不需要遍历查找。TypeScript 的类型定义 { [key: string]: ... } 表示这是一个字符串索引的对象。
鸿蒙 ArkTS 对比:数据结构
interface CurrencyInfo {
name: string
rate: number
flag: string
}
const rates: Record<string, CurrencyInfo> = {
CNY: { name: '人民币', rate: 1, flag: '🇨🇳' },
USD: { name: '美元', rate: 0.14, flag: '🇺🇸' },
EUR: { name: '欧元', rate: 0.13, flag: '🇪🇺' },
JPY: { name: '日元', rate: 21.0, flag: '🇯🇵' },
GBP: { name: '英镑', rate: 0.11, flag: '🇬🇧' },
KRW: { name: '韩元', rate: 186.0, flag: '🇰🇷' },
HKD: { name: '港币', rate: 1.09, flag: '🇭🇰' },
TWD: { name: '台币', rate: 4.5, flag: '🇹🇼' },
}
ArkTS 中推荐使用 Record<string, T> 类型来定义字符串索引对象,这比 { [key: string]: T } 更简洁。同时定义一个 CurrencyInfo 接口,让代码更清晰。
数据结构在两个平台上完全一样,这是跨平台开发的优势——数据层代码可以直接复用。
状态和动画定义
export const CurrencyConverter: React.FC = () => {
const [amount, setAmount] = useState('100');
const [from, setFrom] = useState('CNY');
const [to, setTo] = useState('USD');
const resultAnim = useRef(new Animated.Value(1)).current;
const rotateAnim = useRef(new Animated.Value(0)).current;
const glowAnim = useRef(new Animated.Value(0)).current;
状态变量的设计:
amount:用户输入的金额,用字符串存储方便和 TextInput 绑定。默认值 ‘100’ 让用户打开就能看到换算结果from:源货币代码,默认人民币to:目标货币代码,默认美元
动画值的用途:
resultAnim:结果区域的缩放动画,每次输入变化时触发,给用户"结果更新了"的反馈rotateAnim:交换按钮的旋转动画,点击时旋转 180 度glowAnim:结果区域的发光动画,让界面更有活力
为什么 amount 用字符串而不是数字?因为 TextInput 的 value 属性需要字符串类型,如果用数字还需要来回转换。而且用户可能输入小数点,在输入过程中 “100.” 这样的字符串无法转成有效数字,用字符串存储更灵活。
发光动画初始化
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(glowAnim, { toValue: 1, duration: 1500, useNativeDriver: false }),
Animated.timing(glowAnim, { toValue: 0, duration: 1500, useNativeDriver: false }),
])
).start();
}, []);
组件挂载时启动发光动画。这是一个无限循环的呼吸效果:1.5 秒内从 0 渐变到 1,再 1.5 秒内从 1 渐变回 0,总周期 3 秒。
useNativeDriver: false 是因为我们要用这个动画值控制 shadowOpacity,阴影相关的属性不支持原生驱动。虽然性能稍差,但发光动画的变化很平缓,不会造成卡顿。
空依赖数组 [] 表示这个 effect 只在组件挂载时执行一次,不会因为状态变化而重复执行。
结果更新动画
useEffect(() => {
resultAnim.setValue(0.8);
Animated.spring(resultAnim, { toValue: 1, friction: 4, useNativeDriver: true }).start();
}, [amount, from, to]);
每当金额、源货币或目标货币变化时,触发结果区域的弹跳动画。先把缩放值设为 0.8(稍微缩小),然后用弹簧动画恢复到 1。
这个动画的作用是给用户即时反馈:你的操作生效了,结果已经更新。没有这个动画,用户可能不确定结果是否已经重新计算。
依赖数组 [amount, from, to] 表示这三个值中任何一个变化都会触发动画。这是 React Hooks 的核心概念——声明式地描述副作用的依赖关系。
核心换算逻辑
const convert = () => {
const num = parseFloat(amount) || 0;
const inCNY = num / rates[from].rate;
return (inCNY * rates[to].rate).toFixed(2);
};
换算逻辑分两步:
- 先把源货币金额转换成人民币:
num / rates[from].rate - 再把人民币转换成目标货币:
inCNY * rates[to].rate
举个例子:把 100 美元换成日元
- 100 美元 → 人民币:100 / 0.14 ≈ 714.29 人民币
- 714.29 人民币 → 日元:714.29 × 21 ≈ 15000 日元
parseFloat(amount) || 0 这个写法很实用:如果 amount 无法解析成数字(比如空字符串),parseFloat 返回 NaN,而 NaN || 0 会返回 0。这样就避免了显示 NaN 的尴尬情况。
toFixed(2) 保留两位小数,这是货币金额的常见格式。
鸿蒙 ArkTS 对比:换算逻辑
convert(): string {
let num = parseFloat(this.amount) || 0
let inCNY = num / rates[this.from].rate
return (inCNY * rates[this.to].rate).toFixed(2)
}
换算逻辑完全一样,只是 ArkTS 中需要用 this. 访问状态变量。这种纯计算函数是最容易跨平台复用的代码。
货币交换功能
const swapCurrencies = () => {
Animated.timing(rotateAnim, { toValue: 1, duration: 300, useNativeDriver: true }).start(() => {
rotateAnim.setValue(0);
});
const temp = from;
setFrom(to);
setTo(temp);
};
const spin = rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] });
交换按钮的功能是把源货币和目标货币互换。动画让这个操作更有趣:按钮旋转 180 度,暗示"交换"的含义。
动画完成后的回调 () => { rotateAnim.setValue(0); } 把动画值重置为 0,这样下次点击还能再次旋转。如果不重置,动画值已经是 1,再次执行 toValue: 1 就没有效果了。
交换逻辑用了一个临时变量 temp,这是交换两个变量值的经典写法。在 JavaScript 中也可以用解构赋值 [from, to] = [to, from],但在 React 中这样写会有问题,因为 from 和 to 是状态值,不能直接赋值。
界面渲染:头部和输入
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerIcon}>💱</Text>
<Text style={styles.headerTitle}>汇率换算</Text>
<Text style={styles.headerSubtitle}>实时货币转换</Text>
</View>
<View style={styles.inputCard}>
<Text style={styles.inputLabel}>输入金额</Text>
<View style={styles.inputWrapper}>
<Text style={styles.inputFlag}>{rates[from].flag}</Text>
<TextInput
style={styles.input}
value={amount}
onChangeText={setAmount}
keyboardType="numeric"
placeholder="0"
placeholderTextColor="#666"
/>
<Text style={styles.inputCurrency}>{from}</Text>
</View>
</View>
输入区域的设计考虑了用户体验:
- 左边显示当前源货币的国旗,让用户一眼就知道输入的是什么货币
- 中间是输入框,数字键盘方便输入
- 右边显示货币代码,进一步确认货币类型
rates[from].flag 这种写法动态获取当前源货币的国旗。当用户切换源货币时,国旗会自动更新。
货币选择网格
<Text style={styles.sectionTitle}>从</Text>
<View style={styles.currencyGrid}>
{Object.entries(rates).map(([code, { name, flag }]) => (
<TouchableOpacity
key={code}
style={[styles.currencyBtn, from === code && styles.currencyBtnActive]}
onPress={() => setFrom(code)}
activeOpacity={0.7}
>
<Text style={styles.currencyFlag}>{flag}</Text>
<Text style={[styles.currencyCode, from === code && styles.currencyTextActive]}>{code}</Text>
<Text style={[styles.currencyName, from === code && styles.currencyTextActive]}>{name}</Text>
</TouchableOpacity>
))}
</View>
货币选择用网格布局,每个货币一个按钮。Object.entries(rates) 把对象转换成 [key, value] 数组,方便用 map 遍历。
解构赋值 ([code, { name, flag }]) 同时获取货币代码和货币信息中的 name、flag 属性,代码更简洁。
选中状态通过比较 from === code 来判断,选中的按钮应用 currencyBtnActive 样式,背景变成蓝色,还有阴影效果。
这种"选择一个"的交互模式在移动端很常见,比单选按钮更直观,触摸目标也更大。
鸿蒙 ArkTS 对比:网格布局
Text('从')
.fontSize(16)
.fontColor('#4A90D9')
.fontWeight(FontWeight.Bold)
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(Object.entries(rates), ([code, info]: [string, CurrencyInfo]) => {
Column() {
Text(info.flag)
.fontSize(20)
Text(code)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(this.from === code ? Color.White : Color.White)
Text(info.name)
.fontSize(10)
.fontColor(this.from === code ? Color.White : '#888888')
}
.width('23%')
.margin('1%')
.padding(12)
.borderRadius(12)
.backgroundColor(this.from === code ? '#4A90D9' : '#1a1a3e')
.onClick(() => {
this.from = code
})
})
}
ArkTS 中用 Flex 组件配合 FlexWrap.Wrap 实现网格布局。ForEach 遍历数据,每个货币用 Column 组件垂直排列国旗、代码和名称。
两种写法的结构类似,只是语法风格不同。React Native 用 JSX 和 style 数组,ArkTS 用声明式 UI 和链式调用。
交换按钮
<TouchableOpacity style={styles.swapBtn} onPress={swapCurrencies} activeOpacity={0.7}>
<Animated.View style={{ transform: [{ rotate: spin }] }}>
<Text style={styles.swapIcon}>🔄</Text>
</Animated.View>
</TouchableOpacity>
交换按钮放在两个货币选择区域之间,位置很直观。用 Animated.View 包裹图标,应用旋转动画。
alignSelf: 'center' 让按钮在水平方向居中,不需要额外的容器。
目标货币选择
<Text style={styles.sectionTitle}>转换为</Text>
<View style={styles.currencyGrid}>
{Object.entries(rates).map(([code, { name, flag }]) => (
<TouchableOpacity
key={code}
style={[styles.currencyBtn, to === code && styles.currencyBtnActive]}
onPress={() => setTo(code)}
activeOpacity={0.7}
>
<Text style={styles.currencyFlag}>{flag}</Text>
<Text style={[styles.currencyCode, to === code && styles.currencyTextActive]}>{code}</Text>
<Text style={[styles.currencyName, to === code && styles.currencyTextActive]}>{name}</Text>
</TouchableOpacity>
))}
</View>
目标货币选择和源货币选择的代码几乎一样,只是状态变量从 from 变成了 to。这种重复代码可以提取成一个子组件,但为了保持示例的简洁性,这里直接复制。
在实际项目中,如果这种模式出现三次以上,就应该考虑抽取组件了。
结果显示
<Animated.View style={[styles.result, {
transform: [{ scale: resultAnim }, { perspective: 1000 }],
shadowOpacity: glowAnim.interpolate({ inputRange: [0, 1], outputRange: [0.3, 0.6] }),
}]}>
<View style={styles.resultRow}>
<Text style={styles.resultFlag}>{rates[from].flag}</Text>
<Text style={styles.resultFrom}>{amount} {from}</Text>
</View>
<Text style={styles.resultEquals}>=</Text>
<View style={styles.resultRow}>
<Text style={styles.resultFlag}>{rates[to].flag}</Text>
<Text style={styles.resultValue}>{convert()}</Text>
<Text style={styles.resultCurrency}>{to}</Text>
</View>
</Animated.View>
结果区域是整个界面的焦点,设计上做了几个处理:
- 缩放动画:
scale: resultAnim让结果区域在更新时有弹跳效果 - 透视效果:
perspective: 1000为 3D 变换提供透视,虽然这里没有用到 3D 旋转,但加上透视可以让缩放动画看起来更自然 - 发光动画:
shadowOpacity在 0.3 和 0.6 之间变化,形成呼吸效果
结果的布局分三行:
- 第一行:源货币国旗 + 输入金额 + 源货币代码
- 第二行:等号
- 第三行:目标货币国旗 + 换算结果 + 目标货币代码
convert() 函数在渲染时调用,每次渲染都会重新计算。因为这是一个纯函数且计算量很小,不需要用 useMemo 优化。
免责声明
<Text style={styles.disclaimer}>* 汇率仅供参考,非实时数据</Text>
</ScrollView>
);
};
底部加一行免责声明,告诉用户这不是实时汇率。这是一个好习惯,避免用户因为汇率不准确而产生误解。
样式定义:输入区域
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' },
headerSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },
inputCard: {
backgroundColor: '#1a1a3e',
borderRadius: 20,
padding: 20,
marginBottom: 20,
borderWidth: 1,
borderColor: '#3a3a6a',
},
inputLabel: { color: '#888', fontSize: 14, marginBottom: 12 },
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#252550',
borderRadius: 12,
paddingHorizontal: 16,
},
inputFlag: { fontSize: 24, marginRight: 12 },
input: { flex: 1, fontSize: 32, color: '#fff', paddingVertical: 16, fontWeight: '300' },
inputCurrency: { color: '#4A90D9', fontSize: 18, fontWeight: '600' },
输入框的字号设为 32,比较大,方便用户查看和输入。fontWeight: '300' 使用细字重,看起来更现代。货币代码用蓝色,和主题色一致。
样式定义:货币网格
sectionTitle: { color: '#4A90D9', fontSize: 16, fontWeight: '600', marginBottom: 12 },
currencyGrid: { flexDirection: 'row', flexWrap: 'wrap', marginBottom: 16 },
currencyBtn: {
width: '23%',
margin: '1%',
backgroundColor: '#1a1a3e',
padding: 12,
borderRadius: 12,
alignItems: 'center',
borderWidth: 1,
borderColor: '#3a3a6a',
},
currencyBtnActive: {
backgroundColor: '#4A90D9',
borderColor: '#4A90D9',
shadowColor: '#4A90D9',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.4,
shadowRadius: 8,
},
currencyFlag: { fontSize: 20, marginBottom: 4 },
currencyCode: { fontSize: 14, fontWeight: '600', color: '#fff' },
currencyName: { fontSize: 10, color: '#888', marginTop: 2 },
currencyTextActive: { color: '#fff' },
货币按钮用 width: '23%' 和 margin: '1%' 实现四列布局(23% × 4 + 1% × 8 = 100%)。选中状态加蓝色阴影,让按钮看起来像是浮起来的。
货币名称用 10 号小字,因为空间有限,而且货币代码已经足够识别了。
样式定义:结果区域
swapBtn: {
alignSelf: 'center',
width: 50,
height: 50,
borderRadius: 25,
backgroundColor: '#252550',
justifyContent: 'center',
alignItems: 'center',
marginVertical: 8,
},
swapIcon: { fontSize: 24 },
result: {
backgroundColor: '#1a1a3e',
borderRadius: 20,
padding: 24,
marginTop: 8,
borderWidth: 1,
borderColor: '#4A90D9',
alignItems: 'center',
shadowColor: '#4A90D9',
shadowOffset: { width: 0, height: 8 },
shadowRadius: 20,
},
resultRow: { flexDirection: 'row', alignItems: 'center' },
resultFlag: { fontSize: 28, marginRight: 10 },
resultFrom: { color: '#888', fontSize: 18 },
resultEquals: { color: '#4A90D9', fontSize: 28, marginVertical: 12 },
resultValue: { color: '#fff', fontSize: 42, fontWeight: '700' },
resultCurrency: { color: '#4A90D9', fontSize: 24, marginLeft: 8 },
disclaimer: { textAlign: 'center', color: '#666', marginTop: 20, fontSize: 12 },
});
结果区域用蓝色边框和蓝色阴影,让它成为视觉焦点。换算结果用 42 号大字,是整个界面最大的文字,突出最重要的信息。
小结
这个汇率换算工具展示了如何用 React Native 实现一个数据驱动的应用。通过合理的数据结构设计,换算逻辑变得简单清晰。国旗 emoji 让货币选择更直观,动画效果增强了交互体验。
在 OpenHarmony 平台上,这些功能都能正常工作。如果需要实时汇率,可以在联网时从 API 获取数据更新 rates 对象,换算逻辑不需要修改。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)