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

Flutter MouseRegion滚动时Hover临时中断,Projects按钮停止追踪鼠标

滚动时MouseRegion的Hover效果中断问题解决方案

Flutter应用中,滚动开始时MouseRegion内的hover效果会中断,导致Projects按钮停止追踪鼠标移动。调整MouseRegion属性后问题仍存在。

问题代码片段

class MainButtonStarting extends StatefulWidget {
  const MainButtonStarting({super.key});

  @override
  State<MainButtonStarting> createState() => _MainButtonStartingState();
}

class _MainButtonStartingState extends State<MainButtonStarting>
    with SingleTickerProviderStateMixin {
  var containerPosition = const Offset(
    10000,
    680,
  );

  double containerSize = 100;
  double containerOpacity = .5;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      containerPosition = Offset(
        MediaQuery.of(context).size.width - 410,
        670,
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onExit: (event) {
        containerOpacity = .5;

        containerPosition = Offset(
          MediaQuery.of(context).size.width - 410,
          670,
        );
        containerSize = 100;
      },
      onHover: (event) {
        setState(() {
          containerOpacity = 1;

          containerSize = 156;
          double containerWidth =
              100; // Adjust according to your container size
          double containerHeight =
              100; // Adjust according to your container size
          double mouseX = event.localPosition.dx;
          double mouseY = event.localPosition.dy;
          double screenWidth = MediaQuery.of(context).size.width;
          double screenHeight = MediaQuery.of(context).size.height;

          // Calculate new position for the container
          double newLeft = mouseX - containerSize / 2;
          double newTop = mouseY - containerSize / 2;

          // Ensure the container stays within the bounds of the screen
          if (newLeft < 0) {
            newLeft = 0;
          } else if (newLeft + containerWidth > screenWidth) {
            newLeft = screenWidth - containerWidth;
          }

          if (newTop < 0) {
            newTop = 0;
          } else if (newTop + containerHeight > screenHeight) {
            newTop = screenHeight - containerHeight;
          }

          // Update container position
          containerPosition = Offset(newLeft, newTop);
        });
      },
      child: Stack(
        children: [
          Container(
            height: 782,
            width: double.infinity,
            decoration: basicContainer,
            child: Stack(
              children: [
                ClipRRect(
                  borderRadius: BorderRadius.circular(12),
                  child: Image.asset(
                    'assets/images/newImages/bg_container.png',
                    fit: BoxFit.cover,
                    height: 782,
                  ),
                ),
                Positioned(
                  left: 50,
                  bottom: 40,
                  child: Container(
                    width: 875,
                    decoration: basicContainer,
                    child: Padding(
                      padding: const EdgeInsets.all(46.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          SizedBox(
                            width: 875,
                            child: Text(
                              'Embracing the interplay of artistic freedom and strategic order.',
                              textAlign: TextAlign.right,
                              style: GoogleFonts.gfsDidot(
                                color: Colors.white,
                                fontSize: 40,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                          ),
                          const Gap(74),
                          Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            crossAxisAlignment: CrossAxisAlignment.end,
                            children: [
                              SizedBox(
                                width: 537,
                                child: Text(
                                  'We welcome the invigorating fusion of boundless creativity and steadfast structure. It’s within this captivating balance that we discover our inspiration and drive to deliver exceptional results.\n\nOur design philosophy is rooted in our commitment to creativity, collaboration, and attention to detail. \n\nWe are committed to delivering the best possible design solutions that works for you.',
                                  style: GoogleFonts.inter(
                                    color: Colors.white,
                                    fontSize: 16,
                                    fontWeight: FontWeight.w300,
                                  ),
                                ),
                              ),
                              Container(
                                width: 163,
                                height: 55,
                                clipBehavior: Clip.antiAlias,
                                decoration: ShapeDecoration(
                                  shape: RoundedRectangleBorder(
                                    side: const BorderSide(
                                        width: 1, color: Color(0xFF6C6C6C)),
                                    borderRadius: BorderRadius.circular(6),
                                  ),
                                ),
                                child: const Center(
                                  child: Text(
                                    'About us',
                                    style: TextStyle(
                                      color: Colors.white,
                                      fontSize: 16,
                                      fontFamily: 'Inter',
                                      fontWeight: FontWeight.w300,
                                    ),
                                  ),
                                ),
                              )
                            ],
                          )
                        ],
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
          AnimatedPositioned(
            curve: Curves.fastEaseInToSlowEaseOut,
            duration: const Duration(milliseconds: 250),
            left: containerPosition.dx,
            top: containerPosition.dy,
            child: AnimatedOpacity(
              duration: const Duration(milliseconds: 250),
              opacity: containerOpacity,
              child: AnimatedContainer(
                duration: const Duration(milliseconds: 250),
                width: containerSize,
                height: containerSize,
                decoration: const ShapeDecoration(
                  color: Color(0x7F6C6C6C),
                  shape: OvalBorder(
                    side: BorderSide(width: 1, color: Colors.white),
                  ),
                ),
                child: Center(
                  child: Text(
                    'Projects',
                    style: GoogleFonts.inter(
                      color: Colors.white,
                      fontSize: 15,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

解决思路与方案

根源分析

滚动时Flutter事件分发会优先处理滚动事件,导致MouseRegion的onHover暂时停更,甚至误触发onExit。当前代码中onExit直接重置按钮状态,一旦滚动时触发该事件,按钮就会停止追踪鼠标。

具体解决方案

1. 结合ScrollController过滤滚动时的误触发

添加ScrollController监听滚动状态,滚动时忽略onExit的重置操作:

class _MainButtonStartingState extends State<MainButtonStarting>
    with SingleTickerProviderStateMixin {
  var containerPosition = const Offset(10000, 680);
  double containerSize = 100;
  double containerOpacity = .5;
  final ScrollController _scrollController = ScrollController();
  bool _isScrolling = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      containerPosition = Offset(
        MediaQuery.of(context).size.width - 410,
        670,
      );
    });
    // 监听滚动状态
    _scrollController.addListener(() {
      setState(() {
        _isScrolling = _scrollController.position.isScrollingNotifier.value;
      });
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onExit: (event) {
        // 滚动时不执行重置
        if (!_isScrolling) {
          setState(() {
            containerOpacity = .5;
            containerPosition = Offset(
              MediaQuery.of(context).size.width - 410,
              670,
            );
            containerSize = 100;
          });
        }
      },
      onHover: (event) {
        setState(() {
          containerOpacity = 1;
          containerSize = 156;
          // 用当前containerSize替换固定值100,避免边界判断错误
          double containerWidth = containerSize;
          double containerHeight = containerSize;
          double mouseX = event.localPosition.dx;
          double mouseY = event.localPosition.dy;
          double screenWidth = MediaQuery.of(context).size.width;
          double screenHeight = MediaQuery.of(context).size.height;

          double newLeft = mouseX - containerSize / 2;
          double newTop = mouseY - containerSize / 2;

          if (newLeft < 0) newLeft = 0;
          else if (newLeft + containerWidth > screenWidth) newLeft = screenWidth - containerWidth;

          if (newTop < 0) newTop = 0;
          else if (newTop + containerHeight > screenHeight) newTop = screenHeight - containerHeight;

          containerPosition = Offset(newLeft, newTop);
        });
      },
      // 给滚动组件绑定controller
      child: SingleChildScrollView(
        controller: _scrollController,
        child: Stack(
          // 原Stack内容保持不变
          children: [...],
        ),
      ),
    );
  }
}

2. 使用Listener替代MouseRegion

Listener的指针事件优先级更高,不会被滚动事件轻易打断,适合持续追踪鼠标位置:

@override
Widget build(BuildContext context) {
  return Listener(
    onPointerHover: (event) {
      setState(() {
        // 同原onHover逻辑
      });
    },
    onPointerExit: (event) {
      // 同优化后的onExit逻辑
    },
    child: Stack([...]),
  );
}

3. 确保MouseRegion覆盖完整滚动区域

如果滚动内容超出固定高度的容器,MouseRegion的范围需要动态匹配滚动区域的实际尺寸,避免鼠标滚动出原始容器时触发onExit。可以用LayoutBuilder获取父级尺寸,或者将MouseRegion放在Scrollable的外层。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 12:47:13