ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

鸿蒙 ArkTS 实战:随机点名 RandomPick

鸿蒙 ArkTS 实战:随机点名 RandomPick

随机点名是课堂提问、会议抽签、活动抽奖等场景里最高频的小工具之一。

一、应用概述与功能

Index24 是一个「随机点名」页面,整体数据流是:维护一个名单数组 → 点名时随机选中一项 → 停止后展示结果 → 名单支持增删。页面的具体功能点包括:

  • 内置名单:names 数组预置了 12 个中文姓名——张伟、王芳、李娜、刘洋、陈静、杨磊、赵敏、黄强、周婷、吴杰、徐丽、孙浩,页面一进来就能直接点名。预置名单的好处是演示零成本,打开页面立刻可用;真实使用时,这份名单通常会来自后端接口或本地存储,届时只需替换 names 的初始化来源,其余逻辑完全不用改动。
  • 滚动点名:点击「开始点名」按钮,running 置为 true,同时启动一个 100ms 的 setInterval;每个周期内用 Math.random() 随机算出一个下标,更新 highlight 与 current。此时顶部大字以蓝色高速跳动,列表对应行以浅蓝背景加粗高亮。
  • 停止定格:点击「停止」按钮,clearInterval 停掉定时器,running 置 false,随后弹出 toast「选中:xxx」,当前名字颜色变为绿色,表示点名结束。
  • 添加姓名:底部输入框输入姓名后点「添加」,先 trim 去空格,空串提示「请输入姓名」,重复时提示「该姓名已存在」,合法则通过 concat 追加并清空输入框。
  • 删除姓名:点击任意名单行即可删除该行;若正在点名,删除前会先自动调用 stop(),避免删除后定时器仍引用无效数据。
  • 状态展示:页面右侧显示「剩余人数」实时数值,并提示「点击名单行可删除」,交互引导完整。
  • 返回导航:顶部「返回」按钮调用 router.back() 回到上一页。

这个页面把「定时器、随机数、数组增删、条件样式」全部串在一个小应用里,非常适合作为 setInterval 在 ArkTS 中标准用法的学习范本。

从布局结构上看,页面采用「固定头部 + 可滚动内容」的主框架:外层 Column 铺满全屏,顶部是返回栏,其余内容放进 Scroll,Scroll 内部用 Column({ space: 16 }) 依次排列「显示卡片、操作按钮、剩余人数行、添加行、名单列表」五个区块。显示卡片与名单列表都是白底圆角卡片,在 #f2f3f5 的浅灰底上层次分明。这样的结构让名单很长时依然可以上下滑动查看,同时顶部返回栏始终固定可见。

二、核心知识点

  1. @State 响应式状态

Index24 声明了五个 @State 变量:names(string[] 名单数组)、current(string 当前显示的名字,初始为「待点名」)、highlight(number 当前高亮的下标)、running(boolean 是否正在点名)、input(string 添加输入的文本)。它们的变化都会驱动 UI 更新:highlight 变化时列表高亮跟着移动,current 变化时顶部大字跟着切换,running 变化时按钮状态、标题文案(「点名中…」/「点名结果」)与颜色一起联动。timer 则是普通成员变量,只是定时器句柄,不参与渲染。其中 names 是引用类型,修改它时必须整体赋新数组才能被框架感知,这也是第 4 点「数组操作技巧」反复强调 concat 与 slice 的原因。另外 highlight 与 current 虽然分属两个状态,但每次回调总是成对更新,保证「高亮行」与「显示的名字」始终指向同一个人,避免出现显示与高亮错位的怪异现象。

  1. setInterval / clearInterval

这是本页的灵魂。start() 中用 setInterval(() => {…}, 100) 每 100ms 执行一次回调;stop() 中用 clearInterval(this.timer) 终止。ArkTS 中定时器回调会在时间到后进入事件循环执行,因此回调里直接读写 this 状态即可,无需额外线程同步。注意定时器必须妥善清理,否则页面销毁后回调仍会执行,造成内存泄漏甚至异常。setInterval 与 setTimeout 的差别要分清:setInterval 会按固定间隔无限次执行,直到被 clearInterval 停止;setTimeout 只执行一次。本页面要的是「持续滚动」,所以用 setInterval;而后面扩展章节提到的「先快后慢」减速效果,则需要改用 setTimeout 递归调度。理解二者的差别,是玩转定时器的第一步。

  1. 随机数生成

const idx: number = Math.floor(Math.random() * this.names.length) 生成 0 到 length - 1 之间的整数下标,与 names 长度联动。即使运行中名单被删除,随机范围也始终与数组长度一致,不会越界。从概率上说,Math.random() 均匀分布在 [0, 1) 区间,乘以长度再向下取整后,每个下标被选中的概率近似相等,不存在偏向某一个人的问题。唯一要注意的是,每次点名之间是独立事件,同一人连续被点中是完全正常的概率现象,并不代表随机算法有 bug。

  1. 数组操作技巧
  • concat 追加:this.names = this.names.concat(name) 返回新数组,符合「不可变更新」的响应式要求;
  • slice + splice 删除:先 slice() 拷贝一份,再 splice(idx, 1) 删除目标项,最后整体赋回;
  • indexOf 查重:names.indexOf(name) >= 0 判定重复;
  • trim 去空格:input.trim() 避免「 张三 」之类的脏数据。

这些操作刻意避开了原地修改:concat 生成新数组、slice 生成副本再 splice,都是为了给 @State 一个全新的引用。你可以把「数组变了页面却没更新」作为一条排查信号,遇到时优先怀疑是不是用了原地修改。

  1. 条件样式

列表行用 this.running && idx === this.highlight 同时判断「正在点名」与「是否命中当前高亮」,命中时文字变蓝加粗、行背景变成 #dbe8ff,否则保持默认的 #333333 文字与 #f7f8fa 背景。顶部大字的颜色也在 running 时显示 #1a6cff、停止后变为 #0abf53 的绿色。这种把「样式」写成「状态的函数」的写法,正是声明式 UI 的核心表达:只要状态是对的,界面就自动是对的。未来想改高亮色,只需改动三元表达式里的颜色值,不需要去 UI 里逐个找元素手动改。

  1. running 标志位

running 承担了三重职责:防止重复启动(start 里 if (this.running) return)、防止空停止(stop 里 if (!this.running) return)、作为 UI 状态控制样式与文案。它是典型的「状态机位」,把业务状态与界面状态统一起来。在异步回调和用户操作并发发生的场景下,这种标志位尤为重要:它保证了任何时刻系统只存在一个「正在点名」的事实,无论你连点多少次开始,都只会有一个定时器在运行。

  1. 生命周期清理

aboutToDisappear() 中无条件 clearInterval(this.timer),这是定时器使用的底线要求。这个生命周期方法由框架在页面销毁前自动调用,配合 start / stop 里的手动清理,构成「启动有处、停止有法、销毁有底」的三层保障。真机上如果出现页面返回后点名还在跑的问题,十有八九是这里漏写了清理逻辑。

  1. Column / Row / Blank 布局

顶层 Column 纵向排布各个区块,每个区块内部用 Row 横向排布。返回栏里 Blank() 把「返回」按钮和标题撑在左侧、右侧留白;显示卡片用 Column({ space: 8 }) 把「点名结果」小标题和 44 号大字上下排开;操作按钮行用 Row({ space: 16 }) 让「开始点名」与「停止」等宽分布。space 参数统一控制子元素间距,比手动加 margin 更省心。

  1. 链式样式与主题色

页面用 .layoutWeight(1) 让按钮等宽、.fontSize(44) 放大当前名字、.fontColor(‘#1a6cff’) 与 .fontColor(‘#0abf53’) 区分点名中与定格、.backgroundColor(‘#dbe8ff’) 高亮命中行。统一的蓝色主题(开始、高亮、剩余人数)配橙色停止按钮(#ff8f1f)与红色删除标记(#ff4d4f),功能分区一目了然,用户不用读文字也能猜到每个元素的作用。颜色值也可以进一步抽成主题常量统一管理,方便整体换肤时一键修改。

  1. ForEach 与 key 生成器

名单列表用 ForEach(this.names, (item, idx) => {…}, (item, idx) => idx.toString()) 渲染,key 使用下标字符串。因为删除操作会改变数组下标,用下标做 key 在增删频繁时会有一定的重建成本,但对点名这种小型列表完全够用;如果名单规模很大,可以考虑给每个名字分配一个独立 id 做 key。

  1. promptAction 轻提示

与示例 23 一样,本页用 promptAction.showToast 承担全部用户反馈:点名前的空名单提示、停止后的「选中:xxx」、添加时的「请输入姓名」与「该姓名已存在」、删除后的「已删除:xxx」。这些 toast 让每次操作都有即时反馈,交互体验完整闭环。

三、源码逐段解析

3.1 点名核心:start / stop

点名最核心的两个方法分别是 start() 与 stop():

private start(): void { if (this.running) return; if (this.names.length === 0) { promptAction.showToast({ message: '名单为空,请先添加' }); return; } this.running = true; this.timer = setInterval(() => { const idx: number = Math.floor(Math.random() * this.names.length); this.highlight = idx; this.current = this.names[idx]; }, 100); } private stop(): void { if (!this.running) return; this.running = false; clearInterval(this.timer); this.timer = -1; promptAction.showToast({ message: '选中:' + this.current }); }

start() 的三个步骤:幂等保护(已运行直接返回)→ 空名单兜底(toast 提示)→ 置 running 并启动定时器。回调中每次随机取下标、更新高亮与当前名字。stop() 先清 running,再 clearInterval 并把句柄复位为 -1,最后 toast 定格结果。先置 false 再清定时器,可以避免回调在最后时刻又写回一次状态造成视觉闪烁。

把定时器的执行过程拆开看:start() 启动后,每 100ms 事件循环就会执行一次箭头函数。假设当前 names 有 12 人,某次回调里 Math.random() 返回 0.37,Math.floor(0.37 * 12) = 4,于是 highlight = 4、current = names[4]。下一次回调可能又算出另一个下标,如此反复,就形成了肉眼可见的「滚动」。由于每次都是独立随机,结果不会出现规律性循环,看起来非常自然。

3.2 添加与删除名单

名单管理由 addName() 与 removeName() 负责:

private addName(): void { const name: string = this.input.trim(); if (name.length === 0) { promptAction.showToast({ message: '请输入姓名' }); return; } if (this.names.indexOf(name) >= 0) { promptAction.showToast({ message: '该姓名已存在' }); return; } this.names = this.names.concat(name); this.input = ''; } private removeName(idx: number): void { if (this.running) this.stop(); const list: string[] = this.names.slice(); list.splice(idx, 1); this.names = list; this.current = '待点名'; }

addName 依次做「去空格、判空、查重、追加、清输入」。removeName 的细节:若正在点名先 stop(),把定时器安全停掉;然后 slice() 拷贝原数组,splice 删除指定下标,赋回新数组,保证 @State 收到新引用从而触发刷新;最后把 current 复位为「待点名」,状态回到初始。(原代码在删除后还会 toast「已删除:xxx」,此处略去。)

举一个具体操作:输入「陈晨」点添加,先 trim 得到「陈晨」,长度非 0、indexOf 返回 -1 说明不重复,于是 concat 追加到末尾,input 清空。再点击第 3 行删除:如果正在点名会先 stop(),随后 slice 拷贝出 11 项的新数组,splice(2, 1) 去掉下标 2 的名字,赋回 names,列表立刻少一行,剩余人数同步减一。

3.3 高亮行渲染

列表项的高亮样式由条件三元表达式驱动:

Text(item) .fontColor(this.running && idx === this.highlight ? '#1a6cff' : '#333333') .fontWeight(this.running && idx === this.highlight ? FontWeight.Bold : FontWeight.Normal) .backgroundColor(this.running && idx === this.highlight ? '#dbe8ff' : '#f7f8fa') .onClick(() => { this.removeName(idx); })

列表行由序号、姓名、右侧红色 ✕ 组成。高亮条件组合了 running 与下标比对,既保证只有「点名中」才有滚动效果,又精确到行。整行 onClick 绑定 removeName,点击即删,交互非常直接。另外注意列表行右侧的红色 ✕ 只是装饰性标识,真正的删除动作绑定在整行 Row 的 onClick 上,也就是说点击名字本身也能删除,容错区域更大,符合移动端的触控习惯。key 生成函数沿用 idx.toString(),配合 slice + splice 的不可变更新,删除任意一行后列表都能正确重绘。

3.4 返回栏与生命周期

顶部返回栏结构与示例 23 完全一致:Row 里放「返回」按钮与「随机点名」标题,点击后 router.back()。页面底部的 aboutToDisappear() 在页面销毁前清理定时器,防止页面消失后回调继续执行导致异常。从整体看,build() 里的节点结构与数据流是对应的:显示卡片绑定 current 与 running,操作按钮行绑定 start / stop,添加行绑定 input 与 addName,名单列表绑定 names 与 removeName。每个交互点都有明确的数据来源和动作入口,页面虽小,结构却非常规整,适合作为理解 ArkTS 页面组织的样板。

四、关键实现细节分析

4.1 为什么用 100ms 的间隔

100ms 是人眼能感知「滚动」又不至于因闪烁过快而疲劳的折中值,换算过来就是每秒切换 10 次。间隔调大(如 200ms)节奏更舒缓,调小(如 50ms)更紧张刺激,用户感知差异非常明显,可以按场景自由微调。这个参数就是 setInterval 的第二个参数,改动成本极低,非常适合拿来体验「定时频率对交互手感的影响」。从实现角度,100ms 也意味着每秒最多 10 次状态刷新,对 ArkTS 的渲染管线来说负担很小,即便同时更新 highlight 与 current 两个状态也不会卡顿,这个频率同时为后续的减速动画留出了调整余地。

4.2 随机下标的联动安全性

随机范围始终是 this.names.length,所以即使定时器运行中名单被删除或增加,下一次回调仍在最新长度内取下标,不会越界。这是「随机值必须依赖当前真实数据规模」的直接体现,也是把随机范围硬编码成某个固定值最容易踩的坑——名单一变化,程序就可能崩溃。换个角度,这也是为什么「随机下标」要放在定时器回调里实时计算,而不是在 start() 时算好一次:前者永远拿到最新长度,后者会随着名单增删慢慢失真。

4.3 running 标志的三种角色

running 是理解整个页面交互的钥匙,它同时扮演着三个角色:

  • 业务幂等:start / stop 互斥,避免重复启动或重复停止导致定时器句柄错乱;
  • 定时器生命周期:只有 running 为 true 时才持有有效句柄,stop 后把句柄复位为 -1;
  • UI 状态:标题文案、文字颜色、列表高亮全部依赖它做分支。

4.4 数组的不可变更新

concat 与 slice + splice 都返回新数组,这正是 @State 正确刷新的前提——若直接 push 或原地 splice 原数组,ArkTS 无法感知变化,UI 不会更新。这是声明式 UI 与命令式编程之间最需要转换的思维习惯:不要「改数据」,而要「换数据」。习惯这个思维之后,很多响应式 bug 都能提前预防,比如删行后高亮错位、添加后列表不刷新,本质都是没有换新数组;源码里 addName 与 removeName 都严格遵循了这个约定。

4.5 删除与点名的冲突处理

removeName 先 stop() 再删除,避免「正在点的名字被删掉」导致 current 指向已不存在的项。删除后 current 复位「待点名」,界面状态重新回到一致。这个「先停再改」的顺序很重要,如果反过来,删除那一刻定时器还可能随机到越界下标。

4.6 从状态机视角看整个页面

页面状态可以抽象成「待点名 → 点名中 → 定格」三个阶段:初始时 current 为「待点名」;start() 后进入「点名中」,标题变「点名中…」、文字变蓝、列表高亮滚动;stop() 后定格,toast 告知选中者、文字变绿。删除名单会让 current 回到「待点名」,三个状态之间的转移都由 running 与其他 @State 共同决定,闭环清晰,也方便后续扩展新状态。

4.7 current 复位的设计

添加与删除之后,current 与界面状态要保持同步:删除时把它复位为「待点名」,添加时则不改动它。为什么添加不复位?因为添加只是名单扩容,当前展示不受影响;而删除会改变数组下标,如果被删的恰好是当前高亮项,继续显示旧名字就会产生「名单里没有这个人却显示他的名字」的矛盾,所以删除必须复位。这一行 current = ‘待点名’ 看似不起眼,却是保证界面状态一致性的关键。

4.8 大字号展示与视觉层级

当前名字用 .fontSize(44) 放大显示,是页面的视觉焦点;运行中为蓝色、定格后为绿色,用户不用读文字就能从颜色判断状态。配合「点名中… / 点名结果」的小标题与剩余人数行,整个页面形成了「状态标题 → 大字结果 → 操作按钮 → 名单列表」的清晰视觉层级。小屏幕上也保持了这个比例,44 号字在多数手机上都不会溢出卡片。

五、运行效果与操作指南

  1. 进入页面,看到顶部「随机点名」标题,中间白色卡片显示「点名结果」与 44 号大字「待点名」。
  2. 卡片下方是「开始点名」「停止」两个按钮;再往下是剩余人数行,右侧提示「点击名单行可删除」。
  3. 底部是 12 行姓名列表,每行右侧有一个红色 ✕ 标记。
  4. 点击「开始点名」,标题变为「点名中…」,大字开始蓝色高速跳动,列表行随机高亮滚动。
  5. 点击「停止」,滚动定格,弹出 toast「选中:xxx」,大字变绿显示最终结果。
  6. 在输入框输入姓名点「添加」,新名字追加到列表末尾,剩余人数 +1。
  7. 输入已存在的姓名,弹出「该姓名已存在」;输入纯空格,弹出「请输入姓名」。
  8. 点击某行姓名,该行被删除;点名中删除会先自动停止。
  9. 把名单删空后再点「开始点名」,弹出「名单为空,请先添加」。

操作时可以刻意测试几个边界:刚进入页面不点开始,直接点「停止」,因为 running 为 false,stop() 会直接返回,不会误报「选中」;把列表删到只剩 1 个人再点名,滚动看起来几乎停住,因为随机范围只有一个选项;连续快速点「开始点名」多次,由于幂等保护,不会叠加出多个定时器。这些小细节正是 running 标志位和 if 判断存在的意义。

如果屏幕较小、名单又长,页面主体可以上下滚动;名单列表的高度被固定为 240,容纳初始 12 个名字绰绰有余,但新增到超过 12 人后,超出部分不会自动出现滚动条。扩展时可以把高度改为按内容自适应,或改用 List 自带的滚动能力,让长名单也能完整浏览。

六、可扩展方向

这个页面已经具备完整的基本流程,在此基础上可以轻松做出很多有意思的变体:

  1. 不放回抽取:把抽中的名字从名单移除或标记为「已抽」,保证一轮点名内不重复,适合「点完一个少一个」的场景。
  2. 减速动画:模仿抽奖机「先快后慢」,用 setTimeout 递归调用并逐渐拉长间隔,最后停在选中项上,观感更专业,也更适合年会抽奖。
  3. 音效与震动:配合音频组件播放开始与定格音效、用 Vibration 在定格瞬间震动,增强现场氛围。
  4. 名单持久化:用 Preferences 保存名单,下次打开自动恢复,避免每次手动录入。
  5. 权重点名:给每个名字配权重,按加权随机算法抽取,适合「有偏向」的需求,比如让被点名次数少的同学更容易被抽中。
  6. 分组模式:一次抽取多个人组成小组,配合卡片展开动画展示结果,适合课堂分组活动。
  7. 数据统计:记录每次点名历史,统计被点频率,形成「被点红人榜」,让课堂互动更有趣味。

如果要做成正式产品,还可以考虑:点名结果一键复制或分享到班级群;按周、按月维度生成点名报表;给姓名列表提供批量导入导出;在停止瞬间增加一个放大或翻转动画强调选中者。每个方向都可以复用本页现有的状态机骨架,把新的 UI 元素挂到 running 与 current 这两个核心状态上即可,改造成本并不高。

七、常见问题与调试技巧

这一节汇总了阅读源码或真机调试时最可能遇到的几个问题,多数都可以在动手前就预防,真正遇到时也知道该往哪个方向排查。

  1. 定时器停不下来:检查 stop() 是否因为 if (!this.running) return 提前返回而跳过了 clearInterval;也要检查是否在别处又启动了一个新的定时器覆盖了句柄。
  2. 页面返回后还在滚动:确认 aboutToDisappear 里做了 clearInterval,否则页面销毁后回调继续执行,可能去操作已经销毁的组件而报错。
  3. 高亮不动:确认回调里更新的是 this.highlight 而不是某个局部变量;@State 变量必须通过 this. 访问才会触发刷新。
  4. 删除后名单不刷新:@State 数组务必用新引用更新(concat / slice),原地 splice 不会触发渲染,这是声明式 UI 最常见的翻车点。
  5. 添加重复名字:indexOf 是全等比较,注意输入的前后空格要先 trim 再比较,否则「张三」和「 张三」会被当成两个名字。
  6. 随机是否公平:Math.random 是伪随机,对点名场景足够;若要更公平,可先把名单洗牌再按顺序取用。
  7. 调试技巧:在 DevEco Studio 里对 start() 和 stop() 打上断点,观察 timer 句柄与 running 的变化,可以快速理清定时器生命周期;高亮样式可以先在 Previewer 里验证,不必每次都用真机。
  8. 回调节引用陈旧:如果确实遇到「删除后定时器还在跑」的诡异现象,多半是 removeName 里漏了 stop(),或者回调误用了局部拷贝的数组去更新。把回调里对 this.names 的访问统一改成最新引用,就能避免读到过期数据。
  9. 参数手感调优:把 100ms 抽成常量并在文件顶部注释说明,方便日后统一调整;测试时可以用 500ms 慢速观察高亮是否精确跟随随机下标,确认逻辑正确后再改回正式节奏。

八、总结

Index24 用约两百行代码实现了一个功能完整、交互流畅的随机点名工具。它把 ArkTS 的几个关键技术点串联得非常自然:setInterval 定时器负责「滚动」、Math.random 负责「随机」、@State 与不可变数组更新负责「渲染」、running 标志位负责「状态机」、aboutToDisappear 负责「安全清理」。同时,添加与删除名单的边界处理(判空、查重、删除前停止)也展示了产品化的细节思考。如果把 stop() 里的 toast 替换成带按钮的对话框、再给定格瞬间加一个音效,这个页面就能直接变成年会抽奖的雏形。

相比示例 23 的「输入 → 计算 → 分页展示」,这个页面进一步引入了「定时驱动 + 随机 + 列表增删」的实时交互模型,两者结合起来,基本覆盖了 ArkTS 页面开发中高频使用的核心能力。无论是学习定时器的标准用法,还是想要一个可以改造成抽奖大屏的起点,这份源码都值得仔细研读,动手改一改参数,你会更快理解背后的运行机制。

最后给读者留一个小练习:试着把「停止后自动弹出一个确认对话框」加入流程,观察在 running 与定时器配合下对话框的弹出时机如何把握;或者把名单换成英文昵称或数字编号,验证随机与高亮逻辑是否与语言无关。通过这样的改动,你会真正掌握声明式 UI 与定时器协同工作的组合拳。

返回列表