鸿蒙HarmonyOSArkTS 示例应用系列持续更新本篇聚焦示例 25「打字测速 TypingSpeed」1. 应用概述与功能TypingSpeed 是一个典型的单页面应用通过 Entry 装饰器声明为可独立加载的页面页面从顶到底由四块内容组成。第一块是顶部返回栏一个「返回」按钮加上居中的「打字测速」标题右侧用 Blank() 占位把标题顶在视觉中间点击返回调用 router.back() 回到上一页这是示例系列统一采用的页面导航样式。第二块是正文区域外层套 Scroll 滚动容器内部是一个竖向间距 16 的 Column依次放着逐字着色的原文卡片、输入框、开始/结束按钮、实时统计卡片。第三块是结果卡片它不常驻显示只有当 resultText 非空时才通过 if 条件渲染出现在统计卡片下方浅蓝底、圆角、居中排版。整个页面的根容器设置了 #f2f3f5 的浅灰背景与白色卡片形成柔和对比观感清爽。从用户视角来看核心流程可以概括为五步点击「开始」输入框清空、计时开始对照下方原文逐字输入输入过程中原文逐字变色统计区的「用时、已输入、正确」实时跳动觉得打完就点击「结束」页面弹出测试结果卡片展示用时、速度与正确率并伴随一个「测试结束」的 toast 提示。整个交互是「按钮驱动」的输入本身不会开始计时开始按钮是唯一的计时开关这保证了计时口径的一致性。从工程视角看源码里有几个值得学习的实现选择。第一计时以「开始时间戳与当前时间的差值」为准而不是简单地每秒累加一从根本上避免了定时器漂移带来的误差累积。第二正确字符数统计被抽成独立的 computeCorrect() 方法输入联动与结束统计两处复用同一份逻辑做到了单一职责。第三页面在 aboutToDisappear 生命周期中兜底清理定时器防止用户中途返回上一页后定时器仍在后台运行。第四所有按钮都带颜色区分返回与开始是品牌蓝 #1a6cff结束是橙色 #ff8f1f「正确」计数与绿色 #0abf53 呼应结果卡片与按钮同为蓝色系。这些细节共同构成了一个完成度很高的小示例。2. 核心知识点本节先集中列出这一页涉及的知识点第 3、4 节会逐一对照代码展开。第一个知识点是 State 响应式状态。text、input、running、elapsed、correct、resultText 六个变量全部由 State 装饰。ArkUI 的响应式机制保证任何 State 变量被赋值后UI 中依赖它的表达式都会自动重新求值并刷新。例如统计区写的是 Text(this.elapsed ’ 秒’)只要 elapsed 变化这个文本就会自动更新ForEach 里的 charColor(idx) 同理input 一变每个字符的颜色表达式都会被重新计算。开发者不需要写任何「刷新」代码这正是声明式 UI 相比命令式 UI 的核心优势。另外要注意 State 是组件内状态跨页面共享需要另用 Prop、Link、AppStorage 等机制本页没有用到。第二个知识点是 setInterval 与 clearInterval。start() 里创建定时器stop() 里销毁定时器aboutToDisappear() 里兜底再清一次三段代码形成「创建—销毁—兜底销毁」的完整闭环。定时器回调采用箭头函数写法保证回调内的 this 仍指向组件实例这样访问 this.elapsed、this.startTime 才不会出错这也是初学者最容易踩的坑之一。第三个知识点是逐字符比对。computeCorrect() 与 charColor() 都依赖 charAt(index) 与 Math.min() 的组合前者负责统计正确数后者负责视觉着色两者是同一算法在数据与 UI 两个侧面的体现。这种「算法与展示分离」的思想值得借鉴——逻辑只写一遍UI 与统计各取所需。第四个知识点是 Flex 换行与 ForEach 逐字渲染。原文通过 this.text.split(‘’) 拆成字符数组ForEach 为每个字符生成一个 Text 组件key 取索引的字符串形式保证 Diff 更新时元素可被稳定识别外层 Flex({ wrap: FlexWrap.Wrap }) 让长句子在屏幕宽度不足时自动换行避免被截断。这是「把一个字符串拆成 N 个可独立着色的子组件」的典型做法也是打字测速类应用视觉反馈的基础。第五个知识点是条件渲染。if (this.resultText.length 0) 是 ArkUI 的条件渲染语法条件成立时子组件才会被构建与挂载。这里用「结果文本是否为空」作为条件比额外维护一个布尔变量更简洁语义也更直接。第六个知识点是生命周期与资源清理。aboutToDisappear() 在组件销毁前回调是释放定时器、监听器等资源的标准时机。此外整页仅 import 了 kit.ArkUI 中的 router 与 promptAction 两个模块用 promptAction.showToast 做轻量提示依赖面很小保持了示例的纯粹性。除了上面六个知识点还有两个基础但重要的机制贯穿全页。第一个是 TextInput 受控输入源码通过 TextInput({ text: this.input, placeholder: ‘请输入上面的句子’ }) 把输入框的展示文本与 State 的 input 绑定配合 onChange 回调在用户输入时更新状态这是 ArkUI 中典型的「状态驱动输入」用法——输入框内容完全由 input 决定input 的任何变化都会回显到输入框不会出现组件内部状态与页面状态不同步的问题。第二个是 Button 的链式样式配置示例给每个按钮分别设置 backgroundColor、fontColor并用 layoutWeight(1) 让按钮等宽分布说明通过属性链就能快速定制组件外观而不需要为每种按钮单独封装组件。这两个点虽然不起眼却是写出整洁页面布局的地基。3. 源码逐段解析3.1 状态声明数据模型的划分页面开头是一段状态声明把所有数据清晰地分成两类State text: string The quick brown fox jumps over the lazy dog.; State input: string ; State running: boolean false; State elapsed: number 0; State correct: number 0; State resultText: string ; private timer: number -1; private startTime: number 0;前六个 State 变量是「驱动 UI 的数据」后两个 private 字段是「仅参与逻辑的中间量」。text 是预置的英文短文选用经典的 The quick brown fox jumps over the lazy dog. 作为练习句这句话几乎包含全部英文字母是打字练习的经典素材input 记录用户当前输入running 表示是否处于测试状态elapsed 是已用秒数correct 是正确字符数resultText 是测试结果的文本。timer 保存定时器句柄初始为 -1 表示尚未创建定时器startTime 记录开始时刻的时间戳。把「要渲染的值」与「纯逻辑值」分开既能减少不必要的重渲染也让代码意图更明确这是 ArkTS 开发里值得养成的编码习惯。3.2 start()重置现场并启动计时private start(): void { if (this.running) { return; } this.input ; this.correct 0; this.elapsed 0; this.resultText ; this.startTime Date.now(); this.running true; this.timer setInterval(() { this.elapsed Math.floor((Date.now() - this.startTime) / 1000); }, 1000); }第一行 if (this.running) return 是防重入保护测试进行中再点「开始」会被直接忽略避免多个定时器叠加导致计时错乱。随后把输入、正确数、用时、结果文本全部重置为初始值记录 startTime Date.now()把 running 置为 true最后创建 1000 毫秒周期的定时器。回调里 elapsed Math.floor((Date.now() - startTime) / 1000)用真实时间差除以 1000 再向下取整得到「已经过去的整秒数」。之所以不写成 elapsed 1是因为系统定时器回调可能因主线程繁忙而延迟累加式会逐步失真而差值式每次都会对齐真实时钟即使某一次回调延迟下一次也会立即纠正回来。3.3 stop()结束测试并统计结果stop() 是与 start() 对称的收尾逻辑。它先判断 if (!this.running) 直接返回防止「没开始就结束」的无效调用然后把 running 置为 falseclearInterval(this.timer) 停掉定时器并把句柄复位为 -1接着读取 input.length 作为总输入字符数调用 computeCorrect() 得到正确数再计算速度与正确率把三项结果拼进 resultText最后用 promptAction.showToast 弹一个「测试结束」的提示。其中计算口径的细节留到第 4 节展开。resultText 一旦被赋值页面底部的 if 条件渲染便成立结果卡片随之出现。3.4 输入联动与实时比对TextInput 的 onChange 会把每次输入后的完整字符串交给 onInputprivate onInput(v: string): void { this.input v; if (this.running) { this.correct this.computeCorrect(); } }第一行更新 input让原文着色和「已输入」计数随输入实时变化第二行的 if (this.running) 是关键——只有测试进行中才刷新 correct 统计用户在点击「开始」之前随便打字不会污染「正确」计数。computeCorrect() 的实现如下private computeCorrect(): number { let count: number 0; const len: number Math.min(this.input.length, this.text.length); for (let i: number 0; i len; i) { if (this.input.charAt(i) this.text.charAt(i)) { count 1; } } return count; }循环上限取两个字符串长度的较小值保证索引永不越界逐位比较 charAt(i) 是否相等相等则计数加一。这里对「输入超长」的情形处理得很干净如果用户输入的字符数超过了原文长度循环到 text.length 就停止超出的字符既不进正确也不进错误从语义上它们本来就不在原文范围内。3.5 charColor()字符三色反馈private charColor(idx: number): string { if (idx this.input.length) { return #c0c0c0; } if (this.input.charAt(idx) this.text.charAt(idx)) { return #0abf53; } return #e53935; }这个函数为第 idx 个字符决定颜色尚未输入到的字符是灰色 #c0c0c0已经输入且与原文一致的字符是绿色 #0abf53已经输入但不一样的字符是红色 #e53935。第一个分支判断的是 idx this.input.length即「从第 input.length 个字符开始都属于未输入」。charColor 在 ForEach 渲染每个字符时被调用input 一旦变化所有字符的颜色表达式都会重新求值从而呈现出「绿色追着光标走、错字立刻变红」的动态效果。3.6 UI 布局骨架页面 build() 的根节点是 Column宽高均为 100%背景 #f2f3f5。顶部 Row 内依次是返回按钮、标题与 Blank()Blank 吃掉剩余空间把标题顶到视觉居中的位置。正文用 Scroll 包裹保证内容可滚动内部 Column 间距 16。原文卡片是 Flex({ wrap: FlexWrap.Wrap }) 嵌套 ForEach(this.text.split(‘’), …)为每个字符生成一个粗体 Text颜色由 charColor(idx) 决定卡片白底、圆角 12、内边距 14。输入框 TextInput 的 text 绑定 this.inputplaceholder 提示「请输入上面的句子」onChange 调用 onInput。开始/结束按钮通过 layoutWeight(1) 平分一行颜色分别为蓝 #1a6cff 与橙 #ff8f1f。实时统计区是一个 Row内含三个等宽 Column每个 Column 上部是小号灰色标签用时/已输入/正确下部是 18 号粗体数值「正确」数值额外用绿色强调。最后的 if (this.resultText.length 0) 条件块内是结果卡片浅蓝底 #f0f4ff、圆角 12、文字居中并取品牌蓝色。4. 关键实现细节分析4.1 速度与正确率的计算口径stop() 里的两行计算是统计的核心值得单独拆解const typed: number this.input.length; this.correct this.computeCorrect(); const minutes: number this.elapsed / 60; const speed: number minutes 0 ? typed : Math.round(typed / minutes); const rate: number typed 0 ? 0 : (this.correct / typed) * 100;速度 speed 的语义是「每分钟平均输入多少字符」。typed 是实际输入的字符数minutes 是 elapsed 换算出的分钟数秒数除以 60所以 typed / minutes 的单位就是字/分钟。由于 elapsed 是整数秒当用时不足 1 秒时 minutes 为 0直接相除会得到 Infinity因此代码用 minutes 0 时的兜底值 typed 作为速度Math.round 让结果取整显示更友好。虽然实际场景中「不足 1 秒就结束」的概率极低但防御性写法杜绝了非数字结果的出现。正确率 rate 的定义是 correct / typed * 100即「打对的字符数 ÷ 实际输入的字符数」。这个口径很有讲究用户可能只打了一半就结束也可能打错很多字以实际输入为分母才能衡量「每打一个字打对的概率是多少」反映真实的打字质量。当 typed 为 0一个字符没打就结束时除数不能为零代码直接用 0 兜底。最后用 toFixed(1) 保留一位小数比如 98.3%直观又不冗余。4.2 正确字符数的双路径维护correct 这个状态在源码中有两条维护路径输入过程中由 onInput 触发 computeCorrect() 实时更新结束时由 stop() 再次调用 computeCorrect() 覆盖。两条路径复用同一个方法保证了「实时显示的数字」与「结束统计的数字」口径完全一致不存在两套算法互相打架的问题。这正是把比对逻辑抽成独立函数带来的直接收益——如果输入时和结束时各写一遍后续一旦改动就很容易出现不一致。4.3 三种颜色的边界条件charColor 的三个分支完整覆盖了输入过程的所有阶段尚未输入时所有字符保持灰色输入到一半时已输入部分出现绿红两色未输入部分仍是灰色输入超过原文长度后原文所有字符都进入绿红判断。尤其值得注意灰白分支的判断是 idx this.input.length即「第 input.length 个字符起算未输入」这与数组索引从 0 开始、文本字符序号从 0 编号的特性完全吻合。逻辑顺序上先判断有没有输入、再判断对不对也避免了对未输入字符做无意义的比较运算。4.4 计时精度与生命周期清理本页的计时设计有一个容易被忽略的优点elapsed 是「派生值」而不是「累加值」。每次回调都从 Date.now() 与 startTime 的差重新计算因此它永远反映真实流逝的时间。同时aboutToDisappear() 里 clearInterval(this.timer) 构成了资源清理的最后一道防线如果用户测试中途点返回离开页面定时器会立即被销毁不会出现「组件即将销毁却仍在每秒更新状态」的隐患。start 创建、stop 销毁、生命周期兜底三段式管理非常完整。4.5 结果文本的拼装与展示resultText 的拼装用字符串拼接实现先拼「用时」一行再用换行符 \n 连接「速度」与「正确率」三行文本作为一个整体赋给 resultText。Text 组件渲染时会把 \n 原样解释为换行配合 textAlign(TextAlign.Center) 实现三行居中排版。toast 提示则通过 promptAction.showToast({ message: ‘测试结束’ }) 发出它是 ArkUI 提供的最轻量的用户反馈手段适合作为操作完成的即时确认如果需要更醒目的反馈可以改用弹窗或自定义组件。另一个细节是结果卡片使用独立的 if 分支渲染而不是让卡片常驻再控制显隐——当 resultText 为空时组件树中根本不存在结果卡片节点既节省了布局计算也让状态与视图的对应关系一目了然。5. 运行效果与操作指南使用 DevEco Studio 打开工程把 index25.ets 所在模块作为运行目标连接模拟器或真机编译运行即可看到页面。首次进入时界面从上到下依次是顶部蓝色返回栏与「打字测速」标题白色圆角卡片内是灰字显示的英文原句下方是带「请输入上面的句子」占位提示的输入框再往下是蓝、橙两个大按钮与白色统计卡片此时结果卡片不显示整个界面安静待命。操作步骤十分简单。第一步点击「开始」输入框清空、统计归零、计时启动。第二步在输入框中跟打原句此时原文字符逐字变色——打对的变绿、打错的变红、还没打到的保持灰色统计卡片里的「用时」「已输入」「正确」三个数字实时跳动。第三步打完或打到想停的位置后点击「结束」屏幕弹出「测试结束」toast统计卡片下方出现浅蓝色结果卡片三行显示用时秒、速度字/分钟、正确率百分比一位小数。第四步如需再来一轮再点一次「开始」即可重置重来点「返回」则回到上一页。几点使用提示一是原文与输入必须完全一致才会判对包括大小写、空格与句点中文输入法处于全角状态时打出的符号可能被判定为错字二是测试进行中直接点「开始」会被防重入逻辑拒绝需要先「结束」再「开始」三是句子较长时正文区可上下滚动输入与统计始终可用。整体来看这个示例在真机上的手感顺滑绿红反馈直观很适合拿来做打字练习与速度自测。关于运行环境还有几点补充。ArkUI 的 Previewer 支持直接预览单个 Entry 页面但涉及 router.back() 的页面跳转在 Previewer 中效果有限建议用真机或模拟器验证完整的返回链路。真机上键盘弹出会压缩可视区域本页根节点是高度 100% 的 Column正文由 Scroll 包裹输入框与统计卡片始终可见配合滚动也不会丢失内容。小屏设备上 Flex 开启自动换行后原文卡片会自动调整每行字符数不会出现文字被截断的问题。若在模拟器上调试建议把字体缩放调到常规档位以免卡片布局与真机观感差异过大。6. 可扩展方向示例本身的完成度已经不错在这个基础上可以往多个方向延伸。功能层面可以把预置短文从单一句子扩展为一组练习文本段落、代码、数字串等用数组加随机或顺序切换的逻辑来轮换题目甚至可以定义练习数据模型可以把「字/分钟」扩展为「词/分钟」或补充「按键次数」「最长连续正确」等指标可以加入倒计时模式例如限时 60 秒看谁打得多还可以把成绩持久化保存形成历史成绩与进步曲线。交互层面可以在原文卡片上高亮当前应打的位置让用户视线有落点可以把输入反馈做得更精细例如错词标红下划线、逐词着色结束按钮可以改为「计时到自动结束」减少手动操作结果卡片可以加入星级评价根据正确率与速度综合给分提升趣味性与成就感。工程层面可以把计时与统计逻辑抽成独立的测速引擎类配合 interface 定义练习与成绩的数据模型便于单元测试与复用当内容规模变大后可以引入 AppStorage 等状态管理机制梳理跨页面数据流还可以补充无障碍标签、深浅色主题适配提升应用品质。任何一个方向都能把这个两百行的示例扩成一个完整的工具型应用。从数据模型的角度再看一次当前页面只有 text 一个字符串属于最轻量的形态。如果要支持多篇文章、多轮测速建议先定义 interface Practice { text: string; title: string; } 之类的练习模型再用 State 数组管理多道题目成绩记录同样可以用 interface 表达比如记录日期、用时、速度、正确率并写入本地持久化存储。数据模型先行、UI 与逻辑围绕模型展开是页面复杂度上升时保持可维护性的关键习惯。扩展时也别忘了给「开始」按钮增加题目切换的能力让每轮测试都能选择不同的练习文本。7. 常见问题与调试技巧先整理几个读者在实现同类功能时容易遇到的问题与对策。问题一重复点击「开始」导致计时越走越快。原因是一次次叠加创建了多个 setInterval。对策正是源码中的 if (this.running) return 防重入判断也可以改成「先 clearInterval 再 setInterval」的写法做双保险。问题二从页面返回后出现「组件已销毁仍在更新状态」之类的告警。原因是定时器没有被清理。对策是在 aboutToDisappear() 里 clearInterval并与 start/stop 内的清理形成闭环。调试时可以用 DevEco Studio 的 HiLog 观察日志节奏判断定时器是否仍在运行。问题三计时数字感觉不准或「跳秒」。原因多是把 elapsed 写成自增elapsed 1而定时器回调受主线程任务影响并不严格每秒一次。对策是采用本页的时间戳差值方案elapsed Math.floor((Date.now() - startTime) / 1000)准确实时。问题四汉字输入法下正确率异常。charAt 按码元逐位比较全角/半角符号、输入法联想候选词都会影响最终文本。对策是测速场景限定英文原句并确保输入法处于英文半角状态未来若支持中文句子需要改用更细粒度的一致性判定。问题五结果里出现 Infinity 或 NaN。这是除零边界没处理的典型表现源码用 minutes 0 与 typed 0 两个分支规避。排查时可以临时打印 typed、elapsed、correct 三个中间值快速定位是哪个除数出了问题。调试技巧上推荐三件事一是用 Preview 在 Previewer 里单独预览原文卡片、统计卡片等小块布局快速验证视觉效果二是用 HiLog 在 start、stop、computeCorrect 的关键路径打点观察 running、elapsed、correct 的变化是否符合预期三是真机调试时用「布局查看器」检查 Flex 换行与层级。另外改写代码时务必保持 computeCorrect() 的唯一实现避免出现两处互相矛盾的比对逻辑。最后补充一个容易被忽略的细节本页的输入框没有设置 maxLength 或输入类型限制用户输入任意长度的文本都是允许的超出原文的部分被比对逻辑自然忽略这是有意为之的宽松设计。如果希望严格限制用户输入可以在 onInput 里对 v 做截断处理例如只保留前 text.length 个字符再赋给 input这样「已输入」计数永远不会超过原文长度视觉上更贴近练习场景。8. 总结示例 25「打字测速」用约两百行 ArkTS 代码完成了一个交互完整、反馈即时、统计合理的测速应用。回顾全文这个页面在多个维度上体现了良好的工程素养State 响应式状态让「输入→重算→重绘」全自动发生时间戳差值式计时从原理上规避了定时器漂移computeCorrect() 单一实现被输入与结束两条路径复用保证统计口径一致charColor() 把比对算法映射成直观的绿、红、灰三色反馈防重入、除零兜底、生命周期清理三处防御性代码把边界情况一一封堵。对初学者而言这个示例是理解 ArkUI 声明式开发的良好标本同时示范了状态管理、定时器、条件渲染、列表渲染、Flex 布局、生命周期与模块导入对进阶读者而言文中讨论的统计口径、边界处理与扩展方向也可以迁移到更复杂的业务页面中。把这篇代码读透、动手改一改、加上自己的练习库与成绩曲线你就真正掌握了这个示例的精髓。下一篇我们将解析示例 26「单词闪卡 FlashCard」看看 interface 数据模型与翻卡动画是如何实现的。