最近在做内部合同审批流程的移动化改造,需要支持用户在手机和办公平板上在线签署意见,最终定了 Flutter 来做跨平台 UI,顺手把鸿蒙设备的适配也打通了。整个项目最核心的一个模块就是手写签名板,说白了就是一个能拿手指或触控笔写字画画的画布,配合撤销、清除、导出图片这些能力,把用户的手写笔迹沉淀成可归档的图片或数据。这篇文章不打算讲一堆思想层面的东西,就围绕“Flutter 跨平台 + 鸿蒙适配 + 手写签名板”这条线,把我从需求拆解到实际编码、再到鸿蒙设备跑通的完整过程分享一下。想自己写签名板组件、或者正在做 Flutter 鸿蒙化改造的朋友,可以直接拿走里面的思路和代码。
1. 项目启动前,先想清楚签名板的架构和边界
先别急着写代码。签名板看似简单,但“能在屏幕上画出线条”和“能作为正式签核组件”是两个量级。我在前期把需求拆成了两件事:第一,手写体验必须接近纸笔,不能出现断线、抖线、延迟高的问题;第二,最终产物必须能导出成图片或矢量数据,方便后续归档、比对、存证。这个定位决定了后面所有代码的写法。
1.1 为什么这个场景适合 Flutter 跨平台
如果只做单一平台,原生是首选,但我们的使用场景是:领导在 Android 手机上签、业务员在 Windows 平板上签、部分办公区用鸿蒙设备,还要考虑后续把签名能力嵌到 Web 端 OA 里。这种情况下用 Flutter 一套代码覆盖全端,比维护三套原生组件划算得多。
手写签名板恰好是 Flutter 很擅长的场景:它是轻交互、重绘制的 UI 组件,不涉及复杂系统 API,主要工作都在渲染层完成。Flutter 的 CustomPaint 直接对接 Skia 渲染引擎,在 Android、Windows、Web 上都能保持一致的绘制行为,这对“签名笔迹必须标准化”的合同场景来说非常重要。如果是 ArkUI 或原生 View 各自实现一套,光是抗锯齿和曲线算法在每个平台上调统一就是一笔不小的成本。
1.2 签名板的整体功能清单与实现分层
在动手前我列了一份最小可用功能清单,只保留签名场景真正会用到的东西:
- 手写绘制:支持手指、电容笔、鼠标三种输入方式。
- 清除画布:一键清空所有笔迹。
- 撤销上一步:误操作时回退一个笔画。
- 导出图片:把画布区域渲染成 PNG 图片,可配置透明背景或白底。
- 轨迹数据版本:保存笔迹坐标序列,便于二次渲染或做动态重放。
这里我没有把“橡皮擦”放进第一版。原因很直接:签名场景不需要局部擦除,误签了用撤销就行;橡皮擦在 Flutter 上要配合 saveLayer 和 BlendMode.clear 使用,复杂度和性能成本都不低,签核场景的收益却很小。功能越克制,代码越稳。
在架构上,我把组件拆成四层:
- 输入层:负责接收 PointerEvent / Gesture 事件,区分手指、鼠标、笔,并提取坐标与压感。
- 数据层:把每一条笔画保存为独立的 Stroke 对象,存储点位数组、笔宽、颜色等属性,支撑撤销、重做、序列化。
- 渲染层:基于 CustomPaint + CustomPainter,把笔画数据转换为平滑的 Path 并绘制。
- 导出层:用 RepaintBoundary 把画布区域截图为位图数据,输出成 base64 或文件。
每层只做一件事,后续要扩展“笔迹加密”“PDF 印章合成”“多人会签”等功能时,直接替换对应层就行。
1.3 关于“鸿蒙适配”的三个前置认知
在真正碰鸿蒙之前,我踩过的坑基本都源于对“Flutter 鸿蒙适配”这几个字的误解,先敲三个认知:
第一,Flutter 官方仓库默认并不能直接构建鸿蒙 HAP,社区和硬件厂商维护的 ohos 分支补齐了这部分能力。也就是说,鸿蒙适配发生在 Flutter SDK 和 Engine 层,而不是业务代码层。
第二,在业务代码层面,签名板是纯 Dart 实现、只依赖 Flutter 自带的 rendering 能力,不依赖 Android 或 iOS 插件,这让它成为适配鸿蒙最顺利的一类项目。如果你的业务还用了 shared_preferences、path_provider 这些常用插件,就要额外确认是否有鸿蒙版本支持。
第三,鸿蒙设备和 Android 设备在触控事件、屏幕 density、字体渲染上存在细节差异,跨平台方案跑通很容易,但“跑得和纸笔一样顺手”需要单独调参。所以鸿蒙端既不是改几行代码就能搞定,也没有想象中那么可怕,核心工作集中在环境搭建和事件参数调整上。
如果一开始就把这三点想清楚,后面不会花太多时间在错误方向上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手写签名的核心交互:从手势到平滑笔迹
签名板的技术含量几乎全集中在“如何把触摸轨迹变成一条流畅、真实、贴近纸笔的曲线”。这一节我按从输入到渲染的链路,把每个环节的关键点拆开讲。
2.1 手势事件怎么接:GestureDetector 还是 Listener
很多人习惯直接用 GestureDetector 的 onPanStart/onPanUpdate/onPanEnd,但在手写签名场景里,我更推荐用 Listener 配合 PointerDownEvent / PointerMoveEvent / PointerUpEvent。原因是:
GestureDetector 的 pan 手势带有滑动阈值和手势竞技场判定,在某些情况下会出现“快速落笔后第一个点被吞掉”的现象;而 Listener 直接接收原始指针事件,每个触摸点都能拿到坐标和压力信息,更适合精密的绘制需求。不过 Listener 需要自己处理多点触控和事件类型判断,代码量会多一点。
实际代码中,我会在 PointerDownEvent 里判断 event.kind
