|随机选择(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 1600ms1.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: 选项列表 (可滚动) │ ← layoutWeight1 └─────────────────────────────────┘5.2 视觉焦点设计页面中最醒目的设计元素是红色主色调按钮颜色#F44336Material 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 动画编程和列表操作的优秀教学案例。