在这里插入图片描述

Flutter for OpenHarmony 实战之基础组件:第四十九篇 CustomPaint — 释放像素级的创意绘制力

前言

虽然 Flutter 提供了极其丰富的预置组件,但在追求独特视觉风格或处理动态数据可视化(如自定义图表、复杂的路径动画)时,传统的组件堆砌往往显得力不从心。这时,我们就需要掌握 Flutter 绘图系统的核心——CustomPaint

Flutter for OpenHarmony 平台上,利用 Skia 引擎(或 Impeller)的强大性能,我们可以通过 CustomPaint 直接在画布(Canvas)上指挥每一个像素。本文将带大家从零开始,实战绘制几何形状与一个基础的动态饼图,开启鸿蒙应用的原生图形开发之旅。


一、CustomPaint 与 CustomPainter:画家与画板

  • CustomPaint:是 Widget,它在组件树中占据位置,类似于一块“画板”。
  • CustomPainter:是逻辑类,你在这里编写具体的绘图指令(画线、画圆等),它是真正的“画家”。

二、实战演练:绘制一个基础几何画板

2.1 定义 Painter

我们需要重写两个核心方法:

  • paint: 获取 CanvasSize,进行具体绘制。
  • 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 是你最后的底牌。

  1. 无限自由度:只要 Path 能够到达的地方,画面就能呈现。
  2. 性能优先:在大屏鸿蒙设备上通过 RepaintBoundary 进行分层存储是必修课。
  3. 万物皆可绘:从简单的进度环到复杂的股票 K 线图,掌握了坐标系与画笔,就掌握了 UI 设计的终极话语权。

📦 完整代码已上传至 AtomGitflutter_ohos_examples

🌐 欢迎加入开源鸿蒙跨平台社区开源鸿蒙跨平台开发者社区


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐