
分数这个东西教起来容易讲明白很难。这是我在“HarmonyOS应用实例”系列里做到的第92个练手项目分数的意义演示器。做它的起因很简单我家孩子学“分数初步认识”时教材上的静态图怎么都理解不了“单位1被平均分成几份”这件事。所以我决定用HarmonyOS NEXT的原生能力做一个能拖、能切、能实时反馈的演示工具。项目完整跑在API 12对应DevEco Studio 5.0.0SDK版本5.0.0(12)上核心技术是ArkTS ArkUI声明式开发核心绘图逻辑没有借助任何第三方图表库一个Canvas画布加几个状态变量就把圆饼图、动态等分、分数换算全打通了。这个应用适合三类人第一是小学中高年级的学生用来建立分数与整体比例之间的直觉第二是辅导作业的家长遇到“为什么二分之一大于三分之一”这类问题时随手演示第三是老师可以把屏幕投到大屏上做课堂互动。文章会完整拆开我的设计思路、等分圆的绘图算法、状态管理方案和踩坑记录你照着在DevEco Studio里操作就能复现一个能跑、能演示的完整应用实例。1. 项目思路分数的意义到底难在哪1.1 小学生卡住的地方往往不是计算而是概念很多孩子能把“分母表示平均分的份数分子表示取出的份数”背得滚瓜烂熟但做题时照样把分子分母的位置写反。问题出在分数本质上是一个过程概念不是一个静态结果。教材上的圆被分成八块、涂了三块那是一个“已经分好”的结论学生看不到“为什么必须是平均分”“为什么每一份大小必须相等”这些关键过程。我陪孩子写作业时最典型的场景是问他“三分之一和四分之一谁大”他能脱口而出“四分之一大因为4比3大”。这个错误太经典了。根源在于孩子把分母当成了“数量”而不是“切割的细度”。如果能让分母实时变化孩子亲眼看到同一个圆被切成3份和切成4份后每一块大小的差异认知立刻就会扭转。这就是“分数的意义演示器”的核心定位把抽象的比例关系变成手指可拖动的视觉反馈。1.2 演示器的功能定位与目标用户这个项目最需要注意的地方是克制。它是一个“演示器”不是一套完整的教学系统也不是刷题工具。我一开始也想塞进比较大小、分数加减这些功能后来全部砍掉了。保留的功能只围绕一件事理解“整体被平均分成若干份取其中几份”的对应关系。目标用户画像分三类交互设计要同时兼顾。第一类是四年级左右的学生他们要的是直观、好玩所以滑块拖动的反馈必须即时颜色要鲜艳第二类是家长他们要的是省心所以预设分数按钮要给足最好一键切到“二分之一”“三分之一”“四分之三”这些高频场景第三类是老师他们要的是稳定所以绘图不能闪、文字不能跳投屏时要看得清楚。1.3 功能清单先列需求再做开发我写代码之前先在备忘录里列了需求清单反复删减后留下这些圆饼图等分显示份数跟随分母变化高亮份数跟随分子变化未选中的部分用浅色区分两个滑块分别控制分子和分母分母范围锁定在2到12同步显示分数值、最简分数、小数、百分比预设常用分数快捷按钮方便课堂演示重绘过程无闪烁切换数值时文字不跳动分母上限为什么定成12而不是20我实测之后发现超过12份以后每个扇区的弧长太小在手机竖屏尺寸上间隔线叠在一起高亮色和浅色之间的对比区域变得很窄教学演示反而容易看花眼。这个约束属于典型的“功能克制”先做精再做多。2. 技术选型为什么坚持用ArkUI自绘Canvas2.1 开发环境与API版本工程创建用的是HarmonyOS NEXT的Stage模型语言必须是ArkTS不是普通TypeScript很多JS里随意的写法在ArkTS里会被编译检查拦下来比如不能用any类型。API版本我选的是12也就是SDK 5.0.0(12)这个版本里Canvas、Slider、Watch这些能力都已经稳定。早期API 9上我也试过类似的项目但Slider的回调签名和Canvas的绘制能力都有差异迁移起来稍微要改几个地方后面第五节会专门讲。新建工程时模板选“Empty Ability”就行工程结构默认会生成entry/src/main/ets/pages/Index.ets我们所有的代码基本都写在这个文件里。真机调试需要先在AppGallery Connect那边配置签名如果只是本地预览器跑效果可以跳过签名步骤。2.2 图片方案、Web组件方案与自绘方案的对比同类效果至少有三种实现路径我逐一分析后选了自绘Canvas。第一种是用静态图片提前切好几张等分图。优点是实现最简单缺点是每改一次分母就要换一张图且图片放大后发虚。第二种是用Web组件加载HTML页面用浏览器里的SVG或者Canvas渲染。这样做跨平台很爽但HarmonyOS的Web组件封装了一层JavaScript桥接通信滑块变化要频繁通知Web端重绘手势响应会偶发掉帧而且包体明显变大。第三种就是ArkUI的Canvas自绘ArkTS代码直接调用CanvasRenderingContext2D的API画扇形数据变化时用Watch触发重绘链路最短性能最可控。三套方案实测数据我简单列一下静态图片切图平均响应150毫秒Web方案在SwiftShader渲染下拖动滑块会有肉眼可见的滞后自绘Canvas方案每次重绘不超过5毫秒基本跟随手指。这也是我最终选择自绘的根本原因。2.3 状态管理方案State WatchArkUI声明式开发里最核心的状态管理方式就是State装饰器。我在页面里定义了两个关键状态变量fenMu分母和fenZi分子它们直接绑定Slider滑块的值。当用户拖动滑块时变量更新界面上的Text文本会自动刷新这是声明式框架自带的能力。但Canvas不会自动重绘。画布内容的绘制是在onReady回调里执行的这个回调在组件初始化时只执行一次后面状态再怎么变画布上的饼图还是老样子。解决办法是用Watch装饰器监听状态变量State Watch(onParamChange) fenMu: number 6;。这样每次fenMu或fenZi变化时onParamChange方法会被自动调用我在里面手动触发drawPie()完成重绘。为什么不用全局状态管理或者Observed因为当前页面的状态维度足够简单两个数字变量而已引入全局Store反而是过度设计。教学类工具尤其要控制复杂度。3. 界面布局与控制区交互设计3.1 页面三段式布局整体页面我用一个纵向Column分成三个区域顶部信息区、中间绘图区、底部控制区。顶部显示的是当前分数值、约分结果、小数和百分比用几个Text组件横向排开中间是一个Canvas宽度撑满父容器高度固定为280vp底部是两组Slider和快捷按钮。这里有一个布局细节值得注意Canvas的高度不能用百分比写死。如果设置为height(60%)在键盘弹起或折叠屏切换时画布会被压缩变形圆弧变成椭圆。固定高度280vp配合aspectRatio(1)或者代码里动态计算半径才能保证圆始终是正圆。我最终选择固定高度半径由代码根据实际宽高取较小值的一半再乘0.7。3.2 分子分母双滑条的联动约束两个Slider一个控制分母一个控制分子。分母的范围是2到12步长1分子的范围是1到当前分母值步长1。这里最容易被忽略的是联动关系当分母从6切到4时如果当前分子是5必须立刻把分子压回4否则会出现“分子大于分母”的非法分数。代码里我这样处理分母滑块的onChange回调Slider({ value: this.fenMu, min: 2, max: 12, step: 1 }) .onChange((value: number) { this.fenMu value; if (this.fenZi this.fenMu) { this.fenZi this.fenMu; } })分子滑块则动态绑定最大值Slider({ value: this.fenZi, min: 1, max: this.fenMu, step: 1 }) .onChange((value: number) { this.fenZi value; })这种约束用数学语言说就是1 fenZi fenMu每次分母变化后做一次越界裁剪。实际交互体验上用户拖动分母滑块从6到3分子的圆圈高亮块会瞬间从5块缩到3块这个“由多变少”的视觉反馈本身就是教学的一部分孩子能直观看到“整体切得更少了但取的份数也被限制了”。3.3 快捷分数按钮的设计细节除了滑块我还加了几个预设按钮1/2、1/3、2/3、1/4、3/4。按钮布局我用Flex换行排列每个按钮宽度约80vp。点击事件的逻辑很直接Button(1/2) .onClick(() { this.fenZi 1; this.fenMu 2; })为什么需要预设按钮因为在课堂场景下老师没有时间慢慢拖滑块一个“1/2”按钮点下去饼图立刻变成两等份并高亮一块讲解节奏不会被打断。实际从这个角度说快捷按钮对课堂演示的体验提升比滑块还明显。4. 绘图核心把一个圆等分并高亮显示4.1 等分圆的几何原理与坐标计算画饼图之前要先理解等分的几何逻辑。一个完整圆是360度用弧度表示就是2π。如果分母是n每一份对应的弧度就是2π / n。绘制时从圆心出发沿着圆周画弧就能得到一个扇形。关键在于起始角度。默认的0弧度在单位圆上指向右侧但教学场景里我们希望第一份从正上方开始顺时针排布更符合直觉。所以起始角度设置为-Math.PI / 2也就是-90度。圆心坐标取画布宽高的一半半径则取Math.min(width, height) / 2 * 0.7乘0.7是预留出文字标注和描边线宽的空间否则靠近边缘的扇形会被裁掉一半。用表格整理一下核心几何参数方便你抄作业参数取值说明圆心Xwidth / 2画布宽度一半圆心Yheight / 2画布高度一半半径Rmin(width, height) / 2 * 0.7预留空间起始角度-Math.PI / 2从正上方开始每份弧度2 * Math.PI / fenMu平均等分4.2 扇形绘制、高亮与描边的代码实现绘制过程分成三步先画所有扇形的浅色底再画高亮部分的橙色块最后统一描白边。浅色底我用#EEEEEE高亮色用#FF6B35白色描边宽度2vp。这个配色在高亮和未选中之间形成了强烈对比色弱人群也能分辨。完整绘制方法如下drawPie() { const ctx this.context; const width ctx.width 0 ? ctx.width : 360; const height ctx.height 0 ? ctx.height : 280; const cx width / 2; const cy height / 2; const radius Math.min(width, height) / 2 * 0.7; const startAngle -Math.PI / 2; const perAngle (2 * Math.PI) / this.fenMu; // 1. 先清空画布 ctx.clearRect(0, 0, width, height); // 2. 绘制每个扇区 for (let i 0; i this.fenMu; i) { const angleStart startAngle i * perAngle; const angleEnd startAngle (i 1) * perAngle; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, radius, angleStart, angleEnd); ctx.closePath(); if (i this.fenZi) { ctx.fillStyle #FF6B35; } else { ctx.fillStyle #EEEEEE; } ctx.fill(); // 描白边让等分线清晰可见 ctx.strokeStyle #FFFFFF; ctx.lineWidth 2; ctx.stroke(); } }这段代码运行后屏幕上会出现一个被radial线分割成n份的圆其中前fenZi份被涂成橙色。当滑块变化时drawPie()会被重新调用通过clearRect清空画布并重画实现了动态效果。4.3 分数、小数、百分比与最简分数的同步输出绘图之外信息展示也要同步。我这边的做法是在顶部放一排Text第一个显示当前分数比如2/6第二个显示约分后的最简分数1/3第三个显示小数0.33第四个显示百分比33%。其中约分需要自己写一个最大公约数算法function gcd(a: number, b: number): number { while (b ! 0) { const temp b; b a % b; a temp; } return a; }调用方式很简单const g gcd(this.fenZi, this.fenMu); const simpleFenzi this.fenZi / g; const simpleFenmu this.fenMu / g;。然后拼字符串显示。小数的显示要控制精度我保留两位小数用toFixed(2)百分比保留整数用Math.round((this.fenZi / this.fenMu) * 100) %。有一个细节特别提醒toFixed返回的是字符串在ArkTS里拼接文本时直接当string用没问题但如果你要再参与计算记得先parseFloat转回来。我早期在百分比计算里直接拿字符串拼数字出现过类型不匹配的编译报错。5. 完整实操从新建工程到跑通真机5.1 创建HarmonyOS工程与基础骨架第一步打开DevEco Studio 5.0.0选择“File - New Project”模板选“Empty Ability”Language确认是ArkTS最低API版本选12或者直接选5.0.0(12)。工程创建完成后项目结构会自动生成entry模块和pages/Index.ets页面文件。第二步清空Index.ets的默认模板代码。我们需要自己引入Canvas和状态变量。页面结构大概是Entry Component struct Index { State Watch(onParamChange) fenMu: number 6; State Watch(onParamChange) fenZi: number 2; private settings: RenderingContextSettings new RenderingContextSettings(true); private context: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); build() { Column() { // 顶部信息区 // 绘图区 Canvas // 底部控制区 } } }这里有一个ArkTS的坑private settings和private context必须在RenderingContextSettings构造时一起初始化不能在aboutToAppear里延迟创建否则Canvas的onReady拿到的上下文会是空的。5.2 核心绘制与交互代码解读完整代码我在上文已经拆开了核心部分这里补充页面骨架里的Canvas标签写法Canvas(this.context) .width(100%) .height(280) .onReady(() { this.drawPie(); })onReady是Canvas的初始化完成回调在这个时机调用首次绘制。之后每次状态变化由Watch生命周期的onParamChange方法自动调用drawPie()不需要再手动绑定。onParamChange方法的完整逻辑如下onParamChange() { this.drawPie(); // 文本区域的Text因为绑定了fenMu和fenZi会自动刷新 }为什么这个方法只需要重绘Canvas而不需要手动更新文本因为ArkUI的响应式机制已经帮我们做好了Text(this.fenZi / this.fenMu)这类表达式会在fenZi或fenMu变化时自动重新求值。这是声明式开发舒服的地方数据驱动UI。5.3 真机验证与参数调优记录项目跑通后我在真机上做了几轮调优。第一轮发现的问题是圆的半径偏大6等分时扇区外侧的白色描边有一部分超出了Canvas边界视觉上圆被“切掉”了一小条。我把半径系数从0.85降到0.7问题解决。第二轮是折线边缘的锯齿感。Canvas绘图默认是开了抗锯齿的但描边线宽如果小于1vp中低端机型上还是会出现毛边。我把线宽从1改到2并且把描边颜色设为纯白视觉上分割线更锐利。第三轮是文字区域抖动。原来的小数位显示三位用户在拖动滑块过程中数字无限循环比如1/6会显示成0.166666667位数一变宽度一直在跳。我统一改成toFixed(2)后抖动消失。这个细节折磨了我挺久最后发现是显示位数的问题而不是布局问题。6. 常见问题与排查技巧实录6.1 画面不刷新状态变了但饼图没动这是新手最容易卡住的问题。症状是滑块拖动时顶部的数字在变但Canvas里的饼图纹丝不动。原因几乎都是没有把Watch装饰器加到状态变量上或者onParamChange里没有调用drawPie()。排查思路从最底层开始先确认状态变量是否确实在变化看Text有没有变再确认Watch是否挂上在onParamChange里加一行console.log最后确认drawPie()是否真的执行同样加日志。三步走下来问题定位通常不超过三分钟。6.2 滑条边界与分子分母联动异常另一个高频问题是分子滑块的max没有动态绑定fenMu导致用户把分母从6拖到3时分子滑块还在允许5份的状态实际饼图却只画3份出现“分数显示2/3但滑块位置不对”的错位。解决方法是给分子Slider的max属性直接传this.fenMu并在分母onChange里做一次Math.min裁剪。两个动作缺一不可。6.3 预览器Canvas渲染不兼容DevEco Studio的Previewer在API 12版本下对Canvas的支持其实还有不少小毛病。我遇到过一次预览器里饼图能显示但滑动滑块时画面卡顿明显偶尔还会出现整块画布白屏。排查后确认是预览器渲染引擎的软绘制问题真机上完全正常。我的建议是预览器只看布局结构Canvas的最终效果一律以真机为准。如果你的场景必须依赖预览器做展示可以考虑在Canvas区域套一个固定尺寸的容器减少动态布局带来的重算压力。6.4 文字重叠与样式跳动顶部信息区的多个Text并排展示时容易在数字位数变化时相互挤压。我的处理方式是给每个Text设置固定宽度比如分数区width(120)、小数区width(80)而不是让它们自适应内容宽度。另外小数统一保留两位百分比统一取整从根本上避免位数抖动。6.5 API 12版本迁移的几个坑如果之前写过API 9版本的应用迁移到API 12时至少要注意三处。第一处Slider的onChange回调在API 12里提供了更丰富的SliderChangeMode参数旧版本忽略这个参数没问题但如果调用了旧的位置参数编译会报错。第二处Canvas的arc方法参数顺序在API 12没有变化但某些旧API的写法已经被废弃比如用drawArc代替这个不用记按新模板写就行。第三处构建打包时targetSdkVersion要显式设为12或5.0.0(12)否则部分新特性不会生效。我在实际做这个项目时最大的感受是教育类应用的交互反馈要“快”和“准”。快说的是滑块拖动时画面必须跟手准说的是分数、小数、百分比三者必须完全对应不能出现2/6显示成0.33却没有同步成1/3的约分结果这种低级错误。Canvas自绘方案在这两点上都经住了考验。后面我还打算给这个演示器加上两个扩展一个是“单位长度条”模式用一根数轴展示分数在0到1之间的位置另一个是分数大小自动排序模式输入多个分数后按大小排列成柱状图。有需要的话你也可以在上述代码基础上直接加。做这类工具类应用最忌讳的就是功能堆砌先把一个分数概念演示透比做出五个半成品有意义得多。