Reputation: 2832
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
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
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
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