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

Flutter:MouseRegion关联OverlayEntry的LayerLink更新——Tooltip位置异常

ListView Hover Overlay Tooltip位置不更新问题解决方案

问题场景

使用ListView.builder创建包裹在MouseRegion中的ListTile,鼠标进入MouseRegion时在其右上角显示关联的Overlay,仅显示当前Hover项的Overlay;当鼠标从MouseRegion移至Overlay时,Overlay保持显示。现有代码基本满足需求,但快速移动鼠标切换ListTile时,IconButton的Tooltip位置无法更新。

问题原因

  1. 快速切换时,旧的OverlayEntry未及时销毁,Tooltip仍绑定旧的位置信息
  2. CompositedTransformFollower依赖的LayerLink更新后,OverlayEntry未主动触发重建,导致位置未同步
  3. 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();
  }
}

关键修改说明

  1. 新增_replaceOverlay方法:切换ListTile时先销毁旧Overlay再创建新的,彻底清除旧位置关联
  2. OverlayEntry监听LayerLink:在builder中使用context.watch监听LayerLink变化,自动触发Overlay重建,同步新位置
  3. 优化Timer逻辑:进入ListTile时直接取消计时器,切换时替换Overlay,避免旧Tooltip残留
  4. dispose中清理资源:页面销毁时确保Overlay和Timer都被正确释放,避免内存泄漏

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 05:02:02