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

Flutter Web实现列表项鼠标悬停放大且始终可见的方案

解决Flutter Web横向列表项悬停放大偏移问题

问题分析

你的代码存在几个关键问题导致放大后容器偏移:

  • 所有列表项共享_zoomHeight和_zoomWidth变量,悬停单个项时会影响其他项的状态
  • Positioned的偏移计算逻辑有误,且外层SizedBox固定高度限制了放大元素的显示空间
  • MouseRegion的嵌套逻辑导致onExit事件触发不及时,状态更新混乱

解决方案

  1. 为每个列表项单独维护缩放状态:用Map存储每个index对应的缩放尺寸,避免状态共享
  2. 使用Transform.scale实现居中放大:替代Positioned+AnimatedContainer的组合,更简洁地实现基于原位置的居中放大
  3. 优化MouseRegion逻辑:合并进入/退出事件,确保状态正确更新
  4. 调整外层容器设置:确保父容器允许内容溢出,且预留足够空间容纳放大元素

修改后的完整代码

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final double zoomTargetScale = 1.4; // 缩放比例,替代固定宽高
  final double originalScale = 1.0;
  final double itemHeight = 225;
  final double itemWidth = 400;

  // 为每个index单独维护缩放状态
  final Map<int, double> _scaleStates = {};

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            Image.network("https://source.unsplash.com/random/1600x900?cars"),
            Container(
              color: Colors.black87,
              child: Padding(
                padding: const EdgeInsets.all(20.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const SizedBox(height: 12),
                    const Text(
                      "List of items",
                      style: TextStyle(color: Colors.white),
                    ),
                    const SizedBox(height: 12),
                    // 预留放大空间,避免裁剪
                    Align(
                      alignment: Alignment.topLeft,
                      child: SizedBox(
                        height: itemHeight * zoomTargetScale,
                        child: ListView.separated(
                          clipBehavior: Clip.none,
                          scrollDirection: Axis.horizontal,
                          itemBuilder: (context, index) {
                            return buildCard(index);
                          },
                          separatorBuilder: (context, index) {
                            return const SizedBox(width: 12);
                          },
                          itemCount: 10,
                        ),
                      ),
                    ),
                    const SizedBox(height: 200),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget buildCard(int index) {
    // 初始化当前项的缩放状态
    _scaleStates.putIfAbsent(index, () => originalScale);

    return MouseRegion(
      onEnter: (_) {
        setState(() {
          _scaleStates[index] = zoomTargetScale;
        });
      },
      onExit: (_) {
        setState(() {
          _scaleStates[index] = originalScale;
        });
      },
      child: Stack(
        clipBehavior: Clip.none,
        alignment: Alignment.center,
        children: [
          // 原卡片:悬停时透明,避免遮挡放大层
          Opacity(
            opacity: _scaleStates[index] == originalScale ? 1 : 0,
            child: ClipRRect(
              borderRadius: BorderRadius.circular(20),
              child: Stack(
                children: [
                  Image.network(
                    "https://source.unsplash.com/random/400x225?sig=$index&cars",
                    width: itemWidth,
                    height: itemHeight,
                    fit: BoxFit.cover,
                  ),
                  Container(
                    color: Colors.black.withAlpha(100),
                    width: itemWidth,
                    height: itemHeight,
                  ),
                ],
              ),
            ),
          ),
          // 放大的卡片层:基于原位置居中缩放
          AnimatedScale(
            scale: _scaleStates[index]!,
            duration: const Duration(milliseconds: 400),
            curve: Curves.easeOut,
            child: ClipRRect(
              borderRadius: BorderRadius.circular(20),
              child: Stack(
                children: [
                  Image.network(
                    "https://source.unsplash.com/random/400x225?sig=$index&cars",
                    width: itemWidth,
                    height: itemHeight,
                    fit: BoxFit.cover,
                  ),
                  Container(
                    color: Colors.black.withAlpha(100),
                    width: itemWidth,
                    height: itemHeight,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

关键修改说明

  • 状态隔离:用_scaleStates Map存储每个列表项的缩放比例,彻底解决多元素状态冲突问题
  • AnimatedScale组件:直接基于原位置居中缩放,无需手动计算偏移量,完美解决偏移问题
  • 外层容器调整:将ListView的外层SizedBox高度设置为原高度×缩放比例,预留足够的放大空间,同时设置clipBehavior: Clip.none允许内容溢出
  • MouseRegion优化:合并进入/退出事件,确保状态更新准确,避免嵌套导致的事件丢失

注意:运行前需执行命令 flutter config --enable-web 开启Web支持

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 11:06:20