【问题标题】:Custom shape tappable area with CustomPaint widget on FlutterFlutter 上带有 CustomPaint 小部件的自定义形状可点击区域
【发布时间】:2021-04-17 17:38:12
【问题描述】:

我看到一些帖子与此问题类似,但它们不是我要找的。我想在 Flutter 中创建一个具有自定义形状的按钮。为此,我在 GestureDetector 小部件中使用了 CustomPaint 小部件。问题是我不希望不可见的区域可以点击。这正是 GestureDetector 发生的事情。换句话说,我只希望我创建的形状是可点击的。但现在我的自定义形状似乎有一个看不见的正方形,而且它也是可以点击的。我不想要那个。我在这篇文章中发现的最相似的问题:

Flutter - Custom button tap area

但是,就我而言,我处理的是自定义形状,而不是正方形或圆形。

让我与您分享一个可能的按钮的代码和示例图像。您可以将其复制并直接粘贴到您的主目录上。复制我的问题应该很容易。

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

}

我会尝试让星星是唯一可点击的东西,屏幕上没有其他不可见的地方。

提前致谢!

【问题讨论】:

  • 您需要一个自定义的ShapeBorder 类,并将其传递给RaisedButton.shape

标签: flutter


【解决方案1】:

目前为止有两种解决方案,第一种只是通过覆盖 CustomPainter 类的 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 类。

这里是:

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

【讨论】:

  • 你怎么看那个按钮被点击了?您是否有任何迹象表明您点击了该按钮?不,但是使用 InkWell 您可以清楚地看到您点击了按钮,那么使用 CustomPaint 有什么更好的选择?
  • 你说得很好。在我看来,使用自定义绘画创建表单更容易,但正如您所说,Inkwell 的问题确实更好。
  • 老实说,为什么 CustomPaint 更容易?在这两种情况下,您都必须创建相同的路径...
  • 我不怀疑。我只需要更好地了解ShapeBorder。例如,您将如何创建星形: 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();在这堂课中。
  • 我按照你说的改变了路径,它可以工作。只是有一些奇怪的行为,我的按钮出现在左上角。即使正在使用中心小部件。
【解决方案2】:

借助这个问题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));

}

这是您将看到的图像。在那里,如果你现在点击半空的圆圈,你会发现什么都不会发生。这正是我所期待的。

尽管如此,我还是建议阅读我的另一个答案,这对我来说比这个要好得多。

【讨论】:

  • 顺便说一句,您也可以使用OutlineButtonFlatButton 甚至是原始的Material 小部件,但在这种情况下,您也需要InkWell,更多信息请参见EDIT2这里:stackoverflow.com/a/57943257/2252830
  • 这很有趣,但我不得不说,我仍然在努力解决它。比如说,如果我想创建一个可点击的三角形,而在它之外什么都没有,我认为这是不可能的,因为似乎总是需要一个矩形作为基础。你怎么看?在 custompaint 类中总是有这个 Rect 对象。
  • 只是纠缠?所以很简单Path.moveToPath.lineToPath.lineToPath.close
  • 是的,创建表单很简单。但是要确定唯一可点击的区域实际上是这个三角形是我正在寻找的
  • 你是否使用了置顶评论中的测试代码? paste.ubuntu.com/p/gjgVszpwhC 第 2-8 行
【解决方案3】:

当一个GestureDetector 的孩子说它被击中时,它被击中(并开始检测)(除非你碰巧它的behavior 属性)。 要指定何时命中 CustomPaintCustomPainter 有一个可以覆盖的 hitTest(Offset) 方法。它应该返回 Offset 是否应该考虑在您的形状内。不幸的是,该方法没有大小参数。 (这是解决方案遇到了一些惯性的错误,请参阅https://github.com/flutter/flutter/issues/28206) 唯一好的解决方案是创建一个自定义渲染对象,在其中覆盖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 参数处,而不是Offset.zero 它位于CustomPainter 中。

您可能希望构建一次路径并在hitTestSelf 中使用path_0.contains(position)

【讨论】:

  • 你说得对@spkersten 这个解决方案非常有效。我正在发布带有更正代码的代码。
  • 只有在点击按钮时才会有一些令人满意的行为。一个人可能想要一些splashcolor。手势检测器不会给你那个。但除此之外,这是一个非常可接受的解决方案。
猜你喜欢
  • 2021-10-04
  • 2021-09-04
  • 1970-01-01
  • 1970-01-01
  • 2020-07-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多