☰
HarmonyOS实战:ArkUI+状态机打造小学数学“凑十法”动画演示
2026/10/5 11:04:36 网站建设 项目流程

凑十法这个事,我一直觉得是小学数学里最值得做成动态演示的节点之一。孩子刚接触进位加法,脑子里还没有“凑整”的概念,你跟他讲“8加5,把5拆成2和3,8加2等于10,再加3等于13”,他能复述,但未必真懂。文字的抽象表述对低龄孩子来说太枯燥了,而传统静态板书又无法呈现“拆、移、补”这个过程。正好我在做HarmonyOS应用系列开发,就想着用ArkTS写一个20以内进位加法的凑十法动画演示,把“凑十”的每一步拆成可视化的动画,让数字自己动起来给孩子看。

这个项目的核心价值不在算法,而在交互和动画表现力。HarmonyOS的ArkUI声明式开发模型自带一套比较完整的动画API,从隐式动画、显式动画到属性动画、转场动画都有,适合做这种步骤化的教学演示。而且HarmonyOS NEXT SDK(API 12+,5.0.0(12))对Canvas、属性动画、组件转场的支持已经非常稳定,完全可以支撑这类中小型教育应用。做这个应用的过程,本质上也是把一套“教学流程”翻译成“状态机+动画”的实践,我做完之后最大的感觉是:动画不是花哨的装饰,而是认知过程的具象化。

如果你也是HarmonyOS开发者,或者正在给孩子做数学启蒙相关的数字化工具,这篇实战拆解可以帮你少走不少弯路。我会从数据建模、状态机设计、动画实现到性能优化,把每一步的思路和坑位都摆出来,你可以直接照着写。

1. 这个项目到底在做什么

先明确应用范围。20以内进位加法,指的是两个加数都在10以内、结果在20以内的加法,核心场景是“和超过10”的算式,比如7+6、8+5、9+4这类。为什么单独把“凑十法”拿出来做动画?因为它是小学一年级数学里最经典的策略——通过拆分较小的加数,把其中一个加数补成10,剩下的数再加过去。它的认知过程有三个关键步骤:

  1. 看大数,找它离10还差几。
  2. 拆分小数,拿相应的数量去补大数。
  3. 把补成10的部分和剩余部分合并,得到结果。

这三个步骤如果只用静态文字,孩子很难在头脑中形成图像。如果做成动画,孩子能亲眼看到“小数分走了一部分,大数变成了10,多出来一块拼上去”的动态过程,理解效率会高得多。这也是我做这个项目的初衷——用数字可视化降低认知门槛。

1.1 凑十法的教学痛点

传统教学里,老师会让孩子在图上圈一圈,或者用小棒摆一摆。小棒是实物,孩子操作起来直观,但问题是实物操作无法记录“每一步发生了什么”,也很难自动化生成大量练习题。而APP里常见的算术练习软件,多数只给出正确答案动画,不会拆解凑十的完整思维过程。说白了,市面上的工具要么只做结果验证,要么只做静态图解,缺少一个真正意义上的“过程动画”。

我走访过几位低年级数学老师,他们反馈的最核心痛点是:凑十法最难教的不是“结果”,而是“为什么拆这个数,以及拆出来之后怎么办”。比如“7+6”为什么要把6拆成3和3,而不是4和2?因为7需要3才能凑成10。这个“需要多少”的逻辑,孩子往往要反复看多次实物操作才能内化。而数字动画恰好可以反复演示同一操作,甚至让孩子自己点击控制步骤,对于课后复习和预习都非常有帮助。

1.2 为什么选HarmonyOS做动画演示

选择HarmonyOS而不选普通的Web页面或Android原生,主要基于三个考虑。第一,HarmonyOS NEXT SDK从API 12开始,ArkUI的动画能力已经覆盖了属性动画、显式动画、路径动画、Spring动画等,针对这类“小元素移动、缩放、透明度变化”的演示场景,根本不需要引入第三方动画库,原生API就能做得很顺。第二,ArkTS的声明式UI范式,非常适合把“画面状态”映射成“数据状态”。我可以定义一个步骤枚举,界面根据当前步骤自动渲染不同状态,动画由状态变化触发,逻辑清晰,不容易出错。第三,我打算后续在这个系列里做更多数学教学演示,比如退位减法、乘法口诀表,统一基于鸿蒙生态有利于做一套可复用的教学组件,所以我直接基于HarmonyOS NEXT SDK(API 12+ / 5.0.0(12))来开发。

网上关于HarmonyOS NEXT的动画教程多半是转圈、淡入淡出这类基础效果,真正针对“教学类分步动画”的案例并不多。这个项目是一个很好的补位,把教学流程和动画状态结合起来的思路,可以被很多教育类APP借鉴。

2. 核心设计与数据建模

动手写代码之前,先把数据结构和运行状态模型想清楚。动画演示和静态页面最大的区别在于:每一帧都不是孤立的,你需要知道自己“现在处在流程的哪一步”,才能决定下一步怎么动。这个应用我设计了三层数据模型:算式模型、步骤模型、动画状态模型。

2.1 表达式数据结构

一个加法算式本质上包含两个加数和结果。为了支撑凑十过程,还需要扩展出“大数”“小数”“差量”“拆分结果”等派生字段。我定义一个AdditionExpression类:

@Observed export class AdditionExpression { operand1: number; // 加数1 operand2: number; // 加数2 sum: number; // 和 bigOperand: number; // 较大的加数(凑十的基准数) smallOperand: number; // 较小的加数(用于拆分) diffToTen: number; // 大数距离10的差值 splitFirst: number; // 从小数拆分出的第一部分(补大数) splitSecond: number; // 拆分后的剩余部分 constructor(a: number, b: number) { this.operand1 = a; this.operand2 = b; this.sum = a + b; this.bigOperand = Math.max(a, b); this.smallOperand = Math.min(a, b); this.diffToTen = 10 - this.bigOperand; // 如果大数+小数不需要进位,则不需要拆分 this.splitFirst = this.bigOperand + this.smallOperand > 10 ? this.diffToTen : 0; this.splitSecond = this.smallOperand - this.splitFirst; } }

这里有几个设计细节。第一,为什么要用diffToTen而不是直接写死?因为不同的算式,大数离10的差值不同,比如7+6的diff是3,8+5的diff是2,9+4的diff是1。动画要根据diff决定“从小数挪几个点过去”。第二,splitFirst可能为0,说明这个算式不需要凑十,直接相加就好。这类算式不应该出现在演示模式里,但在随机出题时可以过滤掉。

这个数据类用@Observed装饰,是为了让绑定的UI组件能在数字变化时自动刷新。ArkTS中被观察对象的变化会自动触发组件刷新,省去了手动setState的麻烦。

2.2 动画状态机设计

动画演示的核心是“步骤推进”。我定义了一个枚举表示当前流程阶段:

export enum DemoStep { Initial, // 初始状态:展示两个加数圆点 HighlightBig, // 高亮大数,提示它与10的差距 SplitSmall, // 拆分小数:一部分移向大数 ComposeTen, // 凑成10:大数补满,形成10 AddRemainder, // 加上剩余部分,得出结果 ShowResult // 显示最终算式和结果 }

状态机流转规则如下:

当前步骤用户操作/自动触发下一状态
Initial点击“开始”按钮HighlightBig
HighlightBig自动延迟 1.2 秒SplitSmall
SplitSmall动画完成回调ComposeTen
ComposeTen动画完成回调AddRemainder
AddRemainder动画完成回调ShowResult

为什么要用状态机而不是直接写一个“顺序执行”的动画序列?因为教学演示必须允许用户随时暂停、回放、分步查看。状态机可以把“当前进行到哪一步”作为唯一事实来源,UI据此渲染按钮的可用态和文案,逻辑非常清晰。否则,动画一旦播完,你想回到中间某一步,就得重新算所有中间状态,代码会非常难维护。

我还在状态上绑定了步骤说明文案。比如到了SplitSmall阶段,底部提示文字变成“把小数拆成3和3,其中3去补7”,这样孩子在看动画的同时能听到/看到对应解说,理解更到位。

3. 动画实现细节与代码解析

有了数据模型和状态机,接下来就是整个项目最花功夫的部分:怎么用HarmonyOS ArkUI把“凑十过程”做成一个有说服力的动画。我采用的方案是:用一组圆形组件(Circle或自定义Shape)表示数字单位,每个圆有自己的坐标,通过animateTo显式动画改变坐标和透明度。这样比用Canvas绘制全部内容更容易与ArkUI组件树融合,也方便点击交互。

3.1 小球(数字点)的布局与绑定

每个加数用一组圆点表示,例如“7+6”,左边7个圆点,右边6个圆点。圆点大小、间距、颜色都要考虑低龄儿童的视觉特点,我用了较大的圆点(直径40vp),行间距约8vp,避免密集排列造成视觉混淆。

圆点组件使用ForEach循环生成:

ForEach(this.leftDots, (item: number) => { Circle({ width: 40, height: 40 }) .fill(this.currentStep === DemoStep.HighlightBig ? '#FF8C00' : '#FFB6C1') .opacity(this.dotOpacity(item)) .offset({ x: this.dotOffsetX(item), y: this.dotOffsetY(item) }) }, (item: number) => item.toString() + this.currentStep.toString())

每个圆点有一个唯一ID(其实就是索引号),dotOpacity和dotOffset根据当前状态返回对应的值。这里最关键的点是第三个参数——keyGenerator。因为这个项目里同一个圆点会在不同步骤间改变自身坐标和透明度,如果不把当前步骤拼进key里,ArkUI可能会错误复用组件,导致动画数据错乱。我踩过这个坑:一开始用固定的索引作为key,步骤切换后组件没有被重建,offset计算总是不对。后来把currentStep拼进key,强制状态变化时重新生成组件树,才彻底解决了问题。

3.2 凑十过程的显式动画驱动

HarmonyOS的animateTo是最适合这种“点击/步骤触发”场景的动画接口。它的用法是:在animateTo闭包内修改状态变量,闭包外部的状态变化就不会触发动画,只有闭包内的变化会被捕获并自动生成过渡动画。

举个例子,从SplitSmall阶段进入ComposeTen阶段时,需要把从“小数”拆分出的圆点移动到“大数”区域,使大数刚好变成10个圆点。这段逻辑如下:

this.currentStep = DemoStep.ComposeTen; this.leftDotPositions = this.composeTenPositions(); // 返回新坐标数组 this.rightDotPositions = this.rightAfterSplit(); // 剩余圆点位置 this.animateTo({ duration: 800, curve: Curve.EaseInOut, onFinish: () => { this.currentStep = DemoStep.AddRemainder; } });

这里面animateTo的duration我设置成800毫秒,不是随口定的。太短(比如300毫秒)孩子看不清圆点移动轨迹,太长(超过1.5秒)又容易注意力涣散。实际测试下来,800毫秒配合EaseInOut曲线最舒服:起步慢一点,中间快一点,最后再慢下来,视觉上很像一只手在轻轻推动圆点。

拆分的动画则稍微复杂一些。小数的6个圆点要分成两组,一组(2个)保持原位不动,另一组(3个)分别移动到7对应的空隙位置。这里我没法直接“整体移动一个容器”,因为每个圆点的目标位置不同。所以我的实现方式是:预计算每个圆点的最终x、y坐标,然后在SplitSmall触发时,通过animateTo统一更新所有圆点坐标。

坐标计算具体来说,我预设了一个基准坐标系。初始状态下,左侧圆点放在屏幕左半区域(x=80~240,y=100~260),右侧圆点放在右半区域(x=280~440,y=100~260)。当右侧被拆分时,这3个圆点的目标坐标会“插入”到左侧7个点之间的间隙中,形成“补位”效果。为简化问题,我提前写死了几个固定插槽位置,例如:

  • 第1个补位点:x=200, y=100
  • 第2个补位点:x=200, y=160
  • 第3个补位点:x=200, y=220

这些坐标对应大数7的圆点网格中最右列的空位。因为不同大数的空位数量不同,我写了一个getTargetPositions函数,根据diffToTen动态计算。没必要做得多精巧,关键是让孩子看到“移动+填充”的对应关系。

3.3 高亮与提示逻辑

凑十法动画里,“高亮”不是简单的配色变化,而是认知引导。当过程进行到HighlightBig阶段,大数圆点不仅颜色变化,还可以加上一个呼吸动画——透明度从1.0到0.6循环,提醒孩子“注意看这个大数离10还差几个”。这个呼吸效果用repeat参数实现:

this.animateTo({ duration: 600, iterations: 3, playMode: PlayMode.AlternateReverse }) { this.systemBigHighlight = false; }

同时在界面上出现一个气泡提示:“7还差3个就是10”。这个文本绑定了一个状态变量,状态变化时文本以渐隐渐显的转场效果切换。为了让文字切换不突兀,我给文字外层容器加了.transition(TransitionEffect.OPACITY),当文字内容变化时自动触发淡入淡出。

这里有个细节:儿童教育类应用一定要注意“高亮”本身不能太刺激,饱和度过高的红色或闪烁太快的动画容易让孩子视觉疲劳。我查过一些儿童心理学资料,低龄儿童对慢速、有节奏的视觉反馈接受度更好。所以动画统一用柔和的橙色#FF8C00做高亮,避免刺眼的红色,闪烁也控制在3次以内。

3.4 算式展示与结果反馈

当所有圆点移动完毕,进入ShowResult阶段,底部会显示完整的算式:“7 + 6 = 13”。但这个展示不是一次性出现,而是分步的:先显示“7 + 6 =”,十位和个位的“1”和“3”分别用两个数字卡片弹出,辅以轻微的缩放动画。

个位“3”是原来小数拆分后剩余的部分,十位“1”则是由“7+3=10”这一组圆点整体变化而来。我用了两种视觉帮助孩子理解:

  1. 把“补成10”的那组圆点用一个虚线圆框圈起来,上方标出一个“10”的标签。
  2. 剩余3个圆点下方标出“3”的标签。

这样孩子就能直观看到,“13”是由“10”组和“3”组拼装出来的。这个“部件拼合”的视觉隐喻,其实就是位值制的基础。很多孩子计算进位加法出错,根本原因就是没有建立“10个一组”的位值概念。动画演示能很好地补齐这个认知缺口。

4. 踩坑记录与性能优化

这个项目规模不大,但涉及动画状态管理、组件复用、动态坐标计算等,实际开发中还是遇到了几个值得记录的问题。我把它们整理出来,给后面做同类应用的朋友做个参考。

4.1 动画不同步:多个 animateTo 嵌套的坑

最早版本里,拆分和凑成两个阶段是连续两个animateTo写在一个函数中,但第二个动画总是迟一帧启动,导致圆点会先跳到中间态,再继续移动。

排查后发现,ArkUI的animateTo默认是异步执行的,连续调用时第二个动画会在第一个动画的初始状态基础上计算。解决办法是把第二个动画的触发移到第一个动画的onFinish回调里,形成严格的顺序执行,而不是靠时间猜测。

改造后的核心逻辑:

this.animateTo({ duration: 800, onFinish: () => { this.animateTo({ duration: 500 }) { // 此处更新第二个步骤的状态变量 } } });

这是动画序列的标准写法,虽然嵌套看着有点丑,但确实可靠。

4.2 组件复用导致的坐标残留

前面提到的keyGenerator问题,我再展开讲讲。ArkUI中ForEach组件在数据更新时会尽量复用已有组件实例,如果你只改了数据、不改变key,组件实例会被保留。这本来是性能优化,但在这个项目中,圆点从一个坐标移动到另一个坐标,如果组件实例被复用了,它的内部状态(尤其是offset)可能不会被正确重置,出现“上一个步骤残影”的视觉效果。

我最后的解决方案是:把currentStep作为ForEach的keyGenerator组成部分,每次步骤切换时强制所有圆点重新创建。本身就是几十个圆点,重建成本极低,但画面稳定很多。这提醒我:不要过分迷信组件复用,“无脑复用”有时反而会让动画状态混乱。

4.3 性能优化:减少无关组件刷新

在currentStep变化时,所有绑定此状态变量的组件都会被刷新。如果整个页面由一个大的build方法承载,那么每次步骤切换,布局、圆点、提示文字全都会重新计算。虽然40个圆点不多,但我仍然做了以下优化:

  • 把圆点区域拆成独立组件DotArea,只接收必要的坐标数组和颜色数组。
  • 步骤文字单独放在另一个子组件中,避免和圆点相互干扰。
  • 使用@Prop或@ObjectLink精确传递需要的数据,而不是把整个AdditionExpression实例传下去。

在真机测试(HarmonyOS NEXT API 12,Pixel系列测试机)下,动效流畅度稳定在60帧,动画过程中没有明显掉帧。说明这套方案对低算力设备也是友好的。

4.4 尺寸适配与横竖屏

因为是教学演示,我优先考虑横屏体验——横屏能提供更宽的左右空间布局圆点,避免竖屏下两边圆点太拥挤。我用了响应式布局:横向宽度小于600vp时,自动缩小圆点直径到32vp;大于600vp时,保持40vp并增加间距。这个适配通过MediaQuery实现,使用起来也简单:

@State private isLandscape: boolean = true; build() { if (this.isLandscape) { this.MainPanel(); } else { this.MainPanelPortrait(); } }

我在onPageShow里注册媒体查询监听,当屏幕方向变化时重新加载布局。这里有一个小坑:MediaQuery的监听回调不是立即触发的,页面首次加载时可能拿到错误的方向。解决办法是在onPageShow时主动调用一次getCurrentBreakpoint获取当前窗口尺寸,再决定使用哪种布局,不能完全依赖监听。

5. 一些提升演示效果的经验

项目做完之后,我又迭代了几个小功能,虽然不是核心,但对实际教学效果提升明显,也顺手分享一下。

5.1 步骤回放与手动控制

自动演示虽然省心,但孩子的学习节奏不一样,有的需要反复看某一步。所以我加了一个“分步模式”,用户可以点击“上一步/下一步”手动切换阶段。每次切换前,先调用resetPositions()把圆点恢复到初始布局,再通过状态机向前走一步。这样做的成本很低,因为状态机本身已经定义了每个步骤,手动切换只是重新赋值currentStep而已。

如果继续扩展,还可以做“单步循环”——让某一段动画(比如拆分)循环播放3遍,强化记忆。这个功能实现也不复杂,在onFinish里判断当前step是否等于循环目标,若是则重置步骤重新触发。

5.2 随机出题与难度自适应

为了便于课后练习,我加了一个随机出题功能,基于AdditionExpression生成算式时过滤掉“不需要进位”的题目。目前的过滤规则是:bigOperand + smallOperand <= 10的直接跳过。这样保证每次出现的都是真正需要凑十的题目。

难度分级上,我简单做了两档:初级只出9加几(如9+5、9+7),因为9离10最近,孩子最容易看到补数;高级再出8+几、7+几。对于不同基础的孩子,家长可以手动切换。这个分级思路不是凭空想的,是根据多位一年级老师的建议,先从“补口”最显眼的数开始,逐渐过渡到需要更多思考的数。

5.3 动画与语音解说的同步

目前版本里我用了文字提示,没有接入语音。但如果要做成自主学习工具,语音解说是刚需。简单方案是在每次状态切换时调用系统TTS接口读提示文本。HarmonyOS NEXT的 TTS 接口在API 12版本已经可用,接入成本不高。不过注意,TTS在朗读过程中如果切换步骤,需要先stop()再speak(),否则会排队朗读,造成内容错乱。

以上就是我做“20以内进位加法——凑十法动画演示”这个HarmonyOS应用实例的完整记录。从数据结构、状态机、动画实现到性能优化,整体思路就是一句话:“把教学过程拆成可控的状态,用动画把每个状态过渡讲清楚。” 我个人在实际操作中的体会是,教育类动画和产品类动画的取舍很不一样——不追求炫技,而是要把抽象逻辑以最直观的方式展示给孩子,这一点远比动画算法本身更重要。这套“状态机+属性动画”的方案,我已经复用到减法、乘法等系列里,如果你也在做儿童教育工具,可以按这个思路试试看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询