做一个数学可视化功能时,最容易被低估的需求就是分段函数绘制。我刚接到这个功能时也觉得不难:不就是在画布上取点、连线吗?真正做下去才发现,分段函数不是“把一个普通函数在固定区间里截断”那么简单——定义域边界怎么处理,断点左右两边是否真的相连,函数值趋近无穷时怎么避免画出恐怖的竖线,屏幕坐标系方向和数学坐标系反过来又该怎么换算。这些细节堆在一起,哪怕只是一个非常简单的高中分段函数,也能把第一次实现的人折磨得够呛。
这个实例里的主体是 HarmonyOS 应用中的分段函数绘制,我用的方案是 ArkTS + ArkUI 自带的 Canvas 画布,没有引入任何第三方图表库。整个实现包括:分段函数的数据建模、世界坐标到屏幕坐标的变换、坐标轴与网格绘制、逐段采样连线、跳跃间断点处理,然后配合缩放和平移手势让用户能交互式查看图像。如果你正在做鸿蒙版课件、学习工具或者工程计算类的应用,这个实例的实现思路和代码骨架基本可以直接复用。
1. 函数曲线不是图表:这个需求为什么值得单独做一次设计
1.1 真实需求:不是画图,是让学生把变化规律看出来
我这次做分段函数,起因是一个课堂演示工具:老师输入一个分段函数,学生能直接在手机或平板上看到曲线形状,并且能手动放大某个局部,观察“分段点附近函数到底怎么变化”。
最开始我差点把“普通函数绘图”和“分段函数绘图”当成同一件事处理,后来发现两者最大的不同在于:普通函数往往定义域完整,遍历一遍 x 坐标基本没问题;分段函数则由若干段组成,每一段有自己的定义域,段与段之间可能有间隙、有跳跃、有单独成立的端点。如果只是把所有段拼在一起按连续函数去采样画线,结果多半会出现一条莫名其妙的斜线,比如函数在 x=0 处没有定义,前一秒曲线还在上方,后一秒直接掉到下方,中间被错误连起来。
所以最核心的设计矛盾不是“用什么曲线颜色”,而是“怎么让图像在数学上依然严谨”。既然面向的是课堂场景,任何一条不该出现的连线都会让学生产生误解,这点必须从数据建模阶段就开始规避。
1.2 普通函数绘制思路为什么套不上分段函数
如果是一个像 y = x² 这样的完整定义域函数,最简单的绘制思路是:确定可视范围的最小 x 和最大 x,从 minX 到 maxX 均匀取几百个点,每个点都计算 y,然后把屏幕坐标连起来。
这个思路对分段函数有两个致命问题:
第一,分段点不一定落在采样点上。假设采样步长是 0.1,而分段点在 0.37,那么没有任何一个采样点正好在分界处。曲线经过分界点时,两侧表达式已经切换了,但绘制代码浑然不知,仍然会把前一个采样点和后一个采样点直接连成直线,这样在分界点附近会出现误差极大的斜线,个别情况下甚至画出跨越整个屏幕的线段。
第二,分段函数可能在某个局部没有定义。例如一个函数只定义在 [-6, -1) 和 (1, 6],采样范围如果从 -6 一直扫到 6,中间 [-1,1] 这一段没有函数值。如果计算返回 NaN 或者某个约定值,而绘制代码没有显式处理,那些没有定义的区域也可能被连线覆盖。
这两点是“函数图像”区别于“折线图”的核心差异。普通折线图的数据点是真实存在的测量值或计算值,中间缺失区域本来就该留空;而分段函数里“这段没有定义”不是数据缺失,是数学表达式的天然结构。绘制逻辑必须明确知道每个定义域的起点和终点,而不是靠采样时偶然跳过某个值。
1.3 为什么不引入现成的图表库
HarmonyOS 生态里也有不少图表库,比如基于 Canvas 封装的统计图表组件,画折线图、柱状图、饼图都很方便。但我这次没有用,一个很重要的原因是:图表库面向的是“数据可视化”,它通常假设横轴是一组离散的类别或者等间隔的时间点,曲线只是把已有数据点连起来。分段函数则需要一个“随时计算任意 x 的函数值”的数学引擎,而且要能处理定义域、开闭区间、无穷大这些不属于常规图表数据模型的东西。
另一个原因是定制成本。课堂上可能要放大到某个分段点附近看极限行为,此时坐标轴刻度需要动态重算、网格线需要随缩放变化、曲线要重新按当前可视范围采样。把这些逻辑硬塞进通用图表库,很多时候比直接用 Canvas 重写一遍还麻烦。考虑下来,索性从 0 自己维护渲染管线,所有行为都可控。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先把坐标系搬进 ArkTS:画一切函数的基础
2.1 为什么屏幕坐标系会和数学坐标系“打架”
所有刚接触 Canvas 绘制的人都会遇到这个问题:数学坐标系的原点通常在图形中间,y 轴向上为正;而屏幕坐标系的原点在左上角,x 轴向右为正,y 轴向下为正。
如果你直接把数学里的 (x, y) 当作 Canvas 的像素坐标画上去,画出来的图像要么上下颠倒,要么整体偏移到屏幕右上角。所以第一步不是写绘制代码,而是写一个负责坐标系转换的 Viewport 类。这个类本质上是维护“世界坐标系”到“屏幕坐标系”之间的映射关系,后续所有绘制都只跟世界坐标打交道,只有真正调用 Canvas API 时才转成屏幕坐标。
我在工程里是这样定义的 Viewport 核心内容:
- centerX、centerY:当前视口中心对应的世界坐标;
- pixelsPerUnit:每个世界单位对应多少个屏幕像素,相当于缩放比例;
- canvasWidth、canvasHeight:画布的宽高。
有了这三个要素,就能实现世界坐标和屏幕坐标的双向转换。双向转换非常重要,因为绘制网格线时需要把世界坐标的整数刻度变成屏幕坐标,而手势识别拿到的是屏幕坐标的拖动偏移,又需要反向换算成世界坐标的移动量。
2.2 Viewport 类的设计与转换实现
在 ArkTS 里,我建议把 Viewport 单独写成一个类,最好不要直接散落在页面组件里,因为后面缩放、平移、Canvas 重绘都要访问它。下面是我整理过的核心代码结构:
typescript复制export class Viewport {
centerX: number = 0;
centerY: number = 0;
pixelsPerUnit: number = 60;
canvasWidth: number = 0;
canvasHeight: number = 0;
worldToScreenX(worldX: number): number {
return this.canvasWidth / 2 + (worldX - this.centerX) * this.pixelsPerUnit;
}
worldToScreenY(worldY: number): number {
return this.canvasHeight / 2 - (worldY -
