App开发装X指南:玩转自定义绘制

先来看看某互联网公司前端开发和产品的日常交流(互掐):
前端开发和产品日常交流

很精彩吧,这种故事经常在互联网公司上演,那你可能会问这和本篇文章有什么关系呢?答案是没有关系。

到这里先别急着骂我哈,小编先来捋捋是咋个回事儿,作为一个从来都是和产品和平相处(苦大仇深)的App前端开发,每次碰到类似这种的需求心里都想对产品问候几遍,但是需要装X的时候,咱们得上啊,比人会的咱也会,别人不会的咱还得会,比如说 Flutter 的自定义绘制。

你可能会问,这玩意儿能干啥? Flutter 的内置组件还不够用吗?是的,Flutter 提供的内置组件的确可以满足大部分UI需求,但有时候需要实现一些特殊的UI效果,比如自定义图形(不规则的图形)、动画、渐变背景等,这时候就需要使用自定义绘制来实现。除了可以高度定制化的 UI 效果,同时可以减少 UI 的层级嵌套,优化 UI 性能,好处是不是很多。

自定义图形

比如上图中显示当前温度的圆形进度条,内置的 Widget 组件就没法儿实现了,这里就需要用到 Flutter 中的 CustomPainter

CustomPainter 是啥?

CustomPainterFlutter 中的一个抽象类,用于绘制自定义的图形和图像。通过实现 CustomPainter 类并重写其 paint 方法,开发者可以自由地定义绘制逻辑,从而实现各种复杂的绘图效果。下面使用 CustomPainter 来绘制一个简单的自定义图形.

class CustomPainterPagePage extends StatefulWidget {
  const CustomPainterPagePage({Key? key}) : super(key: key);

  
  State<CustomPainterPagePage> createState() => _CustomPainterPagePageState();
}

class _CustomPainterPagePageState extends State<CustomPainterPagePage> {

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xffF2F4F5),
      body: CustomPaint(
        painter: MyCustomPainter(), // 应用自定义的绘制类
        child: const SizedBox(
          width: 200.0,
          height: 200.0,
        ),
      ),
    );
  }
}

class MyCustomPainter extends CustomPainter {
  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 3.0
      ..style = PaintingStyle.fill;

    // 绘制一个圆形
    canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50.0, paint);
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

效果:
CustomPainter 自定义图形

从上面的例子中可以看到使用 CustomPainter 绘制自定义图形有以下几个步骤:

  1. 创建一个继承自 CustomPainter 的子类 MyCustomPainter,并实现其中的 paint 方法来定义绘图逻辑。在 paint 方法中,可以使用 Canvas API 来执行各种绘制操作,如绘制路径、文本、图像等。
  2. 将自定义的绘制类 MyCustomPainter 的实例传递给 CustomPaintpainter 属性,即可将自定义的绘制逻辑应用到 Widget 中。

CustomPaint 介绍

下面是 CustomPaint 的构造函数:

const CustomPaint({
    super.key,
    this.painter,
    this.foregroundPainter,
    this.size = Size.zero,
    this.isComplex = false,
    this.willChange = false,
    super.child,
  })
  • child:子节点。
  • painter: 背景画笔,会显示在 child 后面;
  • foregroundPainter: 前景画笔,会显示在 child 前面
  • size:当 childnull 时,代表默认绘制区域大小,如果有 child 则忽略此参数,画布尺寸则为 child 尺寸。如果有 child 但是想指定画布为特定大小,可以使用 SizeBox 包裹 CustomPaint 实现。
  • isComplex:是否复杂的绘制,如果是,Flutter 会应用一些缓存策略来减少重复渲染的开销。
  • willChange:和 isComplex 配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变。

CustomPainter 源码

下面是搂出的 CustomPainter 源码,为了好理解,小编在每个函数上面做了注释。

abstract class CustomPainter extends Listenable {
  
  const CustomPainter({ Listenable? repaint }) : _repaint = repaint;

  final Listenable? _repaint;

	// 注册一个回调,以便在需要重新绘制时收到通知。
  
  void addListener(VoidCallback listener) => _repaint?.addListener(listener);

	// 用不到的时候,需要移除监听。
  
  void removeListener(VoidCallback listener) => _repaint?.removeListener(listener);

	// 子类重写在此方法,并执行各种绘制操作。
  void paint(Canvas canvas, Size size);

	// 为该绘制的图形构建语义信息。
  SemanticsBuilderCallback? get semanticsBuilder => null;

	// 是否需要重绘语义信息
  bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => shouldRepaint(oldDelegate);

	// 是否需要重绘。
  bool shouldRepaint(covariant CustomPainter oldDelegate);

	// 点击时是否命中,传过来 position 对于当前绘制图形视为命中的点则为true,否则为false
  bool? hitTest(Offset position) => null;

  
  String toString() => '${describeIdentity(this)}(${ _repaint?.toString() ?? "" })';
}

这里面使用频率最高的就是 void paint(Canvas canvas, Size size); 函数了,Canvas 就是画布,Size 是当前绘制区域大小,下面是 Canvas 内部常用的绘制函数。

  • drawLine 划线
  • drawPoint 画点
  • drawPath 画路径
  • drawImage 画图像
  • drawRect 画矩形
  • drawCircle 画圆
  • drawOval 画椭圆
  • drawArc 画圆弧

Paint 是画笔,可以配置画笔的各种属性如粗细、颜色、样式等。

final paint = Paint()
  ..color = Colors.blue	// 画笔颜色
  ..strokeWidth = 3.0	// 画笔线条大小
  ..isAntiAlias = true	//是否抗锯齿
  ..style = PaintingStyle.fill;	//画笔样式:填充

画板刷新

CustomPainter 源码中,构造函数中 repaint 是干啥用的?

  const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
  final Listenable? _repaint;

其实 Fultter 源码注释文档已经告诉我们了,

画板刷新

翻译过来就是,触发重绘的最高效方式是:

  1. 继承 [CustomPainter] 类,并在构造函数提供一个 'repaint' 参数,当需要重新绘制时,该对象会进行通知它的监听者。
  2. 继承 [Listenable] (比如通过 [ChangeNotifier] )并实现 [CustomPainter],这样对象本身就可以直接提供通知。

可能你会问直接 setState 干不就完了吗?还用得着这么麻烦。setState 当然是可以,但咱们是对程序性能有追求的,而且还得根据具体使用的场景。setState 重建的范围太大,如果绘制的是一个大且复杂的自定义图形,加上 CustomPaint 还有一个 child 子节点,亦或者还有一个高频率的动画和滑动,这些情况下用 setState 来销毁再重建 Widget 有可能直接影响页面的流畅度。下面整个例子来实现触发重绘的最高效方式。

class SizeChangedPainter extends CustomPainter {
  final Animation<double> animation;
  SizeChangedPainter({required this.animation});

  
  void paint(Canvas canvas, Size size) {
    // 绘制逻辑
    double rectWidth = animation.value * size.width;
    double rectHeight = animation.value * size.height;

    Paint paint = Paint()..color = Colors.blue;
    canvas.drawRect(Rect.fromLTRB(0, 0, rectWidth, rectHeight), paint);
  }

  
  bool shouldRepaint(covariant SizeChangedPainter oldDelegate) {
    // 默认返回true,表示总是需要重绘
    return oldDelegate.animation.value != animation.value;
  }
}

页面调用:

class CustomPainterPagePage extends StatefulWidget {
  const CustomPainterPagePage({Key? key}) : super(key: key);

  
  State<CustomPainterPagePage> createState() => _CustomPainterPagePageState();
}

class _CustomPainterPagePageState extends State<CustomPainterPagePage>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  late Animation<double> _animation;

  
  void initState() {
    // TODO: implement initState
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 3),
    );
    _animation = Tween<double>(begin: 0.2, end: 3.0).animate(_controller);
    _controller.forward();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xffF2F4F5),
      body: Column(
        children: [
          CustomPaint(
            painter: SizeChangedPainter(animation: _animation),
            child: const SizedBox(
              width: 200.0,
              height: 200.0,
            ),
          ),
        ],
      ),
    );
  }
}

上面的例子可以看出将 Animation<double> 通过构造函数赋值给成员变量 repaint 。而 repaint 是 Listenable 可监听对象类型,当 repaint 也就是 _animation 值发送变化时,会通知画板调用 paint 实现重绘,效果如下:

CustomPainter 重绘

好了,先啰嗦到这里了,下篇来实现一个有难度点儿的自定义图形,敬请期待吧,我的公众号:Flutter技术实践,记得关注加点赞哦。

  • 29
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值