Flutter:MouseRegion关联OverlayEntry的LayerLink更新——Tooltip位置异常
ListView Hover Overlay Tooltip位置不更新问题解决方案
问题场景
使用ListView.builder创建包裹在MouseRegion中的ListTile,鼠标进入MouseRegion时在其右上角显示关联的Overlay,仅显示当前Hover项的Overlay;当鼠标从MouseRegion移至Overlay时,Overlay保持显示。现有代码基本满足需求,但快速移动鼠标切换ListTile时,IconButton的Tooltip位置无法更新。
问题原因
- 快速切换时,旧的
OverlayEntry未及时销毁,Tooltip仍绑定旧的位置信息 CompositedTransformFollower依赖的LayerLink更新后,OverlayEntry未主动触发重建,导致位置未同步- Timer延迟销毁逻辑存在竞态,可能导致新旧Overlay重叠
解决方案
修改要点
- 切换ListTile时,先移除旧的
OverlayEntry再创建新的,避免残留 - 让
OverlayEntry监听LayerLink变化,调用markNeedsBuild刷新位置 - 优化Timer管理,确保每次操作都正确取消或重置计时器
修改后的完整代码
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; void main() { runApp(ChangeNotifierProvider<SelectedListTileProvider>( child: const MyApp(), create: (_) => SelectedListTileProvider(), )); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const MaterialApp( home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { OverlayEntry? entry; Timer? hideTimer; bool overlayIsActive = false; OverlayState? overlay; // 确保每次创建新Overlay前先移除旧的 void _replaceOverlay() { hideOverlay(); showOverlay(); } OverlayEntry buildOverlayEntry() { return OverlayEntry( builder: (context) { // 监听LayerLink变化,触发Overlay重建 final layerLink = context.watch<SelectedListTileProvider>().layerLink!; return Positioned( right: 1, child: CompositedTransformFollower( link: layerLink, showWhenUnlinked: false, offset: const Offset(0, -20), followerAnchor: Alignment.topRight, targetAnchor: Alignment.topRight, child: buildOverlay(), ), ); }, ); } void showOverlay() { overlay = Overlay.of(context); entry = buildOverlayEntry(); overlay!.insert(entry!); overlayIsActive = true; } void hideOverlay() { hideTimer?.cancel(); entry?.remove(); entry = null; overlayIsActive = false; } void enterListTile() { hideTimer?.cancel(); // 切换项时直接替换Overlay,而不是复用旧的 if (overlayIsActive) { _replaceOverlay(); } else { showOverlay(); } } void exitListTile() { hideTimer = Timer(const Duration(milliseconds: 100), hideOverlay); } Widget buildOverlay() { return MouseRegion( onEnter: (e) => hideTimer?.cancel(), onExit: (e) => hideTimer = Timer(const Duration(milliseconds: 100), hideOverlay), child: Material( child: Row( children: [ IconButton( icon: const Icon(Icons.delete), tooltip: 'delete message', onPressed: () {}, ), IconButton( icon: const Icon(Icons.edit), onPressed: () {}, tooltip: 'edit message (in progress)', ), IconButton( icon: const Icon(Icons.chat_bubble_rounded), onPressed: () {}, tooltip: 'start thread (in progress)', ), IconButton(icon: const Icon(Icons.emoji_emotions), onPressed: () {}, tooltip: 'in progress'), ], ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('MinimalListTile'), ), body: ListView.builder( scrollDirection: Axis.vertical, shrinkWrap: true, itemCount: 20, itemBuilder: (BuildContext context, int index) { final layerLink = LayerLink(); return CompositedTransformTarget( link: layerLink, child: MouseRegion( onEnter: (e) { context.read<SelectedListTileProvider>().layerLink = layerLink; enterListTile(); }, onExit: (e) => exitListTile(), child: ListTile( leading: const Icon(Icons.list), title: Text("List item $index"), onTap: () {}, ), ), ); }, ), ); } @override void dispose() { hideOverlay(); super.dispose(); } } class SelectedListTileProvider with ChangeNotifier { LayerLink? _layerLink; LayerLink? get layerLink => _layerLink; set layerLink(LayerLink? value) { _layerLink = value; notifyListeners(); } }
关键修改说明
- 新增
_replaceOverlay方法:切换ListTile时先销毁旧Overlay再创建新的,彻底清除旧位置关联 - OverlayEntry监听LayerLink:在builder中使用
context.watch监听LayerLink变化,自动触发Overlay重建,同步新位置 - 优化Timer逻辑:进入ListTile时直接取消计时器,切换时替换Overlay,避免旧Tooltip残留
- dispose中清理资源:页面销毁时确保Overlay和Timer都被正确释放,避免内存泄漏
内容的提问来源于stack exchange,提问作者MarkusT75
相关产品推荐
相关产品推荐

