React Native for OpenHarmony 实战:年龄计算实现

年龄计算看起来简单,但要精确到年月日就需要考虑很多细节。今天我们用 React Native 实现一个年龄计算器,不仅显示精确年龄,还能告诉你距离下次生日还有多少天。
功能设计
这个年龄计算器需要实现:
- 输入出生日期
- 计算精确年龄(年、月、日)
- 显示已经活了多少天、周、月、小时
- 显示距离下次生日的天数和进度条
年龄计算的难点在于月份天数不固定,需要正确处理借位。
状态和动画定义
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Animated, ScrollView } from 'react-native';
export const AgeCalculator: React.FC = () => {
const [birthday, setBirthday] = useState('');
const [result, setResult] = useState<any>(null);
const scaleAnim = useRef(new Animated.Value(0)).current;
const rotateAnim = useRef(new Animated.Value(0)).current;
const pulseAnim = useRef(new Animated.Value(1)).current;
const progressAnim = useRef(new Animated.Value(0)).current;
状态变量:
birthday存储用户输入的出生日期result存储计算结果
动画值:
scaleAnim控制结果卡片的弹出效果pulseAnim控制头部图标的脉冲效果progressAnim控制进度条的填充动画
鸿蒙 ArkTS 对比:状态定义
@Entry
@Component
struct AgeCalculator {
@State birthday: string = ''
@State result: AgeResult | null = null
@State scaleValue: number = 0
@State progressValue: number = 0
interface AgeResult {
years: number
months: number
days: number
totalDays: number
totalWeeks: number
totalMonths: number
totalHours: number
daysToNext: number
progressToNext: number
}
ArkTS 中用接口定义结果类型,让代码更清晰。
头部脉冲动画
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, { toValue: 1.1, duration: 800, useNativeDriver: true }),
Animated.timing(pulseAnim, { toValue: 1, duration: 800, useNativeDriver: true }),
])
).start();
}, []);
蛋糕图标有轻微的脉冲效果,1.6 秒一个周期,让界面更有活力。
核心年龄计算逻辑
const calculate = () => {
const birth = new Date(birthday);
if (isNaN(birth.getTime())) {
setResult({ error: '请输入有效日期格式 (YYYY-MM-DD)' });
return;
}
const today = new Date();
let years = today.getFullYear() - birth.getFullYear();
let months = today.getMonth() - birth.getMonth();
let days = today.getDate() - birth.getDate();
if (days < 0) {
months--;
days += new Date(today.getFullYear(), today.getMonth(), 0).getDate();
}
if (months < 0) {
years--;
months += 12;
}
年龄计算的关键是处理借位:
- 如果日期差为负数,从月份借一个月,加上上个月的天数
- 如果月份差为负数,从年份借一年,加上 12 个月
new Date(year, month, 0).getDate() 是获取上个月天数的技巧,传入 0 作为日期会返回上个月的最后一天。
鸿蒙 ArkTS 对比:年龄计算
calculateAge() {
let birth = new Date(this.birthday)
if (isNaN(birth.getTime())) {
this.result = { error: '请输入有效日期格式' } as AgeResult
return
}
let today = new Date()
let years = today.getFullYear() - birth.getFullYear()
let months = today.getMonth() - birth.getMonth()
let days = today.getDate() - birth.getDate()
// 处理借位
if (days < 0) {
months--
let lastMonth = new Date(today.getFullYear(), today.getMonth(), 0)
days += lastMonth.getDate()
}
if (months < 0) {
years--
months += 12
}
// ... 其他计算
}
计算逻辑完全一样,JavaScript 的日期处理在 ArkTS 中同样适用。
统计数据计算
const totalDays = Math.floor((today.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24));
const totalWeeks = Math.floor(totalDays / 7);
const totalMonths = years * 12 + months;
const totalHours = totalDays * 24;
这些统计数据计算比较直接:
- 总天数:时间差除以一天的毫秒数
- 总周数:总天数除以 7
- 总月数:年数乘以 12 加上月数
- 总小时数:总天数乘以 24
下次生日计算
const nextBirthday = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
if (nextBirthday < today) nextBirthday.setFullYear(nextBirthday.getFullYear() + 1);
const daysToNext = Math.ceil((nextBirthday.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
const progressToNext = ((365 - daysToNext) / 365) * 100;
下次生日的计算:
- 用今年的年份和出生的月日创建日期
- 如果这个日期已经过了,就加一年
- 计算距离下次生日的天数
- 计算进度百分比(已经过了多少)
动画触发
// 动画
scaleAnim.setValue(0);
progressAnim.setValue(0);
Animated.parallel([
Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
Animated.timing(progressAnim, { toValue: progressToNext, duration: 1000, useNativeDriver: false }),
]).start();
setResult({ years, months, days, totalDays, totalWeeks, totalMonths, totalHours, daysToNext, progressToNext });
};
计算完成后同时启动两个动画:结果卡片弹出和进度条填充。进度条动画用 useNativeDriver: false,因为要动态改变宽度。
界面渲染:头部和输入
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
<Text style={styles.headerIcon}>🎂</Text>
</Animated.View>
<Text style={styles.headerTitle}>年龄计算</Text>
<Text style={styles.headerSubtitle}>精确计算您的年龄</Text>
</View>
<View style={styles.inputCard}>
<Text style={styles.label}>出生日期</Text>
<View style={styles.inputWrapper}>
<Text style={styles.inputIcon}>📅</Text>
<TextInput
style={styles.input}
value={birthday}
onChangeText={setBirthday}
placeholder="YYYY-MM-DD"
placeholderTextColor="#666"
/>
</View>
</View>
<TouchableOpacity style={styles.btn} onPress={calculate} activeOpacity={0.8}>
<View style={styles.btnInner}>
<Text style={styles.btnIcon}>✨</Text>
<Text style={styles.btnText}>计算年龄</Text>
</View>
</TouchableOpacity>
输入框前面加了日历图标,让用户更容易理解输入内容。
鸿蒙 ArkTS 对比:输入组件
Column() {
Text('出生日期')
.fontSize(14)
.fontColor('#888888')
Row() {
Text('📅')
.fontSize(24)
.margin({ right: 12 })
TextInput({ placeholder: 'YYYY-MM-DD' })
.layoutWeight(1)
.fontSize(20)
.fontColor(Color.White)
.backgroundColor(Color.Transparent)
.onChange((value: string) => {
this.birthday = value
})
}
.backgroundColor('#252550')
.borderRadius(12)
.padding({ left: 16, right: 16 })
}
ArkTS 中用 Row 组件实现图标和输入框的水平排列,layoutWeight(1) 让输入框占据剩余空间。
主要年龄显示
{result && !result.error && (
<Animated.View style={[styles.resultCard, { transform: [{ scale: scaleAnim }] }]}>
<View style={styles.mainAge}>
<View style={styles.ageCircle}>
<Text style={styles.ageNumber}>{result.years}</Text>
<Text style={styles.ageUnit}>岁</Text>
</View>
<Text style={styles.ageDetail}>{result.months}个月 {result.days}天</Text>
</View>
主要年龄用大圆圈显示,年数是最醒目的信息。下面补充显示月和日,给出完整的精确年龄。
进度条显示
<View style={styles.progressSection}>
<Text style={styles.progressLabel}>距离下次生日还有 {result.daysToNext} 天</Text>
<View style={styles.progressBar}>
<Animated.View style={[styles.progressFill, {
width: progressAnim.interpolate({ inputRange: [0, 100], outputRange: ['0%', '100%'] })
}]} />
</View>
</View>
进度条显示距离下次生日的进度,用 interpolate 把 0-100 的数值映射成 0%-100% 的宽度。动画效果让进度条从左到右填充。
统计数据网格
<View style={styles.statsGrid}>
{[
{ icon: '📆', value: result.totalDays.toLocaleString(), label: '天' },
{ icon: '📅', value: result.totalWeeks.toLocaleString(), label: '周' },
{ icon: '🗓️', value: result.totalMonths, label: '月' },
{ icon: '⏰', value: result.totalHours.toLocaleString(), label: '小时' },
].map((item, i) => (
<View key={i} style={styles.statItem}>
<Text style={styles.statIcon}>{item.icon}</Text>
<Text style={styles.statValue}>{item.value}</Text>
<Text style={styles.statLabel}>{item.label}</Text>
</View>
))}
</View>
</Animated.View>
)}
统计数据用四宫格显示,每个格子包含图标、数值和标签。toLocaleString() 给大数字添加千位分隔符。
错误提示
{result?.error && (
<View style={styles.errorBox}>
<Text style={styles.errorText}>⚠️ {result.error}</Text>
</View>
)}
</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',
},
label: { color: '#888', fontSize: 14, marginBottom: 12 },
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#252550',
borderRadius: 12,
paddingHorizontal: 16,
},
inputIcon: { fontSize: 24, marginRight: 12 },
input: { flex: 1, fontSize: 20, color: '#fff', paddingVertical: 14 },
输入框用 flexDirection: 'row' 实现图标和文本框的水平排列。
按钮和结果样式
btn: {
backgroundColor: '#4A90D9',
borderRadius: 16,
marginBottom: 24,
shadowColor: '#4A90D9',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.4,
shadowRadius: 15,
},
btnInner: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 18 },
btnIcon: { fontSize: 20, marginRight: 10 },
btnText: { color: '#fff', fontSize: 18, fontWeight: '700' },
resultCard: {
backgroundColor: '#1a1a3e',
borderRadius: 20,
padding: 24,
borderWidth: 1,
borderColor: '#3a3a6a',
},
mainAge: { alignItems: 'center', marginBottom: 24 },
ageCircle: {
width: 140,
height: 140,
borderRadius: 70,
backgroundColor: '#252550',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 4,
borderColor: '#4A90D9',
marginBottom: 12,
},
ageNumber: { fontSize: 48, fontWeight: '700', color: '#4A90D9' },
ageUnit: { fontSize: 18, color: '#888' },
ageDetail: { fontSize: 18, color: '#fff' },
年龄圆圈用蓝色边框,数字也用蓝色,形成视觉统一。
进度条和统计样式
progressSection: { marginBottom: 24 },
progressLabel: { color: '#888', fontSize: 14, marginBottom: 10, textAlign: 'center' },
progressBar: {
height: 8,
backgroundColor: '#252550',
borderRadius: 4,
overflow: 'hidden',
},
progressFill: { height: '100%', backgroundColor: '#4A90D9', borderRadius: 4 },
statsGrid: { flexDirection: 'row', flexWrap: 'wrap' },
statItem: {
width: '48%',
backgroundColor: '#252550',
borderRadius: 16,
padding: 16,
margin: '1%',
alignItems: 'center',
},
statIcon: { fontSize: 24, marginBottom: 8 },
statValue: { fontSize: 20, fontWeight: '700', color: '#fff' },
statLabel: { fontSize: 12, color: '#888', marginTop: 4 },
errorBox: {
backgroundColor: 'rgba(231, 76, 60, 0.2)',
borderRadius: 12,
padding: 16,
borderWidth: 1,
borderColor: '#e74c3c',
},
errorText: { color: '#e74c3c', textAlign: 'center' },
});
进度条用 overflow: 'hidden' 确保填充部分不会超出容器。统计网格用 width: '48%' 实现两列布局。
小结
这个年龄计算器展示了 JavaScript 日期处理的一些技巧,特别是年龄计算中的借位处理。进度条动画让结果展示更有趣,统计数据让用户从不同角度了解自己的年龄。
在 OpenHarmony 平台上,日期计算和动画效果都能正常工作。JavaScript 的 Date 对象是跨平台的,不需要针对鸿蒙做特殊处理。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)