You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为堆叠的自定义绘制形状实现精准手势检测

Fixing Gesture Detection for Custom Shaped Buttons in Flutter

The issue you're facing is that GestureDetector uses the entire rectangular bounds of your CustomPaint widget for hit testing, instead of the actual custom path you've drawn. To fix this, we need to override the hit testing logic in your CustomPainter classes to only recognize taps within the bounds of your drawn paths.

Step 1: Modify Your CustomPainter Classes

For each of your button shape painters, we'll:

  1. Store the drawn path as a class member variable
  2. Override the hitTest method to check if the tap position is inside the path

Updated SlideButtonShape

class SlideButtonShape extends CustomPainter {
  late Path _buttonPath;

  @override
  void paint(Canvas canvas, Size size) {
    Paint fillPaint = Paint();
    fillPaint.color = const Color(0xFF830B2C);
    fillPaint.style = PaintingStyle.fill;

    // Initialize and store the path as a member variable
    _buttonPath = Path();
    _buttonPath.moveTo(size.width*0.3750000,size.height*0.2000000);
    _buttonPath.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);
    _buttonPath.quadraticBezierTo(size.width*0.7800750,size.height*0.2997800,size.width*0.7625000,size.height*0.5000000);
    _buttonPath.lineTo(size.width*0.5500000,size.height*0.5000000);
    _buttonPath.quadraticBezierTo(size.width*0.5319500,size.height*0.3022000,size.width*0.3750000,size.height*0.2000000);
    _buttonPath.close();

    canvas.drawPath(_buttonPath, fillPaint);

    // Text rendering remains unchanged
    TextPainter textPainter = TextPainter(
      text: const 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;
  }

  // Override hit test to only recognize taps inside our path
  @override
  bool hitTest(Offset position) {
    return _buttonPath.contains(position);
  }
}

Updated TiltButtonShape

class TiltButtonShape extends CustomPainter {
  late Path _buttonPath;

  @override
  void paint(Canvas canvas, Size size) {
    Paint fillPaint = Paint();
    fillPaint.color = const Color(0xFF830B2C);
    fillPaint.style = PaintingStyle.fill;

    _buttonPath = Path();
    _buttonPath.moveTo(size.width*0.2500000,size.height*0.6000000);
    _buttonPath.quadraticBezierTo(size.width*0.4066750,size.height*0.4998400,size.width*0.4250000,size.height*0.3000000);
    _buttonPath.lineTo(size.width*0.6375000,size.height*0.3000000);
    _buttonPath.quadraticBezierTo(size.width*0.6564000,size.height*0.4992800,size.width*0.8125000,size.height*0.6000000);
    _buttonPath.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);
    _buttonPath.close();

    canvas.drawPath(_buttonPath, fillPaint);

    TextPainter textPainter = TextPainter(
      text: const 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;
  }

  @override
  bool hitTest(Offset position) {
    return _buttonPath.contains(position);
  }
}

Updated PanButtonShape

class PanButtonShape extends CustomPainter {
  late Path _buttonPath;

  @override
  void paint(Canvas canvas, Size size) {
    Paint fillPaint = Paint();
    fillPaint.color = const Color(0xFF830B2C);
    fillPaint.style = PaintingStyle.fill;

    _buttonPath = Path();
    _buttonPath.moveTo(size.width*0.3750000,size.height*0.3000000);
    _buttonPath.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);
    _buttonPath.quadraticBezierTo(size.width*0.6124375,size.height*0.4002400,size.width*0.6250000,size.height*0.5000000);
    _buttonPath.lineTo(size.width*0.3750000,size.height*0.5000000);
    _buttonPath.quadraticBezierTo(size.width*0.3879500,size.height*0.4000000,size.width*0.3750000,size.height*0.3000000);
    _buttonPath.close();

    canvas.drawPath(_buttonPath, fillPaint);

    TextPainter textPainter = TextPainter(
      text: const 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;
  }

  @override
  bool hitTest(Offset position) {
    return _buttonPath.contains(position);
  }
}

Step 2: Update Your State Class

Make sure you're using the correct painter classes in your CustomPaint widgets (replace RPSCustomPainter3 with your updated painters):

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: PanButtonShape(),
                          size: const Size(250, 200),
                        ),
                      ),
                    ),
                    Positioned(
                      top: 46,
                      child: GestureDetector(
                        onTap: (){
                          print('slide');
                        },
                        child: CustomPaint(
                          painter: SlideButtonShape(),
                          size: const Size(250, 200),
                        ),
                      ),
                    ),
                    Positioned(
                      top: 106,
                      right: 35,
                      child: GestureDetector(
                        onTap: (){
                          print('tilt');
                        },
                        child: CustomPaint(
                          painter: TiltButtonShape(),
                          size: const Size(300, 150),
                        ),
                      ),
                    ),
                    const Positioned(
                      right:210,
                      child: Joystick(),
                    ),
                    const Positioned(
                      left:210,
                      child: Joystick(),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

How This Works

By default, CustomPaint uses its entire rectangular size for hit testing. When we override the hitTest method in our CustomPainter, we tell Flutter to only consider taps that fall within the bounds of our custom path. The Path.contains(position) method checks if the tap coordinate (relative to the CustomPaint widget) lies inside the path we've drawn.

内容的提问来源于stack exchange,提问作者Zuher Abud Said

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 16:22:42