简体   繁体   English

Flutter 上带有 CustomPaint 小部件的自定义形状可点击区域

[英]Custom shape tappable area with CustomPaint widget on Flutter

I've seen some posts with things similar to this question, but they're not what I'm looking for.我看过一些与这个问题类似的帖子,但它们不是我想要的。 I want to create a button with a custom shape in Flutter.我想在 Flutter 中创建一个具有自定义形状的按钮。 For that I use a CustomPaint widget inside a GestureDetector widget.为此,我在 GestureDetector 小部件中使用了 CustomPaint 小部件。 The problem is that I don't want invisible areas to be tappable.问题是我不希望不可见的区域可以点击。 And that's exactly what happens with the GestureDetector.这正是 GestureDetector 发生的事情。 In others words, I just want my created shape to be tappable.换句话说,我只希望我创建的形状是可点击的。 But right now it seems that there's an invisible square where my custom shape is, and that is also tappable.但是现在我的自定义形状似乎有一个看不见的正方形,而且它也是可以点击的。 I don't want that.我不想那样。 The most similar issue I found in this post:我在这篇文章中发现的最相似的问题:

Flutter - Custom button tap area Flutter - 自定义按钮点击区域

however, in my case I'm dealing with custom shapes and not with squares or circles.但是,就我而言,我正在处理自定义形状而不是正方形或圆形。

Let me share with you the code and an example image of a possible button.让我与您分享一个可能的按钮的代码和示例图像。 You could just copy it and paste it direct on your main.您可以将其复制并直接粘贴到您的主目录上。 It should be easy to replicate my problem.复制我的问题应该很容易。

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;
  }

}

I'd try that the star is the only tappable thing, and no other invisible place on the screen.我会尝试星星是唯一可点击的东西,屏幕上没有其他不可见的地方。

星形按钮

Thanks in advance!提前致谢!

With help of this issue https://github.com/flutter/flutter/issues/60143 and the hint given that I should use a RaisedButton with a custom shape I was able solve the problem.在这个问题https://github.com/flutter/flutter/issues/60143的帮助下,并提示我应该使用带有自定义形状的 RaisedButton,我能够解决问题。 I did some changes to the code posted on github.我对 github 上发布的代码进行了一些更改。 Mine wasn't the best to start with.我的不是最好的开始。 There are other better options than using a CustomPaint widget with a GestureDetector.除了使用带有 GestureDetector 的 CustomPaint 小部件之外,还有其他更好的选择。

Here you have the code.这里有代码。 You should be able to see that if you tap anywhere outside the shape given, the print statement will not be triggered.您应该能够看到,如果您点击给定形状之外的任何位置,将不会触发打印语句。

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));

}

This is the image you will be seeing.这是您将看到的图像。 There, if you now tap on the half empty circle, you'll see nothing will happen.在那里,如果你现在点击半空的圆圈,你会发现什么都不会发生。 That's what I was expecting.这就是我所期待的。

测试攻丝的图像

Nonetheless, I would recommend reading my other answer, which is for me so far better than this one.尽管如此,我还是建议阅读我的另一个答案,这对我来说比这个要好得多。

There are for now two solutions so far, the first one is just by overriding hitTest of CustomPainter class, however the behavior is not the most desired.到目前为止有两种解决方案,第一个只是通过覆盖 CustomPainter class 的 hitTest,但是这种行为并不是最理想的。 Because you don't have any splashcolor or similar when tapping.因为你在点击时没有任何splashcolor或类似的东西。 So here is the first one:所以这是第一个:

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;
  }
} 

And it works.它有效。 The problem is that you don't see any behavior when you tap on the "button".问题是当您点击“按钮”时看不到任何行为。

The other solution, and way better is by using Material with an Inkwell for your button.另一种解决方案,更好的方法是使用带有 Inkwell 的 Material 作为按钮。 And for your shape, ShapeBorder class.对于您的形状,ShapeBorder class。

Here it is:这里是:

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();
  }
}

A GestureDetector is hit (and start detecting) when its child says it is hit (unless you chance its behavior property).当一个GestureDetector的孩子说它被击中时,它被击中(并开始检测)(除非你碰巧它的behavior属性)。 To specify when CustomPaint is hit, CustomPainter has a hitTest(Offset) method that you can override.要指定何时点击CustomPaintCustomPainter有一个可以覆盖的hitTest(Offset)方法。 It should return whether the Offset should be consider inside your shape.它应该返回是否应在您的形状内考虑Offset Unfortunately, the method doesn't have a size parameter.不幸的是,该方法没有大小参数。 (That's a bug which solution has hit some inertia, see https://github.com/flutter/flutter/issues/28206 ) The only good solution is to make a custom render object in which you override the paint and hitTestSelf methods (in the latter, you can use the objects size property). (这是解决方案遇到一些惯性的错误,请参阅https://github.com/flutter/flutter/issues/28206 )唯一好的解决方案是制作自定义渲染 object ,您可以在其中覆盖 paint 和 hitTestSelf 方法(在后者,您可以使用对象的size属性)。

For example:例如:

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;
  }
}

Note that the top-left of the widget is at the offset parameter in the paint method, instead of the Offset.zero it is in CustomPainter .请注意,小部件的左上角位于paint方法中的offset参数处,而不是位于CustomPainter中的Offset.zero

You probably want to construct the path once and use path_0.contains(position) in hitTestSelf .您可能想构建一次路径并在hitTestSelf中使用path_0.contains(position)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM