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

Flutter中如何实现Table组件表头吸顶(无需额外表格包)

实现Table表头吸顶(无需额外表格类包)

完全可以不用额外表格包实现这个需求,核心思路是通过滚动监听结合全局Key获取组件位置,动态控制一个"悬浮表头"的显示状态,步骤如下:

1. 核心实现逻辑

  • 用GlobalKey获取表格组件在屏幕中的位置信息
  • 通过ScrollController监听滚动偏移量
  • 当滚动到表头位置时,显示一个与原表头样式一致的悬浮表头;当整个表格滚出视野后,隐藏悬浮表头

2. 完整代码示例

import 'package:flutter/material.dart';

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

  @override
  State<StickyTablePage> createState() => _StickyTablePageState();
}

class _StickyTablePageState extends State<StickyTablePage> {
  final ScrollController _scrollController = ScrollController();
  final GlobalKey _tableKey1 = GlobalKey();
  final GlobalKey _tableKey2 = GlobalKey();
  
  // 控制两个表格的悬浮表头显示状态
  bool _showStickyHeader1 = false;
  bool _showStickyHeader2 = false;
  // 记录表格的位置范围(顶部和底部偏移)
  double _table1Top = 0;
  double _table1Bottom = 0;
  double _table2Top = 0;
  double _table2Bottom = 0;

  @override
  void initState() {
    super.initState();
    // 等待组件渲染完成后获取表格位置
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _getTablePosition(_tableKey1, isTable1: true);
      _getTablePosition(_tableKey2, isTable1: false);
    });
    // 添加滚动监听
    _scrollController.addListener(_onScroll);
  }

  void _getTablePosition(GlobalKey key, {required bool isTable1}) {
    final renderObject = key.currentContext?.findRenderObject() as RenderBox?;
    if (renderObject != null) {
      final offset = renderObject.localToGlobal(Offset.zero);
      final size = renderObject.size;
      if (isTable1) {
        _table1Top = offset.dy;
        _table1Bottom = offset.dy + size.height;
      } else {
        _table2Top = offset.dy;
        _table2Bottom = offset.dy + size.height;
      }
    }
  }

  void _onScroll() {
    final scrollOffset = _scrollController.offset;
    setState(() {
      // 判断表格1的悬浮表头是否需要显示:滚动偏移 >= 表格顶部,且 < 表格底部
      _showStickyHeader1 = scrollOffset >= _table1Top && scrollOffset < _table1Bottom;
      // 表格2同理
      _showStickyHeader2 = scrollOffset >= _table2Top && scrollOffset < _table2Bottom;
    });
  }

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

  // 复用的表头组件(确保悬浮表头和原表头样式一致)
  Widget _buildHeader() {
    return TableRow(
      decoration: BoxDecoration(color: Colors.grey[200]),
      children: const [
        Padding(
          padding: EdgeInsets.all(8.0),
          child: Text('列1', style: TextStyle(fontWeight: FontWeight.bold)),
        ),
        Padding(
          padding: EdgeInsets.all(8.0),
          child: Text('列2', style: TextStyle(fontWeight: FontWeight.bold)),
        ),
        Padding(
          padding: EdgeInsets.all(8.0),
          child: Text('列3', style: TextStyle(fontWeight: FontWeight.bold)),
        ),
      ],
    );
  }

  Widget _buildTableBody() {
    return TableRow(
      children: const [
        Padding(padding: EdgeInsets.all(8.0), child: Text('数据1')),
        Padding(padding: EdgeInsets.all(8.0), child: Text('数据2')),
        Padding(padding: EdgeInsets.all(8.0), child: Text('数据3')),
      ],
    );
  }

  Widget tableWidget1() {
    return Container(
      key: _tableKey1,
      child: Table(
        border: TableBorder.all(),
        children: [
          _buildHeader(),
          ...List.generate(20, (index) => _buildTableBody()),
        ],
      ),
    );
  }

  Widget tableWidget2() {
    return Container(
      key: _tableKey2,
      margin: const EdgeInsets.only(top: 40),
      child: Table(
        border: TableBorder.all(),
        children: [
          _buildHeader(),
          ...List.generate(20, (index) => _buildTableBody()),
        ],
      ),
    );
  }

  Widget otherWidget1() {
    return Container(
      height: 200,
      color: Colors.blue[100],
      child: const Center(child: Text('其他组件1')),
    );
  }

  Widget otherWidget2() {
    return Container(
      height: 200,
      color: Colors.green[100],
      child: const Center(child: Text('其他组件2')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Stack(
          children: [
            SingleChildScrollView(
              controller: _scrollController,
              child: Column(
                children: [
                  otherWidget1(),
                  tableWidget1(),
                  otherWidget2(),
                  tableWidget2(),
                  // 底部占位,确保能滚出表格
                  const SizedBox(height: 200),
                ],
              ),
            ),
            // 悬浮表头1
            if (_showStickyHeader1)
              Positioned(
                top: MediaQuery.of(context).padding.top,
                left: 0,
                right: 0,
                child: Table(
                  border: TableBorder.all(),
                  children: [_buildHeader()],
                ),
              ),
            // 悬浮表头2
            if (_showStickyHeader2)
              Positioned(
                top: MediaQuery.of(context).padding.top,
                left: 0,
                right: 0,
                child: Table(
                  border: TableBorder.all(),
                  children: [_buildHeader()],
                ),
              ),
          ],
        ),
      ),
    );
  }
}

关键说明

  • 用GlobalKey获取表格的渲染位置,确保能准确判断滚动范围
  • 滚动监听中判断当前偏移是否处于表格的上下边界之间,以此控制悬浮表头的显示
  • 悬浮表头的样式要和原表头完全一致,避免视觉差异
  • 如果有多个表格,只需为每个表格分配独立的GlobalKey和状态变量即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 15:27:31