[英]Custom shape tappable area with CustomPaint widget on Flutter
我看过一些与这个问题类似的帖子,但它们不是我想要的。 我想在 Flutter 中创建一个具有自定义形状的按钮。 为此,我在 GestureDetector 小部件中使用了 CustomPaint 小部件。 问题是我不希望不可见的区域可以点击。 这正是 GestureDetector 发生的事情。 换句话说,我只希望我创建的形状是可点击的。 但是现在我的自定义形状似乎有一个看不见的正方形,而且它也是可以点击的。 我不想那样。 我在这篇文章中发现的最相似的问题:
但是,就我而言,我正在处理自定义形状而不是正方形或圆形。
让我与您分享一个可能的按钮的代码和示例图像。 您可以将其复制并直接粘贴到您的主目录上。 复制我的问题应该很容易。
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Custom Shapes',
theme: ThemeData.dark(),
home: MyHomePage(title: 'Custom Shapes App'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
backgroundColor: Colors.white24,
body: Center(
child: GestureDetector(
child: CustomPaint(
size: Size(300,300), //You can Replace this with your desired WIDTH and HEIGHT
painter: RPSCustomPainter(),
),
onTap: (){
print("Working?");
},
),
),
);
}
}
class RPSCustomPainter extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
Paint paint_0 = new Paint()
..color = Color.fromARGB(255, 33, 150, 243)
..style = PaintingStyle.fill
..strokeWidth = 1;
paint_0.shader = ui.Gradient.linear(Offset(0,size.height*0.50),Offset(size.width,size.height*0.50),[Color(0xffffed08),Color(0xffffd800),Color(0xffff0000)],[0.00,0.34,1.00]);
Path path_0 = Path();
path_0.moveTo(0,size.height*0.50);
path_0.lineTo(size.width*0.33,size.height*0.33);
path_0.lineTo(size.width*0.50,0);
path_0.lineTo(size.width*0.67,size.height*0.33);
path_0.lineTo(size.width,size.height*0.50);
path_0.lineTo(size.width*0.67,size.height*0.67);
path_0.lineTo(size.width*0.50,size.height);
path_0.lineTo(size.width*0.33,size.height*0.67);
path_0.lineTo(0,size.height*0.50);
path_0.close();
canvas.drawPath(path_0, paint_0);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
我会尝试星星是唯一可点击的东西,屏幕上没有其他不可见的地方。
提前致谢!
在这个问题https://github.com/flutter/flutter/issues/60143的帮助下,并提示我应该使用带有自定义形状的 RaisedButton,我能够解决问题。 我对 github 上发布的代码进行了一些更改。 我的不是最好的开始。 除了使用带有 GestureDetector 的 CustomPaint 小部件之外,还有其他更好的选择。
这里有代码。 您应该能够看到,如果您点击给定形状之外的任何位置,将不会触发打印语句。
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: Center(child: BuyTicketButton(100.0, ()=>{})))
);
}
}
class BuyTicketButton extends StatelessWidget {
final double cost;
final Function onTap;
const BuyTicketButton(this.cost, this.onTap, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: 400,
child: RaisedButton(
shape: CustomBorder(),
onPressed: (){
print("this works");
},
),
),
);
}
}
class CustomBorder extends OutlinedBorder {
const CustomBorder({
BorderSide side = BorderSide.none
}) : assert(side != null), super(side: side);
Path customBorderPath(Rect rect) {
Path path = Path();
path.moveTo(0, 0);
path.lineTo(rect.width, 0);
path.lineTo(rect.width, rect.height);
path.lineTo(0, rect.height);
double diameter = rect.height / 3;
double radius = diameter / 2;
path.lineTo(0, diameter * 2);
path.arcToPoint(
Offset(0, diameter),
radius: Radius.circular(radius),
clockwise: false,
);
path.lineTo(0, 0);
return path;
}
@override
OutlinedBorder copyWith({BorderSide side}) {
return CustomBorder(side: side ?? this.side);
}
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width);
@override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
return customBorderPath(rect);
}
@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
return customBorderPath(rect);
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
switch (side.style) {
case BorderStyle.none:
break;
case BorderStyle.solid:
canvas.drawPath(customBorderPath(rect), Paint()
..style = PaintingStyle.stroke
..color = Colors.black
..strokeWidth = 1.0
);
}
}
@override
ShapeBorder scale(double t) => CustomBorder(side: side.scale(t));
}
这是您将看到的图像。 在那里,如果你现在点击半空的圆圈,你会发现什么都不会发生。 这就是我所期待的。
尽管如此,我还是建议阅读我的另一个答案,这对我来说比这个要好得多。
到目前为止有两种解决方案,第一个只是通过覆盖 CustomPainter class 的 hitTest,但是这种行为并不是最理想的。 因为你在点击时没有任何splashcolor或类似的东西。 所以这是第一个:
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black54,
body: Center(
child: TappableStarButton(),
),
),
);
}
}
class TappableStarButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
child: CustomPaint(
size: Size(300, 300),
painter: RPSCustomPainter(),
),
onTap: () {
print("This works");
},
),
);
}
}
class RPSCustomPainter extends CustomPainter {
Path path_0 = Path();
@override
void paint(Canvas canvas, Size size) {
Paint paint_0 = new Paint()
..color = Color.fromARGB(255, 33, 150, 243)
..style = PaintingStyle.fill
..strokeWidth = 1;
paint_0.shader = ui.Gradient.linear(
Offset(10, 150),
Offset(290, 150),
[Color(0xffff1313), Color(0xffffbc00), Color(0xffffca00)],
[0.00, 0.69, 1.00]);
path_0.moveTo(150, 10);
path_0.lineTo(100, 100);
path_0.lineTo(10, 150);
path_0.lineTo(100, 200);
path_0.lineTo(150, 290);
path_0.lineTo(200, 200);
path_0.lineTo(290, 150);
path_0.lineTo(200, 100);
canvas.drawPath(path_0, paint_0);
}
@override
bool hitTest(Offset position) {
bool hit = path_0.contains(position);
return hit;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
它有效。 问题是当您点击“按钮”时看不到任何行为。
另一种解决方案,更好的方法是使用带有 Inkwell 的 Material 作为按钮。 对于您的形状,ShapeBorder class。
这里是:
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black38,
body: StarButton(),
),
);
}
}
class StarButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: 300,
width: 300,
child: Material(
shape: StarShape(),
color: Colors.orange,
child: InkWell(
splashColor: Colors.yellow,
onTap: () => print('it works'),
),
),
),
);
}
}
class StarShape extends ShapeBorder {
@override
EdgeInsetsGeometry get dimensions => null;
@override
Path getInnerPath(Rect rect, {ui.TextDirection textDirection}) => null;
@override
void paint(Canvas canvas, Rect rect, {ui.TextDirection textDirection}) =>
null;
@override
ShapeBorder scale(double t) => null;
@override
Path getOuterPath(Rect rect, {ui.TextDirection textDirection}) {
return Path()
..moveTo(150, 10)
..lineTo(100, 100)
..lineTo(10, 150)
..lineTo(100, 200)
..lineTo(150, 290)
..lineTo(200, 200)
..lineTo(290, 150)
..lineTo(200, 100)
..close();
}
}
当一个GestureDetector
的孩子说它被击中时,它被击中(并开始检测)(除非你碰巧它的behavior
属性)。 要指定何时点击CustomPaint
, CustomPainter
有一个可以覆盖的hitTest(Offset)
方法。 它应该返回是否应在您的形状内考虑Offset
。 不幸的是,该方法没有大小参数。 (这是解决方案遇到一些惯性的错误,请参阅https://github.com/flutter/flutter/issues/28206 )唯一好的解决方案是制作自定义渲染 object ,您可以在其中覆盖 paint 和 hitTestSelf 方法(在后者,您可以使用对象的size
属性)。
例如:
class MyCirclePainter extends LeafRenderObjectWidget {
const MyCirclePainter({@required this.radius, Key key}) : super(key: key);
// radius relative to the widget's size
final double radius;
@override
RenderObject createRenderObject(BuildContext context) => RenderMyCirclePainter()..radius = radius;
@override
void updateRenderObject(BuildContext context, RenderMyCirclePainter renderObject) => renderObject.radius = radius;
}
class RenderMyCirclePainter extends RenderBox {
double radius;
@override
void performLayout() {
size = constraints.biggest;
}
@override
void performResize() {
size = constraints.biggest;
}
@override
void paint(PaintingContext context, Offset offset) {
final center = size.center(offset);
final r = 1.0 * radius * size.width;
final backgroundPaint = Paint()
..color = const Color(0x88202020)
..style = PaintingStyle.fill;
context.canvas.drawCircle(center, r, backgroundPaint);
}
@override
bool hitTestSelf(Offset position) {
final center = size.center(Offset.zero);
return (position - center).distance < size.width * radius;
}
}
请注意,小部件的左上角位于paint
方法中的offset
参数处,而不是位于CustomPainter
中的Offset.zero
。
您可能想构建一次路径并在hitTestSelf
中使用path_0.contains(position)
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.