美文网首页Flutter 开发工具
Flutter列表分组吸顶

Flutter列表分组吸顶

作者: 倪大头 | 来源:发表于2021-07-13 09:26 被阅读0次

    先推荐官方插件:sticky_headers

    下面是用Stack + CustomScrollView实现的方式:

    bzpzl-e2se5.gif

    列表部分是CustomScrollView,吸顶的header利用Stack布局贴在CustomScrollView上层

    监听CustomScrollView的滑动,和每组小列表的header位置作比较,来确定显示哪个浮动header

    完整代码:

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    
    // viewForHeaderInSection 每个section的Header
    typedef ViewForHeaderInSection = Widget Function(
        BuildContext context, int section);
    
    // heightForHeaderInSection 每个sectionHeader的高度
    typedef HeightForHeaderInSection = double Function(int section);
    
    // numberOfRowsInSection 当前section的item个数
    typedef NumberOfRowsInSection = int Function(int section);
    
    // cellForRowAtIndexPath 每个section的item
    typedef CellForRowAtIndexPath = Widget Function(
        BuildContext context, IndexPath indexPath);
    
    class IndexPath {
      int section;
      int row;
    
      IndexPath(this.section, this.row);
    }
    
    class SectionTableView extends StatefulWidget {
      final ViewForHeaderInSection viewForHeaderInSection;
      final HeightForHeaderInSection heightForHeaderInSection;
      final CellForRowAtIndexPath cellForRowAtIndexPath;
      final int numberOfSections; // section个数
      final NumberOfRowsInSection numberOfRowsInSection;
      const SectionTableView({
        required this.viewForHeaderInSection,
        required this.heightForHeaderInSection,
        required this.cellForRowAtIndexPath,
        required this.numberOfSections,
        required this.numberOfRowsInSection,
      });
    
      @override
      _SectionTableViewState createState() => _SectionTableViewState();
    }
    
    class _SectionTableViewState extends State<SectionTableView> {
      List<Widget> slivers = []; // CustomScrollView子组件
      ScrollController scrollController = ScrollController();
      GlobalKey tableKey = GlobalKey(); // CustomScrollView Key
      List<GlobalKey> headerKeys = []; // 所有sectionHeader的Key
      var curHeaderIndex = 0.obs; // 当前悬浮的sectionHeader下标
      var curHeaderTransY = 0.0.obs;
    
      @override
      void initState() {
        // 准备组件
        initSlivers();
    
        scrollController.addListener(() {
          // CustomScrollView 在屏幕中Y坐标
          double tableY = getWidgetOffsetY(tableKey);
          // print('tableY: $tableY');
    
          for (int i = 0; i < headerKeys.length; i++) {
            // i
            double curHeaderY = getWidgetOffsetY(headerKeys[i]);
    
            // i + 1
            double nextHeaderY = scrollController.position.maxScrollExtent;
            if ((i + 1) < headerKeys.length) {
              nextHeaderY = getWidgetOffsetY(headerKeys[i + 1]);
            }
    
            // 当前section高度
            double curSectionHeight = nextHeaderY - curHeaderY;
    
            if (((tableY - curSectionHeight) <= curHeaderY) &&
                (curHeaderY <= tableY)) {
              // 当前header在ScrollView中的OffsetY
              double curHeaderOffsetY = curHeaderY - tableY;
              // 当前section剩余高度(完全上划走前)
              double bottomMargin = curHeaderOffsetY + curSectionHeight;
              // 当前header size
              Size headerSize = getWidgetSize(headerKeys[i]) ?? Size(0, 0);
              if (bottomMargin < headerSize.height) {
                curHeaderTransY.value = headerSize.height - bottomMargin;
              } else {
                curHeaderTransY.value = 0.0;
              }
              // print(curHeaderTransY.value);
    
              if (curHeaderIndex.value != i) {
                curHeaderIndex.value = i;
                // print('第 ${curHeaderIndex.value} 个header悬浮');
              }
            }
          }
        });
    
        super.initState();
      }
    
      // 获取组件offset.Y
      double getWidgetOffsetY(GlobalKey key) {
        RenderBox? renderBox = key.currentContext?.findRenderObject() as RenderBox?;
        double offsetY = renderBox!.localToGlobal(Offset.zero).dy;
        return offsetY;
      }
    
      // 获取组件size
      Size? getWidgetSize(GlobalKey key) {
        RenderBox? renderBox = key.currentContext?.findRenderObject() as RenderBox?;
        return renderBox?.size;
      }
    
      @override
      void dispose() {
        scrollController.dispose();
        super.dispose();
      }
    
      void initSlivers() {
        headerKeys.clear();
        slivers.clear();
    
        List.generate(widget.numberOfSections, (section) {
          // section header
          GlobalKey headerKey = GlobalKey();
          Widget sectionHeader = SliverToBoxAdapter(
            child: Container(
              key: headerKey,
              child: widget.viewForHeaderInSection(context, section),
            ),
          );
    
          headerKeys.add(headerKey);
          slivers.add(sectionHeader);
    
          int rowCount = widget.numberOfRowsInSection(section);
          if (rowCount > 0) {
            // 当前section有item
            List<Widget> rows = [];
            rows = List.generate(rowCount, (row) {
              return SliverToBoxAdapter(
                child:
                    widget.cellForRowAtIndexPath(context, IndexPath(section, row)),
              );
            });
            slivers.addAll(rows);
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: _initSubviews(),
        );
      }
    
      Widget _initSubviews() {
        return Stack(
          children: [
            CustomScrollView(
              key: tableKey,
              shrinkWrap: true,
              controller: scrollController,
              slivers: slivers,
              physics: ClampingScrollPhysics(), // 禁用弹簧效果
            ),
            Obx(() {
              return Positioned(
                top: -curHeaderTransY.value,
                child: widget.viewForHeaderInSection(context, curHeaderIndex.value),
              );
            }),
          ],
        );
      }
    }
    

    使用:

    return Container(
          margin: EdgeInsets.only(top: 200),
          child: SectionTableView(
            numberOfSections: 4,
            viewForHeaderInSection: (BuildContext context, int section) {
              return Container(
                width: ScreenUtil().screenWidth,
                height: 50,
                color: Colors.white,
                child: Center(
                  child: Text(
                    'Header $section',
                    style: TextStyle(
                      fontSize: 28.sp,
                      color: Colors.black,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              );
            },
            heightForHeaderInSection: (int section) {
              return 50;
            },
            numberOfRowsInSection: (int section) {
              return 6;
            },
            cellForRowAtIndexPath: (BuildContext context, IndexPath indexPath) {
              return Container(
                height: 100,
                color: Utils.randomColor(),
                child: Center(
                  child: Text(
                    'Row ${indexPath.row}',
                    style: TextStyle(
                      fontSize: 22.sp,
                      color: Colors.black,
                    ),
                  ),
                ),
              );
            },
          ),
        );
    

    相关文章

      网友评论

        本文标题:Flutter列表分组吸顶

        本文链接:https://www.haomeiwen.com/subject/vkpjpltx.html