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

Flutter自定义可滚动列表如何实现顶部项高亮及吸附效果?

Flutter 可变高度列表实现顶部高亮+吸附效果方案

针对你需要的可变高度列表项、滚动时顶部项高亮、滚动停止后吸附到顶部的需求,这里提供一个基于CustomScrollView+ScrollController的自定义实现方案,直接上代码和关键逻辑说明:

核心实现代码

import 'package:flutter/material.dart';

class ListItem {
  final String content;
  final double height;

  ListItem(this.content, this.height);
}

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

  @override
  State<StickyHighlightList> createState() => _StickyHighlightListState();
}

class _StickyHighlightListState extends State<StickyHighlightList> {
  final ScrollController _scrollController = ScrollController();
  final List<ListItem> _items = [
    ListItem('项1 - 高度150', 150),
    ListItem('项2 - 高度200', 200),
    ListItem('项3 - 高度100', 100),
    ListItem('项4 - 高度250', 250),
    ListItem('项5 - 高度180', 180),
    ListItem('项6 - 高度120', 120),
    ListItem('项7 - 高度220', 220),
  ];
  final List<GlobalKey> _itemKeys = [];
  int _highlightedIndex = 0;
  bool _isScrolling = false;

  @override
  void initState() {
    super.initState();
    // 给每个列表项分配唯一GlobalKey,用于获取布局位置
    for (int i = 0; i < _items.length; i++) {
      _itemKeys.add(GlobalKey());
    }
    _scrollController.addListener(_onScroll);
  }

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

  void _onScroll() {
    setState(() {
      _isScrolling = _scrollController.position.isScrollingNotifier.value;
    });

    // 滚动中实时更新高亮项,滚动结束触发吸附
    if (!_isScrolling) {
      _updateHighlightedIndex();
      _stickToTopItem();
    } else {
      _updateHighlightedIndex();
    }
  }

  void _updateHighlightedIndex() {
    final scrollOffset = _scrollController.offset;
    final topPadding = MediaQuery.of(context).padding.top;
    int newIndex = _highlightedIndex;

    for (int i = 0; i < _itemKeys.length; i++) {
      final renderBox = _itemKeys[i].currentContext?.findRenderObject() as RenderBox?;
      if (renderBox == null) continue;

      final itemTop = renderBox.localToGlobal(Offset.zero).dy - topPadding;
      final itemBottom = itemTop + renderBox.size.height;

      // 判断滚动偏移是否落在当前项的范围内
      if (scrollOffset >= itemTop && scrollOffset < itemBottom) {
        newIndex = i;
        break;
      }
      // 滚动到列表最顶部时,保持第一个项高亮
      if (scrollOffset < itemTop && i == 0) {
        newIndex = 0;
        break;
      }
    }

    if (newIndex != _highlightedIndex) {
      setState(() => _highlightedIndex = newIndex);
    }
  }

  void _stickToTopItem() {
    final renderBox = _itemKeys[_highlightedIndex].currentContext?.findRenderObject() as RenderBox?;
    if (renderBox == null) return;

    final topPadding = MediaQuery.of(context).padding.top;
    final itemTop = renderBox.localToGlobal(Offset.zero).dy - topPadding;
    final currentOffset = _scrollController.offset;

    // 如果当前高亮项顶部未对齐滚动视图顶部,触发吸附动画
    if (itemTop != currentOffset) {
      _scrollController.animateTo(
        itemTop,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('高亮吸附列表')),
      body: CustomScrollView(
        controller: _scrollController,
        slivers: [
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) {
                final item = _items[index];
                return Container(
                  key: _itemKeys[index],
                  height: item.height,
                  color: _highlightedIndex == index ? Colors.lightBlue[100] : Colors.white,
                  padding: const EdgeInsets.all(16),
                  child: Text(
                    item.content,
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: _highlightedIndex == index ? FontWeight.bold : FontWeight.normal,
                    ),
                  ),
                );
              },
              childCount: _items.length,
            ),
          ),
        ],
      ),
    );
  }
}

关键逻辑说明

  1. GlobalKey 跟踪项位置:每个列表项绑定一个GlobalKey,通过它可以获取项的RenderBox,计算出项在滚动视图中的顶部和底部偏移量,用于判断当前滚动位置对应的顶部项。
  2. 滚动监听与高亮更新:通过ScrollController监听滚动状态,滚动过程中实时计算当前顶部可见项,更新高亮样式;滚动停止时触发吸附逻辑。
  3. 吸附效果实现:滚动停止后,检查当前高亮项的顶部是否与滚动视图顶部对齐,若未对齐,调用animateTo滚动到对应位置,实现平滑吸附。
  4. 可变高度兼容:不管是固定高度还是内容自适应高度的项,RenderBox都会自动获取实际高度,无需额外处理。

优化与注意事项

  • 大数据列表优化:如果列表项数量极多,遍历所有GlobalKey会有性能损耗,可以只跟踪当前可见区域附近的项(结合ScrollController的position.viewportDimension计算可见范围)。
  • 动画参数调整:可以修改animateTo的duration和curve参数,让吸附过渡更符合你的交互需求。
  • 自定义高亮样式:除了背景色和字体粗细,还可以添加边框、阴影等样式,强化高亮效果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 15:33:14