🎯 前言
在 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 对象,它允许我们组合直线和曲线。
图针的几何解构
一个经典的图针可以拆解为:
- 上半部分:一个大圆弧。
- 下半部分:两条收敛的曲线交汇于底部尖端。
贝塞尔曲线 (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. 性能优化与避坑指南
- 避免在
paint方法中创建对象 :上面的示例为了直观,在paint内部创建了Paint对象。在实际的高帧率动画中,应将Paint提取为类的成员变量,避免每帧重复分配内存。 - 正确实现
shouldRepaint:如果你的图针颜色或形状会随状态改变,请在这个方法中对比新旧属性;如果纯静态,直接返回false即可。 - 抗锯齿 :确保
Paint.isAntiAlias = true,否则曲线边缘会有明显的像素狗牙。
📝 总结
通过 CustomPaint 和底层的 Canvas API,我们摆脱了内置 Widget 的束缚。掌握了 Path 和贝塞尔曲线后,无论是图针、波浪效果还是复杂的统计图表,在 Flutter 中都不再是难题。 这就体现了 Flutter 最强悍的地方:它不仅仅是一个 UI 框架,更是一个高性能的 2D 渲染引擎!