Issue
I managed to make these 3 weird shapes using CustomPainter
the slide button shape, pan button shape and tilt button shape respectively… i want to make them detectable so i added GestureDetector
but it doesnt work properly.
This is how i Stacked my Custom Painted shapes, I added GestureDetector
to each CustomPaint
but the area that they are gesture detectable are off completely i want the shapes to be detectable only within the boundaries of the shape.
class _MotionControl3State extends State<MotionControl3> {
@override
Widget build(BuildContext context) {
final PageController controller = PageController();
return Scaffold(
backgroundColor: const Color(0xff2e2e2e),
body: SafeArea(
child: Column(
children: [
Container(
color: Colors.red,
height: 250,
width: MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 25,
right: 72,
child: GestureDetector(
onTap: (){
print('pan');
},
child: CustomPaint(
painter: RPSCustomPainter3(),size: Size(250, 200),
),
),
),
Positioned(
top: 46,
child: GestureDetector(
onTap: (){
print('pan');
},
child: CustomPaint(
painter: RPSCustomPainter3(),size: Size(250, 200),
),
),
),
Positioned(
top: 106,
right: 35,
child: GestureDetector(
onTap: (){
print('tilt');
},
child: CustomPaint(
painter: RPSCustomPainter2(),size: Size(300, 150),
),
),
),
Positioned(
right:210,
child: Joystick(),
),
Positioned(
left:210,
child: Joystick(),
),
],
),
),
),
],
),
),
);
}
}
These are my Custom Painted shapes
class SlideButtonShape extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
Paint fillPaint = Paint();
fillPaint.color = const Color(0xFF830B2C);
fillPaint.style = PaintingStyle.fill;
Path path0 = Path();
path0.moveTo(size.width*0.3750000,size.height*0.2000000);
path0.cubicTo(size.width*0.7906500,size.height*0.1983400,size.width*0.7990625,size.height*0.2000000,size.width*0.9375000,size.height*0.1980000);
path0.quadraticBezierTo(size.width*0.7800750,size.height*0.2997800,size.width*0.7625000,size.height*0.5000000);
path0.lineTo(size.width*0.5500000,size.height*0.5000000);
path0.quadraticBezierTo(size.width*0.5319500,size.height*0.3022000,size.width*0.3750000,size.height*0.2000000);
path0.close();
canvas.drawPath(path0, fillPaint);
TextPainter textPainter = TextPainter(
text: TextSpan(
text: 'Slide',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center
);
textPainter.layout();
final offset = Offset(size.width / 2 + 32, size.height / 2 - 30);
textPainter.paint(canvas,offset);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class TiltButtonShape extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
Paint fillPaint = Paint();
fillPaint.color = const Color(0xFF830B2C);
fillPaint.style = PaintingStyle.fill;
Path path0 = Path();
path0.moveTo(size.width*0.2500000,size.height*0.6000000);
path0.quadraticBezierTo(size.width*0.4066750,size.height*0.4998400,size.width*0.4250000,size.height*0.3000000);
path0.lineTo(size.width*0.6375000,size.height*0.3000000);
path0.quadraticBezierTo(size.width*0.6564000,size.height*0.4992800,size.width*0.8125000,size.height*0.6000000);
path0.cubicTo(size.width*0.6718750,size.height*0.6000000,size.width*0.6718750,size.height*0.6000000,size.width*0.2500000,size.height*0.6000000);
path0.close();
canvas.drawPath(path0, fillPaint);
TextPainter textPainter = TextPainter(
text: TextSpan(
text: 'Tilt',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center
);
textPainter.layout();
final offset = Offset(size.width / 2 - 3, size.height / 2 - 15);
textPainter.paint(canvas,offset);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class PanButtonShape extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
Paint fillPaint = Paint();
fillPaint.color = const Color(0xFF830B2C);
fillPaint.style = PaintingStyle.fill;
Path path0 = Path();
path0.moveTo(size.width*0.3750000,size.height*0.3000000);
path0.cubicTo(size.width*0.5625000,size.height*0.3000000,size.width*0.5625000,size.height*0.3000000,size.width*0.6250000,size.height*0.3000000);
path0.quadraticBezierTo(size.width*0.6124375,size.height*0.4002400,size.width*0.6250000,size.height*0.5000000);
path0.lineTo(size.width*0.3750000,size.height*0.5000000);
path0.quadraticBezierTo(size.width*0.3879500,size.height*0.4000000,size.width*0.3750000,size.height*0.3000000);
path0.close();
canvas.drawPath(path0, fillPaint);
TextPainter textPainter = TextPainter(
text: TextSpan(
text: 'Pan',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center
);
textPainter.layout();
final offset = Offset(size.width / 2 - 12, size.height / 2 - 30);
textPainter.paint(canvas,offset);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Solution
instead of CustomPainter
you should extend ShapeBorder
, something like PathBorder
class below:
class MotionControl extends StatelessWidget {
const MotionControl({Key? key, required this.shapes}) : super(key: key);
final List<Shape> shapes;
@override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
for (final shape in shapes) {
shape.computeShapeBorder(const Size(36, 14), Offset.zero & constraints.biggest);
}
return Stack(
children: [
for (final shape in shapes)
Positioned.fromRect(
rect: shape.bounds,
child: Material(
shape: shape.shapeBorder,
color: shape.color,
textStyle: const TextStyle(color: Colors.white70, fontWeight: FontWeight.bold),
elevation: 2,
clipBehavior: Clip.antiAlias,
child: InkWell(
highlightColor: Colors.orange,
splashColor: Colors.deepPurple,
onTap: shape.onTap ?? () {},
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onPanUpdate: shape.onPanUpdate,
child: shape.child,
),
),
),
),
],
);
}
),
);
}
}
class Shape {
final String pathData;
final Color color;
final Widget child;
final GestureTapCallback? onTap;
final GestureDragUpdateCallback? onPanUpdate;
late PathBorder shapeBorder;
late Rect bounds;
Shape({required this.pathData, required this.color, required this.child, this.onTap, this.onPanUpdate});
void computeShapeBorder(Size inputSize, Rect rect) {
final fs = applyBoxFit(BoxFit.contain, inputSize, rect.size);
final r = Alignment.center.inscribe(fs.destination, rect);
final matrix = Matrix4.translationValues(r.left, r.top, 0)
..scale(fs.destination.width / fs.source.width);
// parseSvgPathData() needs: import 'package:path_drawing/path_drawing.dart';
final path = parseSvgPathData(pathData).transform(matrix.storage);
bounds = path.getBounds();
shapeBorder = PathBorder(path.shift(-bounds.topLeft));
}
}
class PathBorder extends ShapeBorder {
final Path path;
const PathBorder(this.path);
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
@override
ui.Path getInnerPath(ui.Rect rect, {ui.TextDirection? textDirection}) => path;
@override
ui.Path getOuterPath(ui.Rect rect, {ui.TextDirection? textDirection}) {
return path.shift(rect.topLeft);
}
@override
void paint(ui.Canvas canvas, ui.Rect rect, {ui.TextDirection? textDirection}) {}
@override
ShapeBorder scale(double t) => this;
}
now you could setup a list of your Shape
s like this:
middleSlide() => print('middle slide clicked');
middlePan() => print('middle pan clicked');
middleTilt() => print('middle tilt clicked');
innerLeft() => print('inner left clicked');
outerLeft(d) => print('outer left pan update $d');
innerRight() => print('inner right clicked');
outerRight(d) => print('outer right pan update $d');
final shapes = [
Shape(
pathData: 'm 11.646486,0.5 c 1.235074,0.880852 2.194241,2.094714 2.765625,3.5 h 7.177735 c 0.571917,-1.405532 1.531783,-2.61942 2.767578,-3.5 z',
color: const Color(0xff71112d),
child: const Center(child: Text('slide')),
onTap: middleSlide,
),
Shape(
pathData: 'm 14.589846,4.5 a 8,8 0 0 1 0.410156,2.5 8,8 0 0 1 -0.40625,2.5 h 6.816406 A 8,8 0 0 1 21,7 8,8 0 0 1 21.406252,4.5 Z',
color: const Color(0xff71112d),
child: const Center(child: Text('pan')),
onTap: middlePan,
),
Shape(
pathData: 'm 14.410158,10 c -0.571917,1.405532 -1.531783,2.61942 -2.767578,3.5 H 24.353518 C 23.118444,12.619148 22.159277,11.405286 21.587893,10 Z',
color: const Color(0xff71112d),
child: const Center(child: Text('tilt')),
onTap: middleTilt,
),
Shape(
pathData: 'M 14,7 A 7,7 0 0 1 7,14 7,7 0 0 1 0,7 7,7 0 0 1 7,0 7,7 0 0 1 14,7 Z',
color: const Color(0xff424242),
child: Stack(
children: const [
Align(alignment: Alignment.centerLeft, child: Icon(Icons.arrow_left)),
Align(alignment: Alignment.centerRight, child: Icon(Icons.arrow_right)),
],
),
onPanUpdate: outerLeft,
),
Shape(
pathData: 'm 10,7 a 3,3 0 0 1 -3,3 3,3 0 0 1 -3,-3 3,3 0 0 1 3,-3 3,3 0 0 1 3,3 z',
color: const Color(0xff71112d),
child: const Center(child: Text('slide')),
onTap: innerLeft,
),
Shape(
pathData: 'm 36,7 a 7,7 0 0 1 -7,7 7,7 0 0 1 -7,-7 7,7 0 0 1 7,-7 7,7 0 0 1 7,7 z',
color: const Color(0xff424242),
child: Stack(
children: const [
Align(alignment: Alignment.centerLeft, child: Icon(Icons.arrow_left)),
Align(alignment: Alignment.topCenter, child: Icon(Icons.arrow_drop_up)),
Align(alignment: Alignment.centerRight, child: Icon(Icons.arrow_right)),
Align(alignment: Alignment.bottomCenter, child: Icon(Icons.arrow_drop_down)),
],
),
onPanUpdate: outerRight,
),
Shape(
pathData: 'm 32,7 a 3,3 0 0 1 -3,3 3,3 0 0 1 -3,-3 3,3 0 0 1 3,-3 3,3 0 0 1 3,3 z',
color: const Color(0xff71112d),
child: const Center(child: Text('pan\ntilt', textAlign: TextAlign.center,)),
onTap: innerRight,
),
];
and finally use MotionControl
like:
MotionControl(shapes: shapes)
this is what you should get:
you may wonder what are those "magic" strings like m 32,7 a 3,3 0 0 1 -3,3 3,3 0 0 1 -3,-3 3,3 0 0 1 3,-3 3,3 0 0 1 3,3 z
passed to Shape
constructor?
this is your shape path definition taken directly from svg
file (<path d=...?/>
) – you can edit this file with free inkscape drawing tool
notice that you need to install path_drawing package to parse those strings into Path
objects
the path definitions were taken from this svg file – you dont need to copy this anywhere to your project (however you can save it somewhere if you want to change the shapes)
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="36"
height="14"
viewBox="0 0 36 14"
version="1.1"
id="SVGRoot"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="shapes.svg">
<defs
id="defs10" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="32.222222"
inkscape:cx="17.952609"
inkscape:cy="7"
inkscape:document-units="px"
inkscape:current-layer="SVGRoot"
showgrid="true"
inkscape:window-width="1366"
inkscape:window-height="744"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:grid-bbox="true"
objecttolerance="10000"
guidetolerance="10000"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
type="xygrid"
id="grid23"
originx="-0.99999809"
originy="-1" />
</sodipodi:namedview>
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<path
style="opacity:1;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.48323965;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 14.589846,4.5 a 8,8 0 0 1 0.410156,2.5 8,8 0 0 1 -0.40625,2.5 h 6.816406 A 8,8 0 0 1 21,7 8,8 0 0 1 21.406252,4.5 Z"
id="path26"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.48323965;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 14.410158,10 c -0.571917,1.405532 -1.531783,2.61942 -2.767578,3.5 H 24.353518 C 23.118444,12.619148 22.159277,11.405286 21.587893,10 Z"
id="path28"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="opacity:1;fill:#800000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.93218362;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 14,7 A 7,7 0 0 1 7,14 7,7 0 0 1 0,7 7,7 0 0 1 7,0 7,7 0 0 1 14,7 Z"
id="circle30" />
<path
style="opacity:1;fill:#800000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.93218362;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 36,7 a 7,7 0 0 1 -7,7 7,7 0 0 1 -7,-7 7,7 0 0 1 7,-7 7,7 0 0 1 7,7 z"
id="circle32" />
<path
style="opacity:1;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.48323965;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 11.646486,0.5 c 1.235074,0.880852 2.194241,2.094714 2.765625,3.5 h 7.177735 c 0.571917,-1.405532 1.531783,-2.61942 2.767578,-3.5 z"
id="path34"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="opacity:1;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.71428573;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 10,7 a 3,3 0 0 1 -3,3 3,3 0 0 1 -3,-3 3,3 0 0 1 3,-3 3,3 0 0 1 3,3 z"
id="circle36" />
<path
style="opacity:1;fill:#008000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.71428573;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 32,7 a 3,3 0 0 1 -3,3 3,3 0 0 1 -3,-3 3,3 0 0 1 3,-3 3,3 0 0 1 3,3 z"
id="circle38" />
</svg>
Answered By – pskink
Answer Checked By – Terry (FlutterFixes Volunteer)