Flutter:如何正确检测CustomPainter对象的触摸进入、移动与退出
解决Flutter中自定义形状的触摸进入/移动/退出事件检测问题
我来帮你搞定这个问题!你遇到的情况其实是Flutter里Pointer事件和hitTest机制的常见坑——默认的Listener和hitTest行为并不能满足“从外部滑入形状也能触发进入事件”的需求,而且你当前的代码还没真正实现基于形状(或MBR)的精确检测。咱们一步步来修复:
问题根源分析
- PointerEnter/Exit的局限性:这两个事件主要是为鼠标hover设计的,移动端触摸场景下,只有当指针一开始就在Widget范围内,或者通过鼠标hover进入时才会触发,从外部滑入的触摸不会触发这些事件。
- 默认hitTest的问题:你当前的
hitTest调用了super.hitTest(position),而默认实现是基于Widget的布局边界(也就是你的Container的矩形),不是你画的圆形形状。而且Stack里的Widget是按顺序堆叠的,后面的Widget会先接收事件,导致前面的Widget可能无法响应。 - 触摸事件的传递:如果触摸起始不在某个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; } }
关键修改说明
- 全局Pointer监听:在Stack的父级添加
Listener,这样能捕获所有触摸事件,不管起始点在哪里。 - 自定义hitTest:
CirclePainter里实现了精确的圆形命中检测(你可以替换成注释里的MBR检测代码),这样能准确判断触摸点是否在形状范围内。 - 状态追踪:维护每个形状的触摸状态,当状态从
false变为true时触发Enter,从true变为false时触发Exit。 - 坐标转换:通过
GlobalKey获取RenderObject,将全局触摸坐标转换为CustomPaint内部的局部坐标,确保hitTest的准确性。
扩展优化建议
- 如果有更多形状,可以把状态管理抽象成一个列表,避免重复代码。
- 对于复杂形状,可以使用
Path的contains方法实现更精确的hitTest。 - 如果需要处理堆叠形状的事件优先级(比如红色圆形在黄色上面,触摸重叠区域时只触发红色),可以在
_updateShapeTouchState里调整检查顺序。
内容的提问来源于stack exchange,提问作者Harkish
相关产品推荐
相关产品推荐

