ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

HarmonyOS四则运算演示器:ArkTS实现运算顺序逐步可视化

HarmonyOS四则运算演示器:ArkTS实现运算顺序逐步可视化 做到第71个练手项目我特别想聊这个四则运算演示器。HarmonyOS的应用实例不难搜很多都套路化但这个题目的价值在于它并不只是写一个计算器而是把小学数学里最容易搞混的“先乘除后加减有括号先算括号”规则在ArkTS里做成了可视化逐步演示。它解决了一个实际痛点光告诉结果还不够必须把每一步运算顺序、每一步优先级怎么变化都展示出来。如果你已经会写基础ArkUI页面想找一个既有界面又有算法逻辑的HarmonyOS项目来练手这个题目很对口味。1. 项目定位它和普通计算器到底有什么不一样1.1 从“计算器”到“运算顺序演示器”的思维转变如果只是做一个能算四则运算的应用HarmonyOS里有现成的组件和逻辑可以直接套输入表达式、点击、出结果三分钟就搞定了。但“运算顺序演示器”这个定位不一样它要求的不只是结果而是要把中间过程完整呈现出来。举个例子表达式2 3 × 4 - 8 ÷ 2普通计算器直接给出10就完事了但演示器需要说明第一步先算3 × 4得到12第二步再算8 ÷ 2得到4第三步计算2 12第四步计算14 - 4最终结果是10这个差异决定了整个项目的设计核心计算引擎必须能够把一次完整运算拆解成多步并且每一步都要记录“我算的是哪个子表达式”、“结果是什么”、“当前整个表达式长什么样”。这是一个典型的“过程展示型”应用计算能力反而是次要的重要的是顺序逻辑。1.2 适合什么人、练什么能力这个实例适合两类人刚学完HarmonyOS基础组件、想深入理解状态管理细节的开发者。项目里会频繁用到State、Prop、ForEach以及数组替换不刷新视图这类经典坑。想给孩子做数学辅助工具、顺带练练手感的家长开发者。把运算规则做成演示器比单纯刷题直观得多孩子可以看到每一步高亮、每一步结果。从技术覆盖面上看这个项目几乎不依赖第三方库核心代码就是“表达式解析 步骤生成 UI绑定”。难点在于把解析算法讲清楚然后配合ArkTS的响应式状态让步骤逐条刷新出来。这比做几十个静态列表页面要扎实得多。1.3 功能拆解我在开发前把功能拆成了四块表达式输入区支持数字、加减乘除、括号输入允许修改和清空。校验与容错表达式不完整、括号不配对、连续运算符要能给出友好提示。逐步运算引擎把表达式转换成一系列带步骤说明的计算过程。步骤展示区用列表展示每一步同时高亮正在计算的局部表达式。这四块听起来简单但每一步都有隐藏问题。比如“高亮当前计算片段”就涉及原始表达式字符串和当前步骤之间的位置映射处理不好就高亮错地方。2. 运算顺序规则与技术选型思路2.1 优先级、结合性和括号的完整处理做这个项目之前必须先明确四则运算的完整规则不是只记一句话就够的括号优先从最内层括号开始计算乘号和除号优先级相同同级从左到右依次计算加号和减号优先级相同同级从左到右依次计算减号可以当成负号处理但这个项目我先不做负号避免解析复杂化这里最容易出问题的就是“同级从左到右”。很多人实现计算器时只处理了优先级没有处理结合性结果8 ÷ 4 ÷ 2会算出错误答案。正确结果是1因为要先把8 ÷ 4算成2再用2 ÷ 2得到1如果从右往左算就会得到4。我在设计引擎时把运算符定义为一个枚举每个运算符带两个属性优先级和方向。优先级用数字表示数字越大越先算方向表示同级时从左还是从右处理。加减乘除都是左结合不需要特殊处理。2.2 为什么选用“中缀转后缀”而不是递归解析表达式3 4 × 2这种写法叫中缀表达式人类看着舒服但程序处理顺序很麻烦。常见方案有两种递归下降解析直接处理中缀表达式通过递归函数识别优先级代码直观但分支多。调度场算法先把中缀表达式转换成后缀表达式后缀表达式也叫逆波兰式然后依次压栈计算顺序天然和运算法则一致。我最终选择了调度场算法原因很实际它完美契合“逐步演示”的需求。调度场算法的输出序列本身就体现了运算顺序比如3 4 × 2转成后缀是3 4 2 × 一眼就能看出要先乘法后加法。后续步骤展示就是把后缀表达式的求值过程翻译成文字。有人可能会问递归解析是不是更简单对于嵌套括号很深的表达式递归代码确实不难写但它不好记录“每一步发生了什么”。而调度场算法天然带着栈操作过程每一步都有迹可循。要把运算过程展示给用户选这种结构化算法更省劲。2.3 步骤数据模型设计要让步骤展示有条理数据模型必须先定清楚。我在项目里定义了一个StepRecord类class StepRecord { // 当前步骤的说明文字 description: string; // 参与这一步运算的两个操作数的值 leftValue: number; rightValue: number; // 运算符 op: string; // 这一步的结果 result: number; // 在当前表达式文本中的起始位置 startIndex: number; // 在当前表达式文本中的结束位置 endIndex: number; // 计算完这一步之后整个表达式的文本快照 snapshot: string; }这个模型里最关键的是startIndex和endIndex。它们用于在原始表达式里高亮当前正在计算的片段。比如原始表达式2 3 × 4在算3 × 4这一步时高亮范围应该从字符串第4位到第8位。没有这两个字段高亮功能就只能靠猜。snapshot字段则用于展示“当前表达式变为什么样了”。每算完一步我都把新的表达式字符串存下来这样步骤列表里可以直接显示中间化简结果不需要用户自己脑补。3. HarmonyOS界面拆解与交互布局3.1 页面整体结构设计在ArkUI里做这个页面我用的是Column嵌套滚动布局整体分三层顶部表达式展示区用大号字体显示当前输入的表达式和一个“演示”按钮。中部步骤结果区用List或Scroll组件展示计算步骤。底部数字和运算符按钮区。之所以把输入区放在顶部、按钮放在底部是为了符合手机单手握持习惯。孩子用的时候拇指最容易够到的是底部按键展示区在整个页面中段视线也更集中。我用滚动列表Scroll而不是List来做步骤区因为步骤数量通常不超过几十条滚动需求简单Scroll加ForEach就够用性能开销也小。3.2 按键布局与输入限制按键区我用了四行7 8 9 ÷ 4 5 6 × 1 2 3 - 0 . C 还有一个单独的全宽按钮放“括号”和“演示”。这里要注意÷和×在字符串里直接保留原符号但在解析时要做映射把÷映射成除法运算符把×映射成乘法运算符不要真的用文本里的unicode参与计算。我还加了一层输入限制逻辑运算符后面不能直接跟另一个运算符除非是负号场景小数点不能重复输入左括号必须和右括号配对。这些限制用onClick或onChange事件拦截都行我用的是在按钮点击处理函数里统一判断这样所有入口都走同一套规则。3.3 高亮和步骤联动的实现思路步骤高亮是这个演示器的灵魂。我的实现方式是解析步骤数据时记录每个步骤在原始表达式中的位置区间[startIndex, endIndex]。在表达式文本显示组件中把普通文本和需要高亮的文本拆成多个Span子组件。根据当前选中的步骤索引动态计算哪些Span需要用不同颜色显示。在ArkTS里可以使用Text组件的Span子组件实现单段混排颜色例如Text() { ForEach(this.textSegments, (segment: TextSegment) { Span(segment.text) .fontSize(24) .fontColor(segment.isHighlighted ? Color.Red : Color.Black) .fontWeight(segment.isHighlighted ? FontWeight.Bold : FontWeight.Normal) }) }这里有个地方要特别注意原始表达式里每个字符都要放进对应的TextSegment数组里哪怕一个连续的数字串也要按“是否落入高亮区间”拆成多个片段不能直接按空格切分否则高亮位置会对不上。4. 核心代码实现详解4.1 表达式合法性校验计算前先校验这一步不能省。我写的校验函数做了三件事括号配对遇到左括号入栈右括号出栈最后栈必须是空的。运算符位置表达式开头不能是运算符负号场景先跳过结尾不能是运算符。连续运算符出现× 这类连续运算符要报错。以括号校验为例思路很简单function checkBrackets(expr: string): boolean { let stack: string[] []; for (let ch of expr) { if (ch () { stack.push(ch); } else if (ch )) { if (stack.length 0) { return false; } stack.pop(); } } return stack.length 0; }这里我把“括号配对”和“表达式位置”分开写因为错误提示需要区分是“括号不配对”还是“输入不完整”合并写虽然省代码但用户看到的提示会很不友好。4.2 表达式拆分数字、运算符、括号字符串表达式不能直接逐字符丢给后续算法得先拆成Token列表。常见的坑是数字有多位比如123必须被识别成整体小数点也要跟数字一起归入同一个Token。下面这段是我项目里的拆分逻辑核心function tokenize(expr: string): Token[] { let tokens: Token[] []; let i 0; while (i expr.length) { let ch expr[i]; if (isDigit(ch) || ch .) { let start i; while (i expr.length (isDigit(expr[i]) || expr[i] .)) { i; } let numStr expr.substring(start, i); tokens.push({ type: TokenType.NUMBER, value: parseFloat(numStr), rawText: numStr, startIndex: start, endIndex: i - 1 }); continue; } if (ch ) { tokens.push({ type: TokenType.PLUS, value: 0, rawText: ch, startIndex: i, endIndex: i }); i; continue; } // 减号、乘号、除号、左括号、右括号同理 } return tokens; }这里每个Token都保存了startIndex和endIndex这部分数据是后面高亮功能能精确工作的基础。如果只在Token里存数字和运算符类型后续就追查不到原始位置了。我就是因为第一版没记录位置后来不得不重写了一次。4.3 调度场算法中缀转后缀调度场算法的核心是维护一个运算符栈。遍历Token时数字直接进入输出队列运算符则要跟栈顶运算符比较优先级如果栈顶优先级更高或相同就先弹出栈顶进输出队列再把当前运算符压栈遇到左括号直接压栈遇到右括号则一直弹栈直到碰到左括号。用2 3 × 4举例读到2直接输出读到栈空压栈读到3直接输出读到×比较栈顶是×优先级更高所以×压栈读到4直接输出结束后弹出栈中所有运算符这样就得到后缀表达式2 3 4 × 。ArkTS代码里我就是按这个逻辑写的注意数组的pop()和push()方法可以直接复现栈操作。不需要自己实现栈类直接用Arraystring就行。4.4 后缀表达式求值并生成演示步骤得到后缀表达式之后求值阶段就是专门为“演示”服务的。每次从后缀表达式里取出一个数字时压入数字栈取出运算符时从数字栈弹出两个数字按顺序计算然后生成一条StepRecord。这里要处理一个最关键的细节表达式的原始展示文本和计算顺序之间的对应关系。后缀表达式里数字和运算符都已经丢失了原始顺序所以我需要额外维护一个映射关系在解析每种Token时就把它们指向的原始位置保存下来。我的处理方式很简单把每个Token都拷贝一份到“求值队列”里这个队列元素除了类型和数值还带着它在原始表达式里的开始和结束位置。每次执行一个运算符时读到的就是这个运算符Token以及它关联的操作数Token的位置信息这样生成的步骤说明可以精确指向原始表达式中的某一段。function evaluateToSteps(postfix: Token[]): StepRecord[] { let numStack: number[] []; let records: StepRecord[] []; for (let token of postfix) { if (token.type TokenType.NUMBER) { numStack.push(token.value); } else { let right numStack.pop()!; let left numStack.pop()!; let result applyOp(left, right, token.type); // 记录这一步对应的原始位置 records.push(buildStepRecord(left, right, token, result)); numStack.push(result); } } return records; }注意applyOp里除法要判断除数为零的情况我在项目里专门弹了个提示“除数不能为0”。虽然在教育场景里这是个好教学点但应用不能崩溃。4.5 演示过程的节奏控制步骤列表一次性生成之后怎么播放也有讲究。我提供了两种模式“一键出步骤”所有步骤同时生成点击“演示”后全部显示。“逐步播放”按时间间隔逐条显示模拟老师在黑板上一步步写。逐步播放我在实现时用了setInterval每500毫秒往步骤数组里追加一条记录。但这里有个ArkTS状态管理的大坑数组元素的添加不一定触发视图刷新。我踩了好几次后面专门用了一个State currentStepCount: number来驱动显示State displayedStepCount: number 0; startPlay() { let timer setInterval(() { if (this.displayedStepCount this.steps.length) { clearInterval(timer); return; } this.displayedStepCount; }, 500); }UI里渲染步骤时只取前displayedStepCount条这样状态改变一定会触发重新渲染方式笨但绝对稳。如果你只把this.steps.push(record)写在定时器里而不改变任何其他State变量界面很可能一动不动。5. 实操踩坑记录状态、动画与输入法的那些事5.1 ArkTS数组更新不刷新视图这个问题可以说是HarmonyOS应用开发里最高频的坑之一。很多人写了this.dataSource.push(item)之后发现列表不更新原因是State监听的是引用变化而不是原地修改。虽然某些场景下框架会深度观察但依赖这种边缘行为太危险了。我的解决方案很简单每次修改数组后重新赋值一个新数组引用。例如let newList: StepRecord[] this.steps.slice(); newList.push(record); this.steps newList;这样视图刷新响应非常稳定。如果你用装饰器Observed和ObjectLink去跟踪复杂的类对象也能解决但对这种简单数据我更推荐直接替换整个数组代码更直观行为也更好预测。5.2 高亮位置偏移与文本拆分这个坑非常隐蔽。原始表达式经过Token拆分后我记录了每个Token的起止位置但用户输入的表达式里可能掺杂了空格。我在输入层限制了用户不能输入空格不过从别处复制粘贴就可能带进来。空格会导致字符位置和下标的计算差一位到两位高亮就会偏。处理办法是入库前先做一次“净化”把所有空格、不可见字符统一剔除expression expression.replace(/\s/g, );如果你不想剔除空格就要在计算高亮区间时做偏移映射。我权衡之后选择了剔除因为作为教学演示器空格本来也不影响语义。5.3 软键盘弹出遮挡按键区页面底部是按钮区在真机上点击输入框编辑表达式时软键盘弹出会遮挡底座。原因很简单页面高度没有随着键盘调整。HarmonyOS里比较直接的处理是用expandSafeArea或者设置页面的键盘避让模式。我在项目里用了window.setWindowInputMode调整默认输入模式设置为adjustResize让页面能自动收缩高度按键区就能跟着上移。不过还要注意不要给底部按钮列设置固定高度height尽量用layoutWeight或安全区适配否则键盘弹出来时按钮区会和键盘叠在一起。5.4 逐步播放时高亮闪烁逐步播放时每一条新步骤出现用户希望当前步骤的表达式高亮能跟着变化。我的实现是在步骤列表当前展示的索引变化时重新计算textSegments并把新数组状态赋值回去。高亮闪烁的bug出在我没设置过渡动画导致每次重新生成文本段数组时整个表达式都被强制刷新一次看起来就像闪了一下。解决方式很直接要么不加动画要么用animateTo做颜色变化的平滑过渡。我最终选择不加动画只改变字体颜色和粗细这样在教育场景里更利索不会因为动画干扰视线。另外如果用户点击任意一条历史步骤我希望高亮能跳转到对应位置。这个我用了一个State只存当前选中步骤的索引点击列表项时动态更新因为它本身就驱动了文本段数组重建所以联动是自动化完成的不需要额外刷一次UI。6. 表达式校验的几个边界情况6.1 括号嵌套与非法输入我实际测试时发现用户最容易输入的非法表达式其实是())重复括号和2 (3 × 4这种漏右括号的情况。前者我在括号配对检查里直接拦截后者在表达式结尾也会被判为非法。但有个边界容易被漏掉2 (3 × 4))这种表达式括号总数是配对的但右括号数量在中间某个位置超过了左括号的累计数量。如果只是简单数左右括号数量会被当成合法。所以括号校验必须用“遇到左括号计数加一遇到右括号计数减一中途出现负数就非法”的方式不能只看最终计数。6.2 除数为零和重复小数点除数为零在演示步骤里如何表现我做了个特殊设计不直接报错而是在步骤里用文字展示“这里不能除以0所以这个表达式不成立”并且终止后续计算。这个处理的逻辑是教育应用应该解释为什么非法而不是只给一个NaN结果。重复小数点1.2.3在Token解析阶段就会出问题我会在解析时判断一个数字字符串内的小数点数量是否大于1如果大于1就返回一个错误码提示用户格式不正确。6.3 形如2 × ÷ 3的连续运算符连续运算符会在解析过程中被识别。我在输入限制之外还给解析器加了一层容错如果刚解析了一个运算符紧接着又出现另一个运算符且中间没有数字就判断当前Token类型不合法。这样即使在极少数绕过输入限制的情况下引擎也不会崩溃而是输出友好的错误提示。这个双保险很重要。输入限制是针对人手点击按钮设计的如果用户用输入法从剪贴板粘贴表达式就能绕过按钮点击逻辑所以解析器本身必须可靠。7. 后续扩展方向与个人经验7.1 可以扩展成“分步讲解器”做完这个演示器之后我其实产生了更多想法。最直接的一个是把它扩展成“步骤详解器”不仅仅是演示运算顺序还可以在每一步下面追加讲解文字比如“为什么这里要先算乘法因为乘法优先级比加法高括号以外的乘除要先于加减计算”。这个扩展需要增加一个“讲解词生成器”在生成步骤时根据运算符类型附上对应的规则说明。代码层面改动不大只是数据模型里加一个explanation字段。如果你想让这个应用更适合儿童教育我建议把颜色做得更大胆一些比如用高亮色块圈住正在计算的部分点击步骤条可以高亮到具体数字。孩子看到“原来先算的是这一段”比大人看到结果更有意义。7.2 从项目里得到的实际体会开发这个应用时最大的收获不是掌握了调度场算法而是理解了“状态管理 计算过程”之间如何配合。一个计算器的功能可以靠一次性计算解决但一旦涉及过程展示就必须把日志记录、变量状态、UI刷新全部编排好。如果你准备动手复刻这个实例我建议从“不带动画的单机版本”开始先把计算引擎和步骤生成跑通再美化界面。直接一开始就追求动画和视觉效果卡壳之后很难定位是解析问题还是渲染问题。还有一点经验可以分享程序里的步骤记录不要只为了人看而写要给每一个步骤配上“机器可读”的字段比如原始位置区间、参与运算的左右值后续做高亮、做撤销、做跳转都有用。只输出一句人话后面想重新计算就抓瞎了。这个项目现在还在我的练习列表里下一步我准备把它移植成一个HarmonyOS的卡片服务让用户不用打开应用就能看到上一条表达式运算过程。做卡片时数据绑定又会有新的坑到时候再跟你们聊聊。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表