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

Flutter实现水平滚动ListView hover边缘显示箭头并点击滚动

实现亚马逊式水平ListView边缘悬停箭头滚动效果

要实现你需要的效果,核心是通过ScrollController控制滚动,结合MouseRegion监听鼠标位置来显示/隐藏箭头按钮,点击按钮触发滚动。以下是修改后的完整代码:

import 'package:flutter/material.dart';
import 'package:frontend_fst/utilities/constants.dart';
import 'package:frontend_fst/components/cards.dart';

class MainScreen extends StatefulWidget {
  static const String id = 'main_screen_web';

  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  late FSTCards cards = const FSTCards();
  // 初始化滚动控制器
  final ScrollController _scrollController = ScrollController();
  // 控制左右箭头的显示状态
  bool _showLeftArrow = false;
  bool _showRightArrow = false;
  // 边缘触发区域宽度(可根据需求调整)
  final double _edgeTriggerWidth = 40;

  @override
  void initState() {
    super.initState();
    // 监听滚动位置,更新箭头显示状态
    _scrollController.addListener(_updateArrowVisibility);
  }

  @override
  void dispose() {
    // 释放控制器,避免内存泄漏
    _scrollController.dispose();
    super.dispose();
  }

  // 更新箭头显示状态:判断是否滚到最左/最右端
  void _updateArrowVisibility() {
    setState(() {
      _showLeftArrow = _scrollController.offset > 0;
      _showRightArrow = _scrollController.offset < _scrollController.position.maxScrollExtent;
    });
  }

  // 滚动到左侧
  void _scrollLeft() {
    // 计算滚动距离:item宽度 + 分隔符宽度 + 左右padding(根据你的布局调整)
    final double scrollDistance = cards.preferredSize.width + 100 + 24;
    _scrollController.animateTo(
      _scrollController.offset - scrollDistance,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );
  }

  // 滚动到右侧
  void _scrollRight() {
    final double scrollDistance = cards.preferredSize.width + 100 + 24;
    _scrollController.animateTo(
      _scrollController.offset + scrollDistance,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );
  }

  // 监听鼠标位置,判断是否在边缘区域
  void _onMouseMove(PointerMoveEvent event) {
    final RenderBox renderBox = context.findRenderObject() as RenderBox;
    final Offset localPosition = renderBox.globalToLocal(event.position);

    setState(() {
      // 鼠标在左侧边缘
      _showLeftArrow = localPosition.dx <= _edgeTriggerWidth && _scrollController.offset > 0;
      // 鼠标在右侧边缘
      _showRightArrow = localPosition.dx >= (renderBox.size.width - _edgeTriggerWidth) && _scrollController.offset < _scrollController.position.maxScrollExtent;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: PreferredSize(
        preferredSize: const Size.fromHeight(70),
        child: AppBar(
          centerTitle: true,
          title: Image.asset(companyLogo, height: 80, width: 85),
        ),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(30),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              // 用Stack包裹ListView和箭头按钮
              SizedBox(
                height: 210,
                child: MouseRegion(
                  onHover: _onMouseMove,
                  child: Stack(
                    children: [
                      ListView.separated(
                        controller: _scrollController,
                        scrollDirection: Axis.horizontal,
                        padding: const EdgeInsets.all(12),
                        itemBuilder: (context, index) {
                          return cards;
                        },
                        separatorBuilder: (context, index) {
                          return const SizedBox(width: 100);
                        },
                        itemCount: 20,
                      ),
                      // 左侧箭头按钮
                      if (_showLeftArrow)
                        Positioned(
                          left: 0,
                          top: 0,
                          bottom: 0,
                          child: GestureDetector(
                            onTap: _scrollLeft,
                            child: Container(
                              width: _edgeTriggerWidth,
                              color: Colors.black.withOpacity(0.1),
                              child: const Icon(Icons.arrow_left, size: 40),
                            ),
                          ),
                        ),
                      // 右侧箭头按钮
                      if (_showRightArrow)
                        Positioned(
                          right: 0,
                          top: 0,
                          bottom: 0,
                          child: GestureDetector(
                            onTap: _scrollRight,
                            child: Container(
                              width: _edgeTriggerWidth,
                              color: Colors.black.withOpacity(0.1),
                              child: const Icon(Icons.arrow_right, size: 40),
                            ),
                          ),
                        ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

关键细节说明

  • ScrollController:绑定到ListView,用来获取当前滚动位置,以及触发平滑滚动操作。记得在组件销毁时调用dispose释放资源。
  • MouseRegion:监听鼠标在整个ListView区域内的移动,当鼠标进入左右边缘区域(宽度可通过_edgeTriggerWidth调整)时,显示对应的箭头按钮。
  • 箭头显示逻辑:除了鼠标悬停边缘,还会判断当前滚动位置——如果已经滚到最左端,左箭头会隐藏;滚到最右端,右箭头会隐藏,避免无效操作。
  • 滚动距离计算:_scrollLeft和_scrollRight里的滚动距离,是你的卡片宽度+分隔符宽度+ListView的padding总和,确保每次滚动刚好切换一组item,你可以根据实际布局调整这个数值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 17:42:48