Flutter for OpenHarmony 实战之基础组件:第四十九篇 CustomPaint — 释放像素级的创意绘制力
Flutter CustomPaint 组件为 OpenHarmony 应用提供了强大的像素级绘制能力。本文通过实战演示了基础几何图形绘制和动态饼图实现,重点讲解了 CustomPainter 的核心方法、OpenHarmony 平台适配建议以及性能优化技巧。主要内容包括: 基础概念:CustomPaint 作为"画板",CustomPainter 作为"画家&quo

Flutter for OpenHarmony 实战之基础组件:第四十九篇 CustomPaint — 释放像素级的创意绘制力
前言
虽然 Flutter 提供了极其丰富的预置组件,但在追求独特视觉风格或处理动态数据可视化(如自定义图表、复杂的路径动画)时,传统的组件堆砌往往显得力不从心。这时,我们就需要掌握 Flutter 绘图系统的核心——CustomPaint。
在 Flutter for OpenHarmony 平台上,利用 Skia 引擎(或 Impeller)的强大性能,我们可以通过 CustomPaint 直接在画布(Canvas)上指挥每一个像素。本文将带大家从零开始,实战绘制几何形状与一个基础的动态饼图,开启鸿蒙应用的原生图形开发之旅。
一、CustomPaint 与 CustomPainter:画家与画板
- CustomPaint:是 Widget,它在组件树中占据位置,类似于一块“画板”。
- CustomPainter:是逻辑类,你在这里编写具体的绘图指令(画线、画圆等),它是真正的“画家”。
二、实战演练:绘制一个基础几何画板
2.1 定义 Painter
我们需要重写两个核心方法:
paint: 获取Canvas和Size,进行具体绘制。shouldRepaint: 决定何时需要重新重绘(通常与数据变化联动)。
class MyPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
// 1. 定义画笔
final paint = Paint()
..color = Colors.blue
..strokeWidth = 4.0
..style = PaintingStyle.stroke; // 描边模式
// 2. 绘制圆形
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);
// 3. 绘制线段
canvas.drawLine(Offset.zero, Offset(size.width, size.height), paint);
}
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
2.2 在组件中使用
CustomPaint(
size: const Size(200, 200), // 指定画布大小
painter: MyPainter(),
)

三、进阶:构建动态进度饼图
数据可视化是 CustomPaint 的主战场。
3.1 扇形绘制逻辑
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
final paint = Paint()
..style = PaintingStyle.fill
..isAntiAlias = true;
// 绘制蓝色扇形 (代表 70%)
paint.color = Colors.blue;
canvas.drawArc(rect, 0, 3.14 * 2 * 0.7, true, paint);
// 绘制灰色背景
paint.color = Colors.grey[200]!;
canvas.drawArc(rect, 3.14 * 2 * 0.7, 3.14 * 2 * 0.3, true, paint);
}

四、OpenHarmony 平台适配建议
4.1 抗锯齿与分辨率适配
鸿蒙设备屏幕像素密度极高(DPI)。
✅ 推荐方案:
在创建 Paint 时,务必设置 isAntiAlias = true。如果不开启抗锯齿,在鸿蒙的高清缩放屏幕下,曲线和圆形的边缘会有明显的颗粒感,影响界面的“高级感”。
4.2 性能监控与重绘控制
CustomPaint 如果在动画中频繁执行,会造成 CPU 压力。
💡 调优建议:
在 shouldRepaint 中进行精细化判断。只有当外部传入的“数据百分比”或“颜色配置”确实发生变化时才返回 true。对于不需要交互的复杂静态背景,建议使用 RepaintBoundary 进行包裹,强制 Flutter 对该层进行位图缓存,避免冗余重绘。
4.3 触控坐标转换
在鸿蒙应用的自定义图表中,用户点击了某个扇区。
✅ 最佳实践:
由于 CustomPainter 本身不具备事件监听能力。你需要将其嵌套在 GestureDetector 中,通过 onPanDown 获取到屏幕绝对坐标后,利用组件的 RenderBox 转换为 Canvas 内部的相对坐标,再进行碰撞检测逻辑判定(如计算点击点与圆心的距离)。
五、完整示例代码
以下代码演示了一个可以根据滑块实时改变填充比例的“动态百分比仪表盘”。
import 'package:flutter/material.dart';
import 'dart:math' as math;
class CustomPaintDemoPage extends StatefulWidget {
const CustomPaintDemoPage({super.key});
State<CustomPaintDemoPage> createState() => _CustomPaintDemoPageState();
}
class _CustomPaintDemoPageState extends State<CustomPaintDemoPage> {
double _value = 0.65;
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[900],
appBar: AppBar(
title: const Text('动态仪表盘实战'), backgroundColor: Colors.transparent),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 💡 视觉显示区
RepaintBoundary(
// 🎨 性能优化建议:对于独立绘制重灾区使用 RepaintBoundary
child: CustomPaint(
size: const Size(260, 260),
painter: DashboardPainter(_value),
),
),
const SizedBox(height: 100),
// 💡 交互控制区
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
children: [
Text("${(_value * 100).toInt()}%",
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold)),
Slider(
value: _value,
activeColor: Colors.cyanAccent,
onChanged: (v) => setState(() => _value = v)),
const Text("滑动以模拟鸿蒙设备性能负载",
style: TextStyle(color: Colors.white38, fontSize: 13)),
],
),
),
const SizedBox(height: 48),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text("返回演示首页")),
],
),
),
);
}
}
class DashboardPainter extends CustomPainter {
final double value;
DashboardPainter(this.value);
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 10;
// 💡 1. 绘制底层坐标刻度
final tickPaint = Paint()
..color = Colors.white24
..strokeWidth = 2;
for (int i = 0; i < 40; i++) {
double angle = (i * math.pi * 1.5 / 40) + math.pi * 0.75;
canvas.drawLine(
Offset(center.dx + math.cos(angle) * (radius - 5),
center.dy + math.sin(angle) * (radius - 5)),
Offset(center.dx + math.cos(angle) * radius,
center.dy + math.sin(angle) * radius),
tickPaint);
}
// 💡 2. 绘制进度电光圆环 (带渐变效果)
final progressPaint = Paint()
..shader = const LinearGradient(colors: [Colors.blue, Colors.cyanAccent])
.createShader(Rect.fromLTWH(0, 0, size.width, size.height))
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeWidth = 12;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius - 20),
math.pi * 0.75, math.pi * 1.5 * value, false, progressPaint);
// 💡 3. 绘制中心发光效果
final glowPaint = Paint()
..color = Colors.cyanAccent.withOpacity(0.1)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 20);
canvas.drawCircle(center, radius / 2, glowPaint);
}
bool shouldRepaint(covariant DashboardPainter oldDelegate) {
return oldDelegate.value != value;
}
}

六、总结
在 Flutter for OpenHarmony 的视觉无人区探索中,CustomPaint 是你最后的底牌。
- 无限自由度:只要 Path 能够到达的地方,画面就能呈现。
- 性能优先:在大屏鸿蒙设备上通过
RepaintBoundary进行分层存储是必修课。 - 万物皆可绘:从简单的进度环到复杂的股票 K 线图,掌握了坐标系与画笔,就掌握了 UI 设计的终极话语权。
📦 完整代码已上传至 AtomGit:flutter_ohos_examples
🌐 欢迎加入开源鸿蒙跨平台社区:开源鸿蒙跨平台开发者社区
更多推荐



所有评论(0)