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

Flutter:如何正确检测CustomPainter对象的触摸进入、移动与退出

解决Flutter中自定义形状的触摸进入/移动/退出事件检测问题

我来帮你搞定这个问题!你遇到的情况其实是Flutter里Pointer事件和hitTest机制的常见坑——默认的Listener和hitTest行为并不能满足“从外部滑入形状也能触发进入事件”的需求,而且你当前的代码还没真正实现基于形状(或MBR)的精确检测。咱们一步步来修复:

问题根源分析

  1. PointerEnter/Exit的局限性:这两个事件主要是为鼠标hover设计的,移动端触摸场景下,只有当指针一开始就在Widget范围内,或者通过鼠标hover进入时才会触发,从外部滑入的触摸不会触发这些事件。
  2. 默认hitTest的问题:你当前的hitTest调用了super.hitTest(position),而默认实现是基于Widget的布局边界(也就是你的Container的矩形),不是你画的圆形形状。而且Stack里的Widget是按顺序堆叠的,后面的Widget会先接收事件,导致前面的Widget可能无法响应。
  3. 触摸事件的传递:如果触摸起始不在某个Widget内,该Widget的onPointerMove不会被触发,因为Flutter的事件传递是基于命中测试的——只有初始命中的Widget才会持续接收move事件。

解决方案:全局监听+自定义hitTest+状态追踪

我们需要在父级监听所有Pointer事件,然后主动检查每个自定义形状是否被触摸,同时维护每个形状的触摸状态,以此模拟enter/move/exit事件。

修改后的完整代码

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.deepOrange),
      home: MyHomePage(title: 'Flutter Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // 追踪每个形状的触摸状态
  bool _isYellowCircleTouched = false;
  bool _isRedCircleTouched = false;

  // 获取CustomPaint的RenderObject,用于转换坐标
  final GlobalKey _yellowPaintKey = GlobalKey();
  final GlobalKey _redPaintKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        // 在父级监听所有Pointer事件
        child: Listener(
          onPointerDown: _handlePointerEvent,
          onPointerMove: _handlePointerEvent,
          onPointerUp: _handlePointerEvent,
          onPointerCancel: _handlePointerEvent,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Stack(
                alignment: Alignment.center,
                children: <Widget>[
                  // 黄色圆形
                  CustomPaint(
                    key: _yellowPaintKey,
                    painter: CirclePainter(color: Colors.yellow, radiusRatio: 0.5),
                    child: Container(height: 400, width: 400),
                  ),
                  // 红色圆形
                  CustomPaint(
                    key: _redPaintKey,
                    painter: CirclePainter(color: Colors.red, radiusRatio: 0.5),
                    child: Container(height: 200, width: 200),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _handlePointerEvent(PointerEvent event) {
    // 处理黄色圆形的触摸状态
    _updateShapeTouchState(
      key: _yellowPaintKey,
      currentState: _isYellowCircleTouched,
      shapeName: "yellow",
      eventPosition: event.position,
    );

    // 处理红色圆形的触摸状态
    _updateShapeTouchState(
      key: _redPaintKey,
      currentState: _isRedCircleTouched,
      shapeName: "red",
      eventPosition: event.position,
    );

    // 如果是move事件,打印当前触摸的形状
    if (event is PointerMoveEvent) {
      if (_isYellowCircleTouched) print("Move yellow");
      if (_isRedCircleTouched) print("Move red");
    }

    // 触摸结束时重置状态
    if (event is PointerUpEvent || event is PointerCancelEvent) {
      setState(() {
        _isYellowCircleTouched = false;
        _isRedCircleTouched = false;
      });
    }
  }

  // 更新单个形状的触摸状态,触发enter/exit
  void _updateShapeTouchState({
    required GlobalKey key,
    required bool currentState,
    required String shapeName,
    required Offset eventPosition,
  }) {
    // 获取CustomPaint的RenderObject
    final renderObject = key.currentContext?.findRenderObject() as RenderBox?;
    if (renderObject == null) return;

    // 将全局坐标转换为CustomPaint内部的局部坐标
    final localPosition = renderObject.globalToLocal(eventPosition);
    final size = renderObject.size;

    // 获取自定义Painter,调用其hitTest方法
    final painter = key.currentContext?.widget is CustomPaint
        ? (key.currentContext?.widget as CustomPaint).painter
        : null;

    if (painter is CirclePainter) {
      final isTouched = painter.hitTest(localPosition, size);

      // 状态变化时触发enter/exit
      if (isTouched != currentState) {
        setState(() {
          if (shapeName == "yellow") {
            _isYellowCircleTouched = isTouched;
          } else if (shapeName == "red") {
            _isRedCircleTouched = isTouched;
          }
        });
        print(isTouched ? "Enter $shapeName" : "Exit $shapeName");
      }
    }
  }
}

// 通用圆形Painter,实现精确hitTest
class CirclePainter extends CustomPainter {
  final Color color;
  final double radiusRatio; // 半径占容器尺寸的比例

  CirclePainter({required this.color, required this.radiusRatio});

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = color;
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width * radiusRatio;
    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;

  // 自定义hitTest:支持精确圆形检测或MBR检测
  bool hitTest(Offset position, Size size) {
    // --- 精确圆形检测 ---
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width * radiusRatio;
    final distance = (position - center).distance;
    return distance <= radius;

    // --- MBR检测(最小边界矩形)---
    // return position.dx >= 0 && position.dx <= size.width && position.dy >=0 && position.dy <= size.height;
  }
}

关键修改说明

  1. 全局Pointer监听:在Stack的父级添加Listener,这样能捕获所有触摸事件,不管起始点在哪里。
  2. 自定义hitTest:CirclePainter里实现了精确的圆形命中检测(你可以替换成注释里的MBR检测代码),这样能准确判断触摸点是否在形状范围内。
  3. 状态追踪:维护每个形状的触摸状态,当状态从false变为true时触发Enter,从true变为false时触发Exit。
  4. 坐标转换:通过GlobalKey获取RenderObject,将全局触摸坐标转换为CustomPaint内部的局部坐标,确保hitTest的准确性。

扩展优化建议

  • 如果有更多形状,可以把状态管理抽象成一个列表,避免重复代码。
  • 对于复杂形状,可以使用Path的contains方法实现更精确的hitTest。
  • 如果需要处理堆叠形状的事件优先级(比如红色圆形在黄色上面,触摸重叠区域时只触发红色),可以在_updateShapeTouchState里调整检查顺序。

内容的提问来源于stack exchange,提问作者Harkish

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:43:44