Lucas
Lucas

Reputation: 2832

Flutter - Getting touch input on CustomPainters

I have a simple CustomPaint/CustomPainter that draws a segment of a circle (code below). I've read that I can't use GestureDetector because it isn't a proper widget, so what's the best way to get input?

I'll have a bunch of segments together so I need pixel-accurate touch location.

Two possibilities I've thought of:

My CustomPainter:

class _SegmentPainter extends CustomPainter {
  static const offset = -pi/2;
  double start;
  double end;
  double innerRadius;
  double outerRadius;
  Color color;
  _SegmentPainter(this.start, this.end, {this.innerRadius = 0.0, this.outerRadius, this.color});

  @override bool shouldRepaint(CustomPainter oldDelegate) => this == oldDelegate;
  @override bool shouldRebuildSemantics(CustomPainter oldDelegate) => this == oldDelegate;

  @override
  void paint(Canvas canvas, Size size) {
    Path path = new Path();
    path.arcTo(Rect.fromCircle(center: new Offset(0.0, 0.0), radius: outerRadius), offset + start, end-start, true);
    path.relativeLineTo(-cos(offset + end)*(outerRadius-innerRadius), -sin(offset + end)*(outerRadius-innerRadius));
    path.arcTo(Rect.fromCircle(center: new Offset(0.0, 0.0), radius: innerRadius), offset + end, start-end, false);
    path.close();

    canvas.drawPath(path, new Paint()..color = color..style = PaintingStyle.fill);
  }
}

Upvotes: 8

Views: 12729

Answers (3)

omid jafarinezhad
omid jafarinezhad

Reputation: 365

set child: Container() in CustomPaint widget!

InkWell(
      child: CustomPaint(painter: HexagonPainter(center, radius, color), child: Container(),),
      onTap: (){
        print('------yumcoder---');
      },

example:

class HexagonPaint extends StatelessWidget {
  final Offset center;
  final double radius;
  final Color color;

  HexagonPaint(this.center, this.radius, this.color);

  @override
  Widget build(BuildContext context) {
    return InkWell(
      child: CustomPaint(painter: HexagonPainter(center, radius, color), child: Container(),),
      onTap: (){
        print('------yumcoder---');
      },
    );
  }
}

class HexagonPainter extends CustomPainter {
  static const int SIDES_OF_HEXAGON = 6;
  final double radius;
  final Offset center;
  final Color color;

  HexagonPainter(this.center, this.radius, this.color);

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()..color = this.color;
    Path path = createHexagonPath();
    canvas.drawPath(path, paint);
  }

  Path createHexagonPath() {
    final path = Path();
    var angle = (math.pi * 2) / SIDES_OF_HEXAGON;
    Offset firstPoint = Offset(radius * math.cos(0.0), radius * math.sin(0.0));
    path.moveTo(firstPoint.dx + center.dx, firstPoint.dy + center.dy);
    for (int i = 1; i <= SIDES_OF_HEXAGON; i++) {
      double x = radius * math.cos(angle * i) + center.dx;
      double y = radius * math.sin(angle * i) + center.dy;
      path.lineTo(x, y);
    }
    path.close();
    return path;
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

Upvotes: 2

Natesh bhat
Natesh bhat

Reputation: 13192

I have developed a library called touchable for this purpose.

Just Wrap your CustomPaint widget with CanvasTouchDetector. It takes a builder function as argument that expects your CustomPaint widget as shown below.

CanvasTouchDetector(
    builder: (context) => 
        CustomPaint(
            painter: MyPainter(context)
        )
)

Inside your CustomPainter class's paint method , create and use the TouchyCanvas object (using the context obtained from the CanvasTouchDetector and canvas) to draw any shape with different gesture callbacks.

var myCanvas = TouchyCanvas(context,canvas);
myCanvas.drawRect( rect , Paint() , onTapDown: (tapDetail){
    //Do stuff here. Probably change your state and animate
});

Upvotes: 3

Richard Heap
Richard Heap

Reputation: 51682

I agree that you have to put the CustomPainter inside a widget that has size. It could be a SizedBox, so I've used that here. Luckily, you don't need to do a manual hit test as the CustomPainter can handle that for you with a little refactoring. The first thing to notice is that path doesn't need to be reconstructed on each paint() - it can be built in the constructor. This allows CustomPainter's hitTest to simply ask whether the tap is inside or outside the path.

class _SegmentPainter extends CustomPainter {
  static const offset = -pi / 2;

  double start;
  double end;
  double innerRadius;
  double outerRadius;
  Color color;

  Path path;

  _SegmentPainter(
      this.start, this.end, this.innerRadius, this.outerRadius, this.color) {
    path = new Path()
      ..arcTo(
          Rect.fromCircle(center: new Offset(0.0, 0.0), radius: outerRadius),
          offset + start,
          end - start,
          true)
      ..relativeLineTo(-cos(offset + end) * (outerRadius - innerRadius),
          -sin(offset + end) * (outerRadius - innerRadius))
      ..arcTo(
          Rect.fromCircle(center: new Offset(0.0, 0.0), radius: innerRadius),
          offset + end,
          start - end,
          false)
      ..close();
  }

  @override
  bool shouldRepaint(_SegmentPainter oldDelegate) {
    return oldDelegate.start != start ||
        oldDelegate.end != end ||
        oldDelegate.innerRadius != innerRadius ||
        oldDelegate.outerRadius != outerRadius ||
        oldDelegate.color != color;
  }

  @override
  bool shouldRebuildSemantics(_SegmentPainter oldDelegate) => true;

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawPath(
        path,
        new Paint()
          ..color = color
          ..style = PaintingStyle.fill);
  }

  @override
  bool hitTest(Offset position) {
    return path.contains(position);
  }
}

class SegmentWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new GestureDetector(
      onTap: () => print('tap'),
      child: new SizedBox(
        width: 250.0,
        height: 250.0,
        child: new CustomPaint(
          painter: new _SegmentPainter(0.0, 2.8, 150.0, 200.0, Colors.orange),
        ),
      ),
    );
  }
}

I've used Dart .. (cascade) syntax to clean up the path. (I think your should... tests were negated.) I added a StatelessWidget just as a home for the SizedBox and GestureDetector.

Upvotes: 14

Related Questions