Flutter 像素级还原实战:用 CustomPaint 与 Bezier 曲线手绘精致图针

🎯 前言

在 Flutter 开发中,我们绝大部分时间都在拼装内置的 Widget。然而,当设计师丢给你一个极具设计感的异形组件(例如地图应用中带有优美弧度的位置图针)时,仅仅靠 Container 和 Clip 是无法实现像素级还原的。

这时候,就该请出 Flutter 渲染层的大杀器 ------ CustomPaint 和 Canvas API。本文将带你实战:如何用代码手绘一个带有贝塞尔曲线和阴影的精美图针。


🎨 1. 核心武器库:CustomPaint 与 CustomPainter

在 Flutter 中,自定义绘制的基本结构如下:

less 复制代码
CustomPaint(
  size: Size(100, 150), // 指定画布大小
  painter: MapPinPainter(), // 负责背景和主体的绘制
  foregroundPainter: null, // 可选:负责前景绘制
  child: Center(child: Text('针')), // 可以在图针上放普通的 Widget
)

核心逻辑都在 MapPinPainter 中,我们需要继承 CustomPainter 并重写两个方法:

  • paint(Canvas canvas, Size size): 所有的绘制魔法都在这里。
  • shouldRepaint: 决定何时需要重绘以优化性能。

📐 2. 坐标系与 Path 的魔法

Flutter 的 Canvas 坐标系以左上角为 (0, 0),向右为 X 轴正向,向下为 Y 轴正向。 为了画一个图针,我们需要使用 Path 对象,它允许我们组合直线和曲线。

图针的几何解构

一个经典的图针可以拆解为:

  1. 上半部分:一个大圆弧。
  2. 下半部分:两条收敛的曲线交汇于底部尖端。

贝塞尔曲线 (Bezier Curve)

我们使用二阶贝塞尔曲线 quadraticBezierTo(x1, y1, x2, y2)。

  • (x1, y1) 是控制点,决定了曲线的弧度。
  • (x2, y2) 是曲线的终点。

💻 3. 像素级绘制实战代码

直接上代码,我们将使用一个完整的 Painter 来绘制带有渐变色和阴影的图针。

arduino 复制代码
import 'package:flutter/material.dart';
​
class MapPinPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final width = size.width;
    final height = size.height;
​
    // 1. 绘制阴影 (利用 Path 生成对应的阴影层)
    final path = Path();
    // 起点:左侧圆弧和曲线交界处
    path.moveTo(0, width / 2);
    // 上半部分大半圆
    path.arcToPoint(
      Offset(width, width / 2),
      radius: Radius.circular(width / 2),
      clockwise: true,
    );
    // 右侧收敛到下方尖端 (二阶贝塞尔)
    path.quadraticBezierTo(width, height * 0.75, width / 2, height);
    // 左侧由尖端收敛回原点
    path.quadraticBezierTo(0, height * 0.75, 0, width / 2);
    path.close();
​
    // 添加外部发光阴影
    canvas.drawShadow(path, Colors.black, 8.0, false);
​
    // 2. 配置主体的画笔 (使用渐变色)
    final paint = Paint()
      ..shader = LinearGradient(
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
        colors: [Colors.redAccent, Colors.deepOrange],
      ).createShader(Rect.fromLTWH(0, 0, width, height))
      ..style = PaintingStyle.fill
      ..isAntiAlias = true;
​
    // 绘制图针主体
    canvas.drawPath(path, paint);
​
    // 3. 绘制中间的白色小镂空圆 (设计细节)
    final innerCirclePaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;
    canvas.drawCircle(Offset(width / 2, width / 2), width / 6, innerCirclePaint);
  }
​
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // 静态图形,不需要重绘
    return false;
  }
}

如何使用

less 复制代码
Center(
  child: CustomPaint(
    size: Size(80, 120),
    painter: MapPinPainter(),
  ),
)

⚡ 4. 性能优化与避坑指南

  1. 避免在 paint 方法中创建对象 :上面的示例为了直观,在 paint 内部创建了 Paint 对象。在实际的高帧率动画中,应将 Paint 提取为类的成员变量,避免每帧重复分配内存。
  2. 正确实现 shouldRepaint :如果你的图针颜色或形状会随状态改变,请在这个方法中对比新旧属性;如果纯静态,直接返回 false 即可。
  3. 抗锯齿 :确保 Paint.isAntiAlias = true,否则曲线边缘会有明显的像素狗牙。

📝 总结

通过 CustomPaint 和底层的 Canvas API,我们摆脱了内置 Widget 的束缚。掌握了 Path 和贝塞尔曲线后,无论是图针、波浪效果还是复杂的统计图表,在 Flutter 中都不再是难题。 这就体现了 Flutter 最强悍的地方:它不仅仅是一个 UI 框架,更是一个高性能的 2D 渲染引擎!

相关推荐
ShineWinsu8 分钟前
对于JavaScript:JavaScript基础语法的解析
开发语言·前端·javascript·面试·web·基础语法·循环
月夏16 分钟前
Vue3 转 React:18 项对照表 + 5 个必踩的坑(附 7 天上手路线)
前端
广州华水科技24 分钟前
单北斗GNSS位移监测系统的应用与维护解析
前端
梦曦i1 小时前
uni-storage 0.1.0 发布:TTL过期管理+多实例隔离
前端·storage·uni-app x
三少爷的鞋2 小时前
Android 协程实践:为什么不应将 `supervisorScope` 作为常规方案
android
猫头_2 小时前
AI 流式传输工程指南:模型吐出半截 JSON,前端如何边收边渲染?
前端·javascript·llm
console.log('npc')2 小时前
第 1 章 项目概述与环境搭建
前端·人工智能·大模型
定栓2 小时前
Webpack迁到Vite第七节(二):全家桶移除 — Babel 全家桶深度分析
前端·webpack·node.js
IT_陈寒2 小时前
React组件莫名重渲染?你可能忘了这个小细节
前端·人工智能·后端
承渊政道3 小时前
【从零开始大模型开发与微调:基于PyTorch与ChatGLM】(从ChatGLM本地化到LoRA、PEFT的高级微调)
android·人工智能·pytorch·chatglm·本地化部署·大模型开发与微调