Flutter之自定义画板CustomPainter

673 阅读2分钟

「时光不负,创作不停,本文正在参加2022年中总结征文大赛

先上效果图:

first_flutter_project 2022-07-02 14-06-59 00_00_00-00_00_30.gif

流程:

  • 继承CustomPainter自定义画笔核心---BoardPainter
  • 使用Listener监听滑动手势将移动的位置点收集起来_pointOffset
  • 使用画布的canvas.drawLine方法将所有点连接起来绘制出线条
  • 采用RepaintBoundary包裹CustomPaint隔离画笔控件,防止singlescrollview 滑动发生重绘,导致画笔paint响应
  • 使用_HitTestBlocker使事件可以穿透,使画板下的控件可以参与命中测试,防止点击事件无效比如:TextButton的点击事件被上面消耗(hitTest()事件

一、继承CustomPainter自定义画笔核心

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

import '../Gesture/GestureDetectorHitTest.dart';

///组合动画用法
///
///抽奖滚动动画 stack的组合
///
///
///
///

class PointOffset {
  Offset point;
  bool isEnd;


  PointOffset.name(this.point, this.isEnd);
}

class CanvasPointScroll extends StatefulWidget {
  @override
  _CanvasPointScrollState createState() => _CanvasPointScrollState();
}

class _CanvasPointScrollState extends State<CanvasPointScroll> {
  //布局
  final GlobalKey containWidgetGlobalKey = GlobalKey();

  var _scrollController = ScrollController();

  //画布
  late BoardPainterWidget canvasWidget;

  //局部刷新
  late CounterNotify counterNotify;

  bool isScroll = true;

  @override
  void initState() {
    super.initState();
    counterNotify = CounterNotify();

    Future.delayed(Duration(milliseconds: 5000), () {
      counterNotify.addValue(400);
    });

    canvasWidget = BoardPainterWidget(containWidgetGlobalKey);
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: SingleChildScrollView(
          physics: isScroll?null:NeverScrollableScrollPhysics(),
          controller: _scrollController,
            child: Stack(
              children: [
                ListView(
                  shrinkWrap: true,
                  physics: new NeverScrollableScrollPhysics(),
                  key: containWidgetGlobalKey,
                  children: [
                    Column(
                      children: [
                        Container(
                          color: Color(0xFF15C9E8),
                          width: 100,
                          height: 400,
                        ),
                        Container(
                          color: Color(0xFF300C92),
                          width: 400,
                          height: 400,
                        ),
                        Container(
                          color: Color(0xFFB41C89),
                          width: 400,
                          height: 400,
                        ),
                        TextButton(
                            onPressed: () {
                              print("按钮");
                              isScroll = !isScroll;
                              setState(() {

                              });
                            },
                            child: Text("data")),
                        TextField(),
                        ValueListenableBuilder<double>(
                          builder: (BuildContext context, value, Widget? child) {
                            return Container(
                              color: Color(0xFF1947EE),
                              width: 400,
                              height: value,
                            );
                          },
                          valueListenable: counterNotify.countListenable,
                        )
                      ],
                    ),
                  ],
                ),

                canvasWidget
              ],

          ),
        ),
      ),
    );
  }
}

class CounterNotify with ChangeNotifier {
  ValueNotifier<double> _countListenable = ValueNotifier<double>(0.0);

  ValueNotifier<double> get countListenable => _countListenable;

  void addValue(double i) {
    _countListenable.value += i;
  }
}

class BoardPainterWidget extends StatefulWidget {
  GlobalKey containWidgetGlobalKey;

  BoardPainterWidget(this.containWidgetGlobalKey);

  // void Function(double move)? callMoveBack;

  // void getMoveDistance(double move){
  //    print("getMoveDistance--:$move");
  //    _state.getMoveDistance(move);
  // }

  _BoardPainterWidgetState _state = _BoardPainterWidgetState();

  @override
  _BoardPainterWidgetState createState() {
    return _state;
  }
}

class _BoardPainterWidgetState extends State<BoardPainterWidget> {
  /// 已描绘的点
  List<PointOffset> _pointOffset = <PointOffset>[];

  final GlobalKey containWidgetGlobalKey = GlobalKey();

  //一屏的高度
  double screenHeight = 0;

  //画布的高度
  double _gestureDetectorHeight = 0;
  double _gestureDetectorWidth = 0;

  late ValueNotifier<List<PointOffset>> _pointListenable;

  /// 添加点,注意不要超过Widget范围
  _addPoint(PointerMoveEvent details) {
    Size? referenceBox = widget.containWidgetGlobalKey.currentContext?.size;
    double maxW = referenceBox?.width ?? 0;
    double maxH = referenceBox?.height ?? 0;
    // 校验范围
    if (details.localPosition.dx <= 0 || details.localPosition.dy <= 0) return;
    if (details.localPosition.dx > maxW || details.localPosition.dy > maxH)
      return;

    // print("_addPoint $_gestureDetectorHeight");
    // setState(() {
    //   _pointOffset = List.from(_pointOffset)
    //     ..add(PointOffset.name(details.localPosition, true));
    // });
    //listener监听,不会影响整棵树的绘制
    _pointOffset = List.from(_pointOffset)
      ..add(PointOffset.name(details.localPosition, true));
    _pointListenable.value = _pointOffset;
  }

  void moveCallback(double move) {}

  @override
  void initState() {
    super.initState();
    _pointListenable = ValueNotifier<List<PointOffset>>(_pointOffset);

    WidgetsBinding.instance?.addPostFrameCallback((_) {
      _gestureDetectorHeight =
          widget.containWidgetGlobalKey.currentContext?.size?.height ?? 0;
      _gestureDetectorWidth =
          widget.containWidgetGlobalKey.currentContext?.size?.width ?? 0;
      print("绘制完成1:$_gestureDetectorHeight");
      WidgetsBinding.instance?.addPersistentFrameCallback((_) {
        print("实时Frame绘制回调"); //每帧都回调
        var widgetHeight =
            widget.containWidgetGlobalKey.currentContext?.size?.height ?? 0;

        if (widgetHeight != _gestureDetectorHeight &&
            widgetHeight > _gestureDetectorHeight) {
          _gestureDetectorHeight = widgetHeight;

          _gestureDetectorWidth =
              widget.containWidgetGlobalKey.currentContext?.size?.width ?? 0;
          setState(() {});
          print("绘制完成2:$_gestureDetectorHeight");
        }
      });

      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
       _HitTestBlocker(child: Listener(
              onPointerMove: (details) {
                print(
                    "object:${details.localPosition}----${details.distance}");
                _addPoint(details);
              },
              onPointerUp: (details) {
                _pointOffset.add(PointOffset.name(Offset.zero, false));
              },
              child: Container(
                height: _gestureDetectorHeight,
                width: _gestureDetectorWidth,
                color: Color(0xFFFFFF),
              ))),

        RepaintBoundary(
          //隔离重绘控件,singlescrollview 滑动发生重绘,导致画笔paint响应
            child: CustomPaint(
                painter: BoardPainter(
                    points: _pointOffset, factor: _pointListenable))),
      ],
    );
  }
}

class BoardPainter extends CustomPainter {
  final ValueNotifier<List<PointOffset>>? factor;

  BoardPainter({required this.points, this.factor}) : super(repaint: factor);

  List<PointOffset> points;

  void paint(Canvas canvas, Size size) {
    points = factor?.value ?? points;
    print("绘制 paint1 ${points.length}-- ${factor?.value.length}");
    if (points == null) {
      return;
    }
    Paint paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null &&
          points[i + 1] != null &&
          points[i + 1].isEnd &&
          points[i].isEnd) {
        // print("绘制 paint${points[i].point}----${points[i + 1].point}");

        canvas.drawLine(points[i].point, points[i + 1].point, paint);
      }
    }
  }

  bool shouldRepaint(BoardPainter other) {
    // print("绘制 shouldRepaint--:${other.points != points}");
    return other.points != points;
  }
}

class _HitTestBlocker extends SingleChildRenderObjectWidget {
  _HitTestBlocker({
    Key? key,
    this.blocker = true,
    Widget? child,
  }) : super(key: key, child: child);

  final bool blocker;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderHitTestBlocker(blocker: blocker);
  }

  @override
  void updateRenderObject(
      BuildContext context, _RenderHitTestBlocker renderObject) {
    renderObject..blocker = blocker;
  }
}

class _RenderHitTestBlocker extends RenderProxyBox {
  _RenderHitTestBlocker({this.blocker = true});

  bool blocker;

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (blocker) {
      hitTestChildren(result, position: position);
      return false;
    } else {
      return super.hitTest(result, position: position);
    }
  }

  @override
  bool hitTestSelf(Offset position) => blocker;
}

参考:juejin.cn/post/691629…