Flutter Web移动端:如何检测用户触摸元素?
解决Flutter移动端Web触摸滑动弧度菜单的选项检测问题
移动端Web不支持MouseRegion的onEnter/onExit这类鼠标专属事件,要实现触摸滑动时识别划过的选项,需要通过触摸手势跟踪来模拟类似逻辑,以下是具体实现方案:
核心思路
通过监听全局触摸移动事件(onPanUpdate),实时计算触摸点是否落在某个选项的区域内,结合状态管理来触发选项的高亮/详情显示逻辑。
修改后的完整代码
import 'package:flutter/material.dart'; import 'dart:math'; import 'package:vector_math/vector_math.dart' show radians; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: const RadialMenuScreen(), ); } } class RadialMenuScreen extends StatefulWidget { const RadialMenuScreen({super.key}); @override State<RadialMenuScreen> createState() => _RadialMenuScreenState(); } class _RadialMenuScreenState extends State<RadialMenuScreen> { final List<IconData> flags = [ Icons.person, Icons.access_alarm, Icons.photo, Icons.phone, Icons.telegram, ]; const double distance = 125; const double circleSize = 45; // 记录当前触摸的选项索引,null表示没有触摸任何选项 int? _currentHoverIndex; // 判断触摸点是否在指定索引的选项范围内 bool _isPointInMenuOption(Offset globalPosition, int index) { // 获取当前组件的渲染对象,用于坐标转换 final renderBox = context.findRenderObject() as RenderBox; final localPosition = renderBox.globalToLocal(globalPosition); // 计算该选项的中心坐标(基于Stack的中心对齐) final centerX = MediaQuery.of(context).size.width / 2; final centerY = 550 / 2; // 对应外层SizedBox的height final optionCenterX = centerX + distance * cos(radians(-90 + 360 * index / flags.length)); final optionCenterY = centerY + distance * sin(radians(-90 + 360 * index / flags.length)); // 计算触摸点到选项中心的距离,小于半径则判定在范围内 final dx = localPosition.dx - optionCenterX; final dy = localPosition.dy - optionCenterY; final distanceToCenter = sqrt(dx * dx + dy * dy); return distanceToCenter <= circleSize; } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: SizedBox( height: 550, child: Column( spacing: 40, children: [ // 外层添加GestureDetector监听触摸移动 GestureDetector( behavior: HitTestBehavior.translucent, onPanUpdate: (details) { int? newHoverIndex; // 遍历所有选项,判断当前触摸点落在哪个选项内 for (int i = 0; i < flags.length; i++) { if (_isPointInMenuOption(details.globalPosition, i)) { newHoverIndex = i; break; } } // 只有当索引变化时才更新状态,避免不必要的重绘 if (newHoverIndex != _currentHoverIndex) { setState(() { // 之前有选中的选项,触发离开逻辑 if (_currentHoverIndex != null) { print('onExit: ${_currentHoverIndex}'); } _currentHoverIndex = newHoverIndex; // 新选中了选项,触发进入逻辑 if (_currentHoverIndex != null) { print('onEnter: ${_currentHoverIndex}'); } }); } }, onPanEnd: (_) { // 触摸结束时清空当前选中索引 setState(() { if (_currentHoverIndex != null) { print('onExit: ${_currentHoverIndex}'); } _currentHoverIndex = null; }); }, child: SizedBox( width: 350, height: 350, child: Stack( alignment: Alignment.center, children: [ for (int i = 0; i < flags.length; i++) Transform( transform: Matrix4.identity() ..translate( distance * cos(radians(-90 + 360 * i / flags.length)), distance * sin(radians(-90 + 360 * i / flags.length)), ), child: Material( color: Colors.transparent, child: GestureDetector( onTapUp: (_) { print('tapped option $i'); }, child: CircleAvatar( minRadius: circleSize, maxRadius: circleSize, // 根据当前选中索引高亮选项 backgroundColor: _currentHoverIndex == i ? Colors.green : Colors.blue, child: Icon(Icons.add), ), ), ), ) ], ), ), ), // 显示当前选中的选项详情 if (_currentHoverIndex != null) Text( '当前选中: ${flags[_currentHoverIndex!].name}', style: const TextStyle(fontSize: 18), ) else const Text('滑动触摸选项查看详情'), ], ), ), ), ); } }
关键说明
- 状态管理:通过
_currentHoverIndex跟踪当前触摸的选项,状态变化时自动触发UI更新 - 坐标计算:将全局触摸坐标转换为本地坐标,结合三角函数计算每个选项的中心位置,通过距离判断触摸点是否在选项范围内
- 手势监听:用
onPanUpdate跟踪触摸移动,onPanEnd处理触摸结束的清空逻辑,确保状态正确重置 - 性能优化:仅当选中索引变化时才调用
setState,避免频繁重绘
内容的提问来源于stack exchange,提问作者progNewbie
相关产品推荐
相关产品推荐

