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

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('滑动触摸选项查看详情'),
            ],
          ),
        ),
      ),
    );
  }
}

关键说明

  1. 状态管理:通过_currentHoverIndex跟踪当前触摸的选项,状态变化时自动触发UI更新
  2. 坐标计算:将全局触摸坐标转换为本地坐标,结合三角函数计算每个选项的中心位置,通过距离判断触摸点是否在选项范围内
  3. 手势监听:用onPanUpdate跟踪触摸移动,onPanEnd处理触摸结束的清空逻辑,确保状态正确重置
  4. 性能优化:仅当选中索引变化时才调用setState,避免频繁重绘

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 16:25:56