React Native for OpenHarmony 实战:抽签工具
React Native 抽签工具实现摘要(148字): 本文实现了一个支持自定义选项的抽签工具,核心功能包括: 状态管理:维护选项列表、抽签结果和动画状态 多重动画:包含旋转(720度)、缩放(1-1.2倍)、发光(呼吸效果)和按钮反馈动画 抽签逻辑:随机切换选项15次后确定结果,伴随动画效果增强交互体验 预设模板:提供"今天吃什么"等3个常用场景模板 动画优化:使用Anim

今天我们用 React Native 实现一个抽签工具,支持自定义选项列表,带有旋转、缩放、发光等多重动画效果。
状态设计
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';
export const LotteryPicker: React.FC = () => {
const [options, setOptions] = useState('选项1\n选项2\n选项3\n选项4\n选项5');
const [result, setResult] = useState<string | null>(null);
const [isAnimating, setIsAnimating] = useState(false);
const scaleAnim = useRef(new Animated.Value(1)).current;
const rotateAnim = useRef(new Animated.Value(0)).current;
const glowAnim = useRef(new Animated.Value(0)).current;
const buttonAnim = useRef(new Animated.Value(1)).current;
状态设计包含选项列表、抽签结果、动画状态。
选项列表:options 是多行文本,每行一个选项。默认值是 5 个选项,用换行符 \n 分隔。用户可以编辑这个文本,添加或删除选项。
抽签结果:result 是当前显示的结果。初始值是 null,显示"点击抽签"提示。抽签过程中会快速切换不同的选项,最后停在随机选中的选项上。
动画状态:isAnimating 标记是否正在抽签。抽签过程中禁用按钮,防止重复点击。
四个动画值:
scaleAnim:结果框的缩放动画,抽签过程中快速缩放,最终结果放大后缩回rotateAnim:结果框的旋转动画,抽签时旋转 720 度(两圈)glowAnim:结果框的发光动画,阴影透明度循环变化,营造"呼吸"效果buttonAnim:按钮的缩放动画,点击时缩小再弹回
发光动画循环
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(glowAnim, { toValue: 1, duration: 1500, useNativeDriver: false }),
Animated.timing(glowAnim, { toValue: 0, duration: 1500, useNativeDriver: false }),
])
).start();
}, []);
组件加载时启动发光动画的无限循环。
循环动画:Animated.loop 让动画无限重复。内部是一个序列动画,先从 0 到 1(1.5 秒),再从 1 到 0(1.5 秒),总共 3 秒一个周期。
为什么不用 useNativeDriver?因为发光动画控制的是 shadowOpacity(阴影透明度),这是一个样式属性,不是变换属性。useNativeDriver 只支持 transform 和 opacity,不支持 shadowOpacity。如果强制使用 useNativeDriver: true,会报错。
发光效果:glowAnim 的值在 0 和 1 之间循环变化,后面会用插值映射到阴影透明度 0.3-0.6。阴影透明度变化让结果框看起来像在"呼吸",吸引用户注意。
为什么用 1500ms?因为 1.5 秒的节奏不快不慢,既能让用户注意到动画,又不会太频繁导致视觉疲劳。3 秒一个完整周期,符合人的呼吸节奏。
抽签核心逻辑
const pick = () => {
const items = options.split('\n').filter(s => s.trim());
if (items.length === 0) return;
setIsAnimating(true);
// 按钮动画
Animated.sequence([
Animated.timing(buttonAnim, { toValue: 0.9, duration: 100, useNativeDriver: true }),
Animated.spring(buttonAnim, { toValue: 1, friction: 3, useNativeDriver: true }),
]).start();
// 旋转动画
rotateAnim.setValue(0);
Animated.timing(rotateAnim, { toValue: 1, duration: 1500, useNativeDriver: true }).start();
抽签函数包含选项解析、动画启动、随机选择三个部分。
选项解析:options.split('\n') 按换行符分割,得到选项数组。filter(s => s.trim()) 过滤掉空行和只有空格的行。trim() 去除首尾空格,如果结果是空字符串,filter 会过滤掉。
空选项检查:如果没有有效选项,直接返回,不执行抽签。这避免了空数组导致的错误。
设置动画状态:setIsAnimating(true) 标记开始抽签,禁用按钮。
按钮动画:点击按钮时,按钮缩小到 90%(100ms),再弹回到 100%。friction: 3 让弹簧有明显的回弹效果,给用户"按下去"的触感反馈。
旋转动画:rotateAnim.setValue(0) 重置旋转角度为 0,确保每次抽签都从 0 度开始旋转。Animated.timing 让旋转值从 0 到 1(1.5 秒),后面会用插值映射到 0-720 度。
快速切换动画
let count = 0;
const interval = setInterval(() => {
setResult(items[Math.floor(Math.random() * items.length)]);
// 缩放动画
Animated.sequence([
Animated.timing(scaleAnim, { toValue: 1.1, duration: 50, useNativeDriver: true }),
Animated.timing(scaleAnim, { toValue: 1, duration: 50, useNativeDriver: true }),
]).start();
count++;
if (count > 15) {
clearInterval(interval);
setIsAnimating(false);
setResult(items[Math.floor(Math.random() * items.length)]);
// 最终结果动画
Animated.spring(scaleAnim, { toValue: 1.2, friction: 3, useNativeDriver: true }).start(() => {
Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true }).start();
});
}
}, 100);
};
快速切换动画营造"抽签"的紧张感。
定时器:setInterval 每 100ms 执行一次,快速切换显示的选项。
随机选择:Math.floor(Math.random() * items.length) 生成 0 到 items.length - 1 的随机整数,作为数组索引。每次都随机选择一个选项显示。
缩放动画:每次切换选项时,结果框放大到 110%(50ms),再缩回到 100%(50ms)。总共 100ms,和定时器间隔一致。这让每次切换都有视觉反馈,用户能清楚看到选项在变化。
计数器:count 记录切换次数。切换 15 次后停止(1.5 秒),和旋转动画的时长一致。
停止动画:
clearInterval(interval)停止定时器setIsAnimating(false)解除动画状态,启用按钮- 再次随机选择一个选项作为最终结果
最终结果动画:结果框放大到 120%,再缩回到 100%。两次弹簧动画串联,第一次放大用 friction: 3(弹性强),第二次缩回用 friction: 4(弹性弱),形成"弹出 → 稳定"的效果。
为什么切换 15 次?因为 15 次 × 100ms = 1500ms,和旋转动画的时长一致。旋转和切换同时结束,视觉上更协调。如果切换次数太少(比如 5 次),抽签过程太短,用户感觉不到紧张感;如果太多(比如 30 次),抽签过程太长,用户会不耐烦。
快速模板
const presets = [
{ name: '今天吃什么', icon: '🍜', options: '火锅\n烧烤\n麻辣烫\n炒菜\n面条\n饺子\n汉堡\n披萨' },
{ name: '做还是不做', icon: '🤔', options: '做!\n不做\n再想想\n明天再说' },
{ name: '谁来干活', icon: '👷', options: '张三\n李四\n王五\n赵六' },
];
预设模板让用户快速开始,不需要手动输入选项。
模板结构:每个模板包含名称、图标、选项列表。选项列表是多行文本,和 options 状态的格式一致。
模板内容:
- “今天吃什么”:8 种常见食物,解决选择困难症
- “做还是不做”:4 种决策选项,帮助做决定
- “谁来干活”:4 个人名,随机分配任务
为什么选择这 3 个模板?因为它们覆盖了抽签工具的 3 种典型场景:选择(吃什么)、决策(做不做)、分配(谁来做)。用户可以直接使用,也可以在模板基础上修改。
旋转插值
const spin = rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '720deg'] });
插值把动画值 0-1 映射到旋转角度 0-720 度。
为什么旋转 720 度?因为 720 度是两圈,视觉效果明显,但又不会太多导致眩晕。一圈(360 度)太少,感觉不够刺激;三圈(1080 度)太多,用户会觉得晕。
为什么用字符串?因为 rotate 变换需要带单位的字符串,比如 '45deg'。如果用数字 45,React Native 会报错。
界面渲染:头部和结果框
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>
<Animated.View style={[styles.resultBox, {
transform: [{ scale: scaleAnim }, { rotate: spin }],
shadowOpacity: glowAnim.interpolate({ inputRange: [0, 1], outputRange: [0.3, 0.6] }),
}]}>
<Text style={[styles.result, isAnimating && styles.resultAnimating]}>
{result || '点击抽签'}
</Text>
</Animated.View>
头部区域包含图标、标题、副标题。🎰 老虎机图标表示"随机抽取"的概念。
结果框动画:
transform: [{ scale: scaleAnim }, { rotate: spin }]:同时应用缩放和旋转变换shadowOpacity:用插值把glowAnim的 0-1 映射到阴影透明度 0.3-0.6。阴影透明度循环变化,营造"呼吸"效果
结果文本:
result || '点击抽签':如果没有结果,显示提示文字isAnimating && styles.resultAnimating:抽签过程中应用resultAnimating样式,降低透明度到 0.7,让用户知道结果还在变化
为什么阴影透明度是 0.3-0.6?因为 0.3 是最小值,阴影可见但不明显;0.6 是最大值,阴影明显但不过分。如果最小值是 0,阴影会完全消失,视觉上不连续;如果最大值是 1,阴影太重,会遮挡内容。
抽签按钮
<Animated.View style={{ transform: [{ scale: buttonAnim }] }}>
<TouchableOpacity
style={[styles.btn, isAnimating && styles.btnDisabled]}
onPress={pick}
disabled={isAnimating}
activeOpacity={0.8}
>
<Text style={styles.btnIcon}>{isAnimating ? '🎲' : '🎯'}</Text>
<Text style={styles.btnText}>{isAnimating ? '抽签中...' : '开始抽签'}</Text>
</TouchableOpacity>
</Animated.View>
按钮用 Animated.View 包裹,应用缩放动画。
按钮状态:
disabled={isAnimating}:抽签过程中禁用按钮,防止重复点击isAnimating && styles.btnDisabled:抽签过程中应用btnDisabled样式,降低透明度到 0.7
按钮内容:
- 图标:抽签中显示 🎲(骰子),空闲时显示 🎯(靶心)
- 文字:抽签中显示"抽签中…“,空闲时显示"开始抽签”
为什么用两个图标?因为图标变化能让用户立即感知状态变化。🎲 骰子表示"正在随机",🎯 靶心表示"准备就绪"。
选项输入和模板
<View style={styles.inputCard}>
<Text style={styles.label}>📝 选项列表(每行一个)</Text>
<TextInput
style={styles.input}
value={options}
onChangeText={setOptions}
multiline
placeholderTextColor="#666"
/>
</View>
<View style={styles.presetsSection}>
<Text style={styles.label}>⚡ 快速模板</Text>
<View style={styles.presets}>
{presets.map(p => (
<TouchableOpacity
key={p.name}
style={styles.preset}
onPress={() => setOptions(p.options)}
activeOpacity={0.7}
>
<Text style={styles.presetIcon}>{p.icon}</Text>
<Text style={styles.presetText}>{p.name}</Text>
</TouchableOpacity>
))}
</View>
</View>
</ScrollView>
);
};
选项输入框:
multiline:支持多行输入- 标签提示"每行一个",告诉用户输入格式
- 最小高度 150 像素,确保有足够的空间
模板按钮:
- 遍历
presets数组,渲染 3 个模板按钮 - 点击模板按钮时,用模板的选项列表替换当前的
options - 每个按钮显示图标和名称
为什么用 flexWrap: 'wrap'?因为模板按钮的宽度不固定(取决于文字长度),用 flexWrap 让按钮自动换行。如果一行放不下,自动换到下一行。
鸿蒙 ArkTS 对比:抽签逻辑
@State options: string = '选项1\n选项2\n选项3\n选项4\n选项5'
@State result: string = '点击抽签'
@State isAnimating: boolean = false
private scaleAnim: number = 1
private rotateAnim: number = 0
pick() {
const items = this.options.split('\n').filter(s => s.trim())
if (items.length === 0) return
this.isAnimating = true
// 启动旋转动画
animateTo({ duration: 1500 }, () => {
this.rotateAnim = 720
})
let count = 0
const interval = setInterval(() => {
this.result = items[Math.floor(Math.random() * items.length)]
// 缩放动画
animateTo({ duration: 50 }, () => { this.scaleAnim = 1.1 })
setTimeout(() => {
animateTo({ duration: 50 }, () => { this.scaleAnim = 1 })
}, 50)
count++
if (count > 15) {
clearInterval(interval)
this.isAnimating = false
this.result = items[Math.floor(Math.random() * items.length)]
// 最终结果动画
animateTo({ duration: 300, curve: Curve.Spring }, () => {
this.scaleAnim = 1.2
})
setTimeout(() => {
animateTo({ duration: 300, curve: Curve.Spring }, () => {
this.scaleAnim = 1
})
}, 300)
}
}, 100)
}
ArkTS 中的抽签逻辑完全一样,核心是定时器 + 随机选择 + 动画。区别在于动画 API:React Native 用 Animated API,ArkTS 用 animateTo 函数。animateTo 更简洁,不需要创建动画值对象,直接修改状态变量就能触发动画。
样式定义:结果框和按钮
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 },
resultBox: {
backgroundColor: '#1a1a3e',
padding: 50,
borderRadius: 20,
marginBottom: 24,
alignItems: 'center',
borderWidth: 2,
borderColor: '#4A90D9',
shadowColor: '#4A90D9',
shadowOffset: { width: 0, height: 8 },
shadowRadius: 20,
},
result: { fontSize: 36, color: '#fff', fontWeight: '700' },
resultAnimating: { opacity: 0.7 },
结果框是页面的视觉焦点,用大内边距(50)、大圆角(20)、蓝色边框、蓝色阴影营造"舞台"效果。
阴影设置:
shadowColor: '#4A90D9':蓝色阴影,和边框颜色一致shadowOffset: { width: 0, height: 8 }:阴影向下偏移 8 像素,模拟"悬浮"效果shadowRadius: 20:阴影模糊半径 20,让阴影柔和扩散
结果文字:字号 36,白色粗体,醒目。抽签过程中透明度降低到 0.7,让用户知道结果还在变化。
样式定义:按钮和输入框
btn: {
backgroundColor: '#e74c3c',
padding: 18,
borderRadius: 16,
alignItems: 'center',
marginBottom: 24,
flexDirection: 'row',
justifyContent: 'center',
shadowColor: '#e74c3c',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.4,
shadowRadius: 15,
},
btnDisabled: { opacity: 0.7 },
btnIcon: { fontSize: 24, marginRight: 10 },
btnText: { color: '#fff', fontSize: 20, fontWeight: '700' },
inputCard: {
backgroundColor: '#1a1a3e',
borderRadius: 16,
padding: 16,
marginBottom: 20,
borderWidth: 1,
borderColor: '#3a3a6a',
},
label: { color: '#888', fontSize: 14, marginBottom: 12 },
input: {
backgroundColor: '#252550',
padding: 14,
borderRadius: 12,
minHeight: 150,
color: '#fff',
fontSize: 16,
},
按钮用红色背景(#e74c3c),和蓝色的结果框形成对比,是页面的第二个视觉焦点。红色阴影和红色背景一致,增强立体感。
按钮布局:
flexDirection: 'row':图标和文字水平排列justifyContent: 'center':内容居中对齐- 图标字号 24,文字字号 20,图标稍大,更醒目
输入框用深蓝色背景,最小高度 150 像素,确保能显示多行选项。
样式定义:模板按钮
presetsSection: { marginBottom: 20 },
presets: { flexDirection: 'row', flexWrap: 'wrap' },
preset: {
backgroundColor: '#1a1a3e',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 12,
marginRight: 10,
marginBottom: 10,
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#3a3a6a',
},
presetIcon: { fontSize: 18, marginRight: 8 },
presetText: { color: '#4A90D9', fontSize: 14 },
});
模板按钮用 flexWrap: 'wrap' 自动换行。每个按钮包含图标和文字,水平排列。
按钮样式:
- 背景色和输入卡片一致,保持视觉统一
- 圆角 12,比卡片的圆角 16 小,形成层次
- 右边距和下边距各 10,让按钮之间有间隙
- 文字用蓝色,和结果框的边框颜色一致
为什么模板按钮用蓝色文字?因为蓝色是页面的主题色(结果框边框、阴影都是蓝色),用蓝色文字保持视觉一致性。红色只用在主按钮上,突出主要操作。
小结
这个抽签工具展示了多重动画的协调配合。旋转、缩放、发光三个动画同时运行,营造丰富的视觉效果。定时器 + 随机选择实现快速切换,给用户紧张感。最终结果用弹簧动画强调,让用户清楚看到选中的选项。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)