# 鸿蒙 HarmonyOS 应用开发实战(第24期)|随机选择(Random Picker)— List 列表与 setInterval 动画精讲
一、应用概述
随机选择(Random Picker) 是一款解决"选择困难症"的趣味工具应用,帮助用户在多个选项中随机做出决定。应用内置了12个预设选项(涵盖饮食、娱乐、学习等生活场景),用户也可以自由添加和删除自定义选项。点击"帮我选"按钮后,选项会以快速轮播动画的方式跳动,最终定格在选中的选项上。应用基于 HarmonyOS 的 ArkTS 语言和声明式 UI 框架构建,完美展示了 List 列表组件、setInterval 定时器动画、动态数组操作、以及条件渲染等核心技术的综合运用。
对于鸿蒙开发者而言,随机选择是一个典型的交互动画应用案例——它让你在约98行代码中,同时掌握列表渲染、定时器动画、数组增删、状态锁定四大核心能力,这些能力广泛适用于抽奖、投票、问卷、游戏等场景。
1.1 功能特性
- 一键随机选择:点击按钮以轮播动画方式快速选择
- 预设12个选项:覆盖日常常见的选择难题
- 自定义添加选项:通过输入框自由增加新选项
- 删除选项:每个选项旁提供删除按钮
- 轮播动画效果:选项快速跳动20次后定格,模拟抽奖效果
- 选项计数显示:实时显示当前选项总数
- 空列表保护:选项为空时给出友好提示
1.2 适用场景
- 中午吃什么的选择困难
- 周末活动安排决策
- 团队活动抽奖环节
- 教学中的随机点名
- 日常生活中的小决策辅助
二、技术架构
2.1 整体架构概览
┌─────────────────────────────────────────┐
│ 数据层 │
│ ITEMS (预设12个默认选项) │
│ currentItems (动态选项列表) │
├─────────────────────────────────────────┤
│ 状态管理层 │
│ @State currentItems / picked / │
│ spinning / newItem │
├─────────────────────────────────────────┤
│ 业务逻辑层 │
│ pick() —— 随机选择 + 轮播动画 │
│ addItem() —— 添加新选项 │
│ removeItem() —— 删除指定选项 │
├─────────────────────────────────────────┤
│ UI 表示层 │
│ 结果显示区 / 选择按钮 / 添加栏 / │
│ 选项列表 (List + ForEach) │
└─────────────────────────────────────────┘
2.2 状态与动画数据流
用户点击"帮我选"
↓
spinning = true (锁定按钮)
↓
setInterval(80ms) 启动
↓
每80ms随机选取一个选项 → UI 更新 → 视觉跳动
↓
count = 20 (循环20次)
↓
clearInterval → spinning = false (解锁)
↓
最终选项定格显示
三、核心代码深度分析
3.1 完整的页面源码
import { router } from '@kit.ArkUI';
const ITEMS: string[] = [
'今天吃什么', '去哪里旅游', '看什么电影', '学什么编程语言',
'读哪本书', '买什么颜色', '周末做什么', '用什么手机',
'喝什么饮料', '做什么运动', '养什么宠物', '听什么音乐',
];
@Entry
@Component
struct index24 {
@State currentItems: string[] = [...ITEMS];
@State picked: string = '';
@State spinning: boolean = false;
@State newItem: string = '';
intervalId: number = -1;
pick() {
if (this.spinning) return;
if (this.currentItems.length === 0) {
this.picked = '列表是空的!';
return;
}
this.spinning = true;
let count = 0;
this.intervalId = setInterval(() => {
const idx = Math.floor(Math.random() * this.currentItems.length);
this.picked = this.currentItems[idx];
count++;
if (count >= 20) {
clearInterval(this.intervalId);
this.spinning = false;
}
}, 80);
}
addItem() {
const t = this.newItem.trim();
if (t === '') return;
this.currentItems = [...this.currentItems, t];
this.newItem = '';
}
removeItem(idx: number) {
this.currentItems = this.currentItems.filter((_, i) => i !== idx);
}
build() {
Column() {
Row() {
Text('‹ 返回').fontSize(16).fontColor('#0077FF').onClick(() => router.back())
Text('🎲 随机选择').fontSize(18).fontWeight(FontWeight.Bold)
Blank()
}.width('92%').padding({ top: 16, bottom: 8 })
Column() {
// 结果展示区
Column() {
Text(this.picked || '点击按钮帮你做决定!')
.fontSize(24).fontWeight(FontWeight.Bold)
.fontColor(this.picked ? '#F44336' : '#888')
.padding(12).textAlign(TextAlign.Center)
}
.width('92%').height(80).backgroundColor('#fff').borderRadius(12)
.justifyContent(FlexAlign.Center).margin({ bottom: 8 })
// 选择按钮
Button('🎯 帮我选!').width(200).height(52).borderRadius(26)
.backgroundColor('#F44336').fontSize(20).fontWeight(FontWeight.Bold)
.onClick(() => this.pick())
// 添加选项
Row({ space: 8 }) {
TextInput({ placeholder: '添加选项...', text: this.newItem })
.layoutWeight(1).height(40).backgroundColor('#fff').borderRadius(8)
.padding({ left: 12 }).onChange(v => this.newItem = v)
.onSubmit(() => this.addItem())
Button('+').width(40).height(40).borderRadius(20).backgroundColor('#0077FF')
.fontSize(20).fontColor('#fff').onClick(() => this.addItem())
}.width('92%').margin({ top: 12, bottom: 8 })
// 选项列表
Text(`当前选项 (${this.currentItems.length})`).fontSize(14).fontColor('#888').width('92%')
List() {
ForEach(this.currentItems, (item: string, idx: number) => {
ListItem() {
Row() {
Text(`${idx + 1}. ${item}`).fontSize(15).layoutWeight(1)
Text('✕').fontSize(16).fontColor('#F44336')
.onClick(() => this.removeItem(idx))
}.padding(10).backgroundColor('#fff').borderRadius(8).margin({ bottom: 4 })
}
})
}
.layoutWeight(1).width('92%')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 12 })
}
.width('100%').height('100%').backgroundColor('#f8f8f8')
}
}
3.2 轮播动画的核心实现
随机选择的动画效果是应用中最具技术含量的部分:
pick() {
if (this.spinning) return; // 防止重复触发
if (this.currentItems.length === 0) {
this.picked = '列表是空的!';
return;
}
this.spinning = true;
let count = 0;
this.intervalId = setInterval(() => {
const idx = Math.floor(Math.random() * this.currentItems.length);
this.picked = this.currentItems[idx];
count++;
if (count >= 20) {
clearInterval(this.intervalId);
this.spinning = false;
}
}, 80);
}
动画机制详解:
第一步:状态锁定
if (this.spinning) return;
使用 spinning 标志位防止用户在动画进行中重复点击按钮。这是一种防重入(Reentrancy Guard)机制,确保动画过程的完整性。
第二步:空列表检查
if (this.currentItems.length === 0) {
this.picked = '列表是空的!';
return;
}
当选项列表为空时,显示友好提示而非执行无效的随机选择。
第三步:启动定时器
this.intervalId = setInterval(() => { ... }, 80);
setInterval 每80ms触发一次回调,每次回调更新 picked 为随机选项。80ms的间隔约等于12.5fps的刷新率,在人眼感知中形成了流畅的"跳动"效果。
第四步:动画终止
count++;
if (count >= 20) {
clearInterval(this.intervalId);
this.spinning = false;
}
20次循环 × 80ms = 1600ms(1.6秒)的总动画时长。这个时长经过了精心的设计——太短则用户感受不到"随机"过程,太长则显得拖沓。1.6秒恰好是让用户感到"有在认真随机"的合理时长。
3.3 列表的动态增删操作
添加选项:
addItem() {
const t = this.newItem.trim();
if (t === '') return; // 忽略空输入
this.currentItems = [...this.currentItems, t]; // 创建新数组
this.newItem = ''; // 清空输入框
}
这里的关键是 [...this.currentItems, t]——展开运算符(Spread Operator)创建了一个全新的数组,而非修改原数组。在 ArkTS 中,@State 只能检测到引用类型变量本身的赋值操作,对于数组的内部修改(如 push、splice)无法触发 UI 更新。因此必须通过创建新数组的方式来更新状态。
删除选项:
removeItem(idx: number) {
this.currentItems = this.currentItems.filter((_, i) => i !== idx);
}
Array.filter() 方法同样返回一个新数组,只保留索引不等于 idx 的所有元素。这种函数式编程风格既简洁又安全,避免了直接操作原数组可能带来的副作用。
3.4 List 组件的高效列表渲染
List() {
ForEach(this.currentItems, (item: string, idx: number) => {
ListItem() {
Row() {
Text(`${idx + 1}. ${item}`).fontSize(15).layoutWeight(1)
Text('✕').fontSize(16).fontColor('#F44336')
.onClick(() => this.removeItem(idx))
}
.padding(10).backgroundColor('#fff').borderRadius(8).margin({ bottom: 4 })
}
})
}
.layoutWeight(1).width('92%')
List 是 HarmonyOS 中用于渲染长列表的高性能组件,其核心优势包括:
- 懒加载:只渲染当前可见区域的列表项,而非全部渲染
- 滚动优化:内置滚动惯性效果和性能优化
- 复用机制:离屏的列表项可以被回收复用
ForEach 循环遍历 currentItems 数组,为每个元素创建一个 ListItem。每个列表项包含:
- 序号 + 文本:使用
${idx + 1}. ${item}格式化显示 - 删除按钮:红色 ✕ 符号,点击时调用
removeItem(idx)
layoutWeight(1) 让文本占据删除按钮之外的所有剩余空间,确保长文本不会被截断。
四、HarmonyOS 特性深度解析
4.1 setInterval 定时器与资源管理
setInterval 是 HarmonyOS 中创建周期性定时器的标准 API,原型与 Web API 保持一致:
setInterval(callback: Function, delay: number): number
返回值为定时器的唯一标识符(整数),可用于后续的 clearInterval 调用。
在本应用中,定时器的使用遵循了以下最佳实践:
- 保存句柄:将返回值存储在
intervalId中 - 计数终止:通过计数器
count在指定次数后停止定时器 - 状态同步:停止定时器后同步更新
spinning标志位
注意事项:虽然本应用中没有在页面销毁时清理定时器(因为动画完成后定时器自动停止),但在需要持续运行的场景中,必须在 aboutToDisappear 生命周期回调中调用 clearInterval,防止内存泄漏。
4.2 @State 与数组的不可变性
@State currentItems: string[] = [...ITEMS];
@State 装饰的数组变量,其赋值操作(=)能够触发 UI 更新,但变异方法(push、pop、splice 等)不会触发更新。这是 ArkTS 响应式系统的一个重要约束:
// ❌ 不会触发 UI 更新
this.currentItems.push(newItem);
// ✅ 会触发 UI 更新
this.currentItems = [...this.currentItems, newItem];
这个约束源于 JavaScript 引擎无法高效追踪数组内部变化的设计选择。因此,在 ArkTS 开发中,应始终使用不可变更新(Immutability Update)模式来操作数组和对象。
4.3 TextInput 的 onSubmit 事件
TextInput({ placeholder: '添加选项...', text: this.newItem })
.onChange(v => this.newItem = v)
.onSubmit(() => this.addItem())
onSubmit 事件在用户按下键盘的"确认/回车"键时触发。本应用巧妙地将 onSubmit 与添加逻辑绑定,让用户既可以通过点击"+"按钮添加,也可以通过键盘回车添加——两种操作方式覆盖了不同用户的操作习惯。
4.4 条件渲染的状态驱动
结果区域的文字颜色根据状态动态变化:
Text(this.picked || '点击按钮帮你做决定!')
.fontColor(this.picked ? '#F44336' : '#888')
当 picked 为空字符串(未选择)时,显示灰色的提示文字;当 picked 有值(已选择或正在选择)时,显示红色的结果文字。这种状态驱动的样式切换让 UI 与数据状态保持同步。
五、UI/UX 设计模式
5.1 页面布局结构
┌─────────────────────────────────┐
│ Row: 导航栏 │
├─────────────────────────────────┤
│ Column: 结果展示区 (80px) │ ← 显示选中结果或提示
│ Button: "🎯 帮我选!" │ ← 核心操作按钮
│ Row: 添加输入栏 │ ← 扩展功能
│ Text: 选项计数 │
│ List: 选项列表 (可滚动) │ ← layoutWeight=1
└─────────────────────────────────┘
5.2 视觉焦点设计
页面中最醒目的设计元素是红色主色调:
- 按钮颜色:
#F44336(Material Design Red) - 结果文字颜色:
#F44336 - 删除按钮颜色:
#F44336
红色在心理学上代表"注意"和"行动",恰好契合"帮我做决定"的核心功能。搭配白色卡片和灰色背景,形成了清晰的视觉层次。
5.3 交互反馈设计
| 操作 | 视觉反馈 | 状态变化 |
|---|---|---|
| 点击"帮我选" | 结果区域开始快速切换文字 | spinning = true |
| 动画进行中 | 按钮点击无效(防重入) | spinning = true |
| 动画结束 | 结果文字定格,红色高亮 | spinning = false |
| 添加选项 | 选项列表新增一项,计数更新 | currentItems 更新 |
| 删除选项 | 选项列表移除一项,计数更新 | currentItems 更新 |
六、最佳实践与经验总结
6.1 动画流畅度的参数调优
轮播动画的效果取决于两个关键参数:
| 参数 | 值 | 效果 |
|---|---|---|
| 间隔时间 | 80ms | 约12.5fps刷新率,兼顾流畅度和性能 |
| 循环次数 | 20次 | 总时长1.6秒,适中 |
调优建议:对于更真实的"抽奖"效果,可以设计减速动画——开始时切换快,逐渐变慢:
// 减速动画示例
pick() {
if (this.spinning) return;
this.spinning = true;
let count = 0;
const totalSteps = 30;
const animate = () => {
const idx = Math.floor(Math.random() * this.currentItems.length);
this.picked = this.currentItems[idx];
count++;
if (count < totalSteps) {
// 逐渐增加间隔时间(从50ms逐渐增加到300ms)
const delay = 50 + (count / totalSteps) * 250;
setTimeout(animate, delay);
} else {
this.spinning = false;
}
};
animate();
}
6.2 数组展开运算符的正确使用
// 初始化时复制数组
@State currentItems: string[] = [...ITEMS];
使用 [...ITEMS] 而非直接赋值 = ITEMS 的原因:
- 避免引用共享:直接赋值会使
currentItems和ITEMS指向同一个数组对象 - 保持预设纯净:用户对
currentItems的增删操作不会影响预设数据ITEMS - 重置便利:如果需要"重置到默认",只需再次
= [...ITEMS]
6.3 输入框的即时清空
addItem() {
const t = this.newItem.trim();
if (t === '') return;
this.currentItems = [...this.currentItems, t];
this.newItem = ''; // 添加后立即清空输入框
}
添加选项后立即清空输入框,这是表单设计中的标准做法——用户可以立即开始输入下一个选项,无需手动删除已输入的内容。trim() 方法去除首尾空格,避免添加纯空格的无效选项。
6.4 索引删除的安全实践
removeItem(idx: number) {
this.currentItems = this.currentItems.filter((_, i) => i !== idx);
}
使用 filter 而非 splice 的好处:
- 不可变性:返回新数组而非修改原数组
- 安全性:不会因为索引越界而出错
- 声明式:更清晰地表达"排除指定索引"的意图
七、总结
随机选择应用在约98行代码中,完美展示了 ArkTS 声明式 UI 开发的多个核心概念:
- setInterval 定时器动画:80ms间隔的快速轮播效果
- List + ForEach 列表渲染:高性能的可滚动选项列表
- 数组不可变更新:展开运算符和 filter 的安全操作
- 防重入机制:动画过程中的按钮锁定
- onSubmit 键盘事件:输入框的回车提交功能
这个应用以"解决选择困难"这一趣味需求为切入点,展示了如何在 ArkTS 中实现流畅的定时器动画和动态列表管理。它不仅是日常生活的好帮手,更是学习 ArkTS 动画编程和列表操作的优秀教学案例。
更多推荐




所有评论(0)