2、画笔Paint:Paint对象属性详解、颜色与样式、抗锯齿与图像滤波、画笔性能优化

好,咱们进入第二章。画笔Paint,说白了就是Flutter绘制的「灵魂」。你画布铺好了,路径画好了,但颜色不对、边缘锯齿、性能拉胯,那画面简直没法看。我刚开始用CustomPainter时,就吃过这个亏——画了个圆,边缘全是锯齿,项目经理直接截图发群里问我是不是用画图板画的……

所以这一章,咱们把Paint这个对象彻底吃透。从属性到样式,从抗锯齿到性能优化,一条龙讲清楚。

2.1 Paint对象的核心属性

Paint对象在Flutter里,其实就是一个配置类。你给它什么参数,它就用什么方式去画。我个人习惯把Paint的配置分成三类:颜色与样式、边缘处理、性能相关。

先看最常用的几个属性:

属性名 类型 作用
color Color 画笔颜色
style PaintingStyle 填充还是描边
strokeWidth double 描边宽度
strokeCap StrokeCap 线条端点样式
strokeJoin StrokeJoin 线条连接处样式
isAntiAlias bool 是否抗锯齿
filterQuality FilterQuality 图像滤波质量
shader Shader? 渐变或图像着色器
maskFilter MaskFilter? 模糊等遮罩效果

嗯,这里要注意:colorshader是互斥的。你设置了shader,color就失效了。我在项目中遇到过有人同时设了color和shader,结果颜色死活不对,排查了半天才发现是shader覆盖了color。

2.2 颜色与样式:不只是填个色

颜色这块,Flutter提供了Color类,支持ARGB。但实际开发中,我建议直接用Color(0xFF2196F3)这种十六进制写法,清晰明了。别用Colors.blue这种,因为后期调色时你根本不知道它具体是什么蓝。

样式方面,PaintingStyle就两个值:fillstroke。但组合起来玩法很多。比如你想画一个空心圆,再在内部填个半透明色,那就得用两个Paint对象:

Paint strokePaint = Paint()
  ..color = Colors.blue
  ..style = PaintingStyle.stroke
  ..strokeWidth = 4.0;

Paint fillPaint = Paint()
  ..color = Colors.blue.withOpacity(0.2)
  ..style = PaintingStyle.fill;

canvas.drawCircle(Offset(100, 100), 50, fillPaint);
canvas.drawCircle(Offset(100, 100), 50, strokePaint);

你想想看,如果只用同一个Paint,先fill再stroke,第二个会把第一个覆盖掉。所以记住:不同样式,用不同Paint对象

小技巧: 我个人习惯把常用的Paint对象缓存起来,比如在CustomPainter的构造函数里初始化好。别在paint方法里new Paint(),那样每次重绘都会创建新对象,性能堪忧。

2.3 抗锯齿与图像滤波:边缘的「美颜」

抗锯齿,就是isAntiAlias属性。默认是true,我建议永远不要把它设为false。除非你刻意追求像素风,否则锯齿边缘真的很掉价。

为什么会这样?因为屏幕是像素矩阵,你画一条斜线,不可能完美贴合每个像素。抗锯齿就是通过边缘半透明像素来「欺骗」眼睛,让线条看起来平滑。我曾经为了性能测试,关掉抗锯齿画了一堆图形,结果画面惨不忍睹,性能提升却微乎其微。所以,别省这个。

图像滤波filterQuality,这个属性很多人忽略。它影响的是图片缩放时的采样质量。有四个级别:

枚举值 质量 性能 适用场景
none 最差 最快 极少使用
low 缩略图预览
medium 中等 中等 一般场景
high 最好 最慢 大图精细展示

嗯,这里要注意:如果你用canvas.drawImagedrawImageRect,这个属性就很重要。我做过一个图片编辑App,用户缩放图片时卡顿严重,后来把filterQuality从high降到了medium,流畅度直接翻倍。说白了,就是质量和性能的trade-off。

避坑指南: 我曾经在列表滚动时用了filterQuality.high,结果列表卡成PPT。后来发现,列表里的图片根本不需要那么高的滤波质量。所以,只在真正需要精细展示的地方用high,其他地方用medium甚至low就够了。

2.4 画笔性能优化:别让Paint拖垮你的帧率

性能优化这块,我踩过的坑最多。先列几个核心原则:

  • 复用Paint对象:不要在paint方法里new Paint()。在构造函数里创建好,paint方法里直接用。
  • 减少属性变更:每次修改Paint的属性,都会触发内部状态刷新。如果频繁改颜色或宽度,考虑用多个Paint对象。
  • 避免不必要的抗锯齿:虽然我说默认开抗锯齿,但如果你画的是水平或垂直线条,其实可以关掉。因为这类线条天然对齐像素,抗锯齿反而增加计算量。
  • 慎用MaskFilter和Shader:这两个东西很耗性能。尤其是模糊效果,能不用就不用。如果一定要用,考虑用图片代替。

举个例子,我之前做一个实时绘图App,用户手指滑动时每秒要画几十个点。一开始我每个点都new Paint(),结果帧率直接掉到20fps。后来改成复用Paint,帧率稳定在60fps。差距就这么大。

class MyPainter extends CustomPainter {
  final Paint _linePaint;
  final Paint _fillPaint;

  MyPainter() :
    _linePaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0
      ..isAntiAlias = true,
    _fillPaint = Paint()
      ..color = Colors.blue.withOpacity(0.3)
      ..style = PaintingStyle.fill;

  @override
  void paint(Canvas canvas, Size size) {
    // 直接使用缓存的Paint对象
    canvas.drawCircle(Offset(50, 50), 30, _fillPaint);
    canvas.drawCircle(Offset(50, 50), 30, _linePaint);
  }

  @override
  bool shouldRepaint(covariant MyPainter oldDelegate) {
    return false; // 如果内容不变,返回false避免重绘
  }
}
核心总结: Paint对象本身很轻量,但频繁创建和修改属性会拖累性能。我的经验是:一次创建,多次使用;按需配置,避免冗余。记住这十六个字,你的绘制性能基本不会出大问题。

好,这一章就到这里。下一章咱们聊聊路径Path,那可是绘制复杂图形的核心。到时候我会分享一个我画「贝塞尔曲线」时踩过的坑,保证让你少走弯路。