Flutter无限滚动粘性列表插件sticky_infinite_list_plus的使用

Flutter无限滚动粘性列表插件sticky_infinite_list_plus的使用

简介

Infinite list with sticky headers.

此包是为了使无限列表在两个方向上渲染成为可能,并且带有粘性头部,这与其他大多数Dart Pub上的包不同。

该包支持多种头部定位方式。同时支持垂直和水平滚动列表。

它高度可定制,没有任何第三方依赖或原生(Android/iOS)代码。

除了默认用法外,此包还暴露了一些类,如果需要可以覆盖这些类。此外,一些类可以在Scrollable小部件内独立于InfiniteList容器使用。

此包使用CustomScrollView进行滚动,利用了Flutter提供的所有性能优势。

特性

  • 在无限列表中使用粘性头部
  • 支持多方向无限列表
  • 自定义粘性头部位置
  • 水平粘性列表支持
  • 动态内容滚动时构建头部
  • 动态计算内容滚动时的最小偏移量

Flutter版本低于1.20

如果你使用的Flutter版本低于1.20,请考虑使用v2.x.x版本。

迁移指南

如果你使用的是较旧的主要版本,请访问此迁移指南

属性未定义

如果你在构建过程中遇到类似错误消息,请先阅读此部分

示例

基础滚动示例

基础滚动示例

开始使用

安装包并导入:

import 'package:sticky_infinite_list_plus/sticky_infinite_list_plus.dart';

该包提供了InfiniteListInfiniteListItemStickyListItemStickyListItemRenderObject等类。

示例

简单示例

要开始使用带有粘性头部的无限列表,你需要创建一个指定构建器的InfiniteList实例。

无需指定任何额外配置即可使其工作:

import 'package:sticky_infinite_list_plus/sticky_infinite_list_plus.dart';

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return InfiniteList(
      builder: (BuildContext context, int index) {
        /// 构建器需要返回 InfiniteList
        return InfiniteListItem(
          /// 头部构建器
          headerBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
          /// 内容构建器
          contentBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
        );
      }
    );
  }
}

或者带覆盖内容的头部:

import 'package:sticky_infinite_list_plus/sticky_infinite_list_plus.dart';

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return InfiniteList(
      builder: (BuildContext context, int index) {
        /// 构建器需要返回 InfiniteList
        return InfiniteListItem.overlay(
          /// 头部构建器
          headerBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
          /// 内容构建器
          contentBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
        );
      }
    );
  }
}

状态

当min offset回调被调用或头部构建器被调用时,对象StickyState作为参数传递。

此对象描述了粘性头部的当前状态。

class StickyState<I> {
  /// 头部已经通过的位置
  ///
  /// 值可以介于0.0和1.0之间
  ///
  /// 如果它是 `0.0` - 粘性在最大起始位置
  ///
  /// `1.0` - 最大结束位置
  ///
  /// 如果 [InfiniteListItem.initialHeaderBuild] 为真,初始
  /// 头部渲染将处于位置 = 0
  final double position;

  /// 超出视口的像素数
  ///
  /// 如果 [InfiniteListItem.initialHeaderBuild] 为真,初始
  /// 头部渲染将处于偏移量 = 0
  ///
  /// 对于头部底部位置(或水平方向的右侧),偏移值也将是已滚动远离的像素数
  final double offset;

  /// 项目索引
  final I index;

  /// 如果头部处于粘性状态
  ///
  /// 如果 [InfiniteListItem.minOffsetProvider] 定义,
  /// 可能会在滚动时发出新的状态,但如果没有超过最小偏移值,[sticky] 将为假
  ///
  /// 当 [InfiniteListItem.minOffsetProvider] 被调用时,[sticky] 总是为 `false`。因为对于最小偏移量计算,
  /// 偏移量本身尚未定义
  final bool sticky;

  /// 内容项的高度。
  ///
  /// 如果 [InfiniteListItem.initialHeaderBuild] 为真,初始
  /// 头部渲染将被调用而没有此值
  final double contentSize;
}

扩展配置

可用配置

除了最小配置以开始使用外。

InfiniteList允许你定义滚动列表渲染的配置。

InfiniteList(
  /// 可选参数,用于传递 ScrollController 实例
  controller: ScrollController(),
  
  /// 可选参数
  /// 指定滚动方向
  ///
  /// 默认情况下,滚动将仅正向渲染 `InfiniteListDirection.forward`
  ///
  /// 如果你需要双向无限列表,请使用 `InfiniteListDirection.multi`
  direction: InfiniteListDirection.multi,
  
  /// 负向最大子元素计数。
  /// 
  /// 只有在 `direction: InfiniteListDirection.multi` 时才会使用
  ///
  /// 如果没有提供,负向滚动将无限滚动
  negChildCount: 100,
  
  /// 正向最大子元素计数
  ///
  /// 指定正向列表的元素数量
  ///
  /// 如果没有提供,正向滚动将无限滚动
  posChildCount: 100,
  
  /// ScrollView 锚点值
  anchor: 0.0,
  
  /// ScrollView 物理属性值
  physics: null,

  /// 项目构建器
  ///
  /// 应返回 `InfiniteListItem`
  builder: (BuildContext context, int index) {
    return InfiniteListItem(
      //...
    )
  }
)

InfiniteListItem 允许你指定更多选项来满足你的自定义需求。

InfiniteListItem(
  /// 查看类描述获取更多信息
  ///
  /// 当 [headerStateBuilder] 被指定时,强制初始头部渲染
  initialHeaderBuild: false,

  /// 简单的头部构建器
  /// 它将在每次列表项渲染时调用一次
  headerBuilder: (BuildContext context) {},
  
  /// 头部构建器,当头部应该改变其位置时会被调用
  ///
  /// 与前一种方法不同,它还会提供 `state` 的头部位置
  ///
  /// 这个回调比 [headerBuilder] 优先级更高,因此如果两者都提供了,[headerBuilder] 将被忽略
  headerStateBuilder: (BuildContext context, StickyState<int> state) {},
  
  /// 内容构建器
  contentBuilder: (BuildContext context) {},
  
  /// 最小偏移量提供者
  ///
  /// 此回调在每次头部位置更改时被调用,以定义头部何时应粘贴到底部
  ///
  /// 如果此方法返回 `0`,
  /// 头部将在列表项可见于视口之前保持粘性状态
  ///
  /// 如果此方法未提供或返回空值,头部将在偏移量等于头部大小时保持粘性
  minOffsetProvider: (StickyState<int> state) {},
  
  /// 头部相对于主轴方向的对齐方式
  ///
  /// 有关更多信息,请参阅 [HeaderMainAxisAlignment]
  HeaderMainAxisAlignment mainAxisAlignment: HeaderMainAxisAlignment.start,

  /// 头部相对于交叉轴方向的对齐方式
  ///
  /// 有关更多信息,请参阅 [HeaderCrossAxisAlignment]
  HeaderCrossAxisAlignment crossAxisAlignment: HeaderCrossAxisAlignment.start,

  /// 相对定位头部相对于滚动轴的位置
  ///
  /// 仅适用于相对头部定位
  HeaderPositionAxis positionAxis: HeaderPositionAxis.mainAxis,

  /// 列表项填充,详见 [EdgeInsets]
  EdgeInsets padding: const EdgeInsets.all(8.0),
  
  /// 滚动方向
  ///
  /// 可以是垂直或水平(参见 [Axis] 类)
  ///
  /// 此值也会影响底部或顶部边缘头部定位头部的行为
  scrollDirection: Axis.vertical,
);

示例

import 'package:sticky_infinite_list_plus/sticky_infinite_list_plus.dart';
import 'package:flutter/material.dart';

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return InfiniteList(

        /// 滚动控制器。可选
        controller: ScrollController(),

        /// 渲染双方向滚动。可选
        direction: InfiniteListDirection.multi,

        /// 渲染100个元素在负方向。可选
        ///
        /// 如果没有提供,负方向滚动将无限滚动
        ///
        /// 如果 [direction] 是 forward,则会被忽略
        ///
        /// 如果是 `null`,列表将无限滚动
        negChildCount: 100,

        /// 渲染100个元素在正方向。可选
        ///
        /// 如果没有提供,正方向滚动将无限滚动
        ///
        /// 如果是 `null`,列表将无限滚动
        posChildCount: 100,

        /// 视口锚点值。查看 [ScrollView] 文档获取更多信息
        anchor: 0.0,

        /// 项目构建器
        builder: (BuildContext context, int index) {
          /// 构建器需要返回 InfiniteList
          return InfiniteListItem(
            /// 带状态的头部构建器
            ///
            /// 每次头部改变其位置时都会被调用
            ///
            /// 如果不需要在每次位置变化时重新渲染头部 - 使用 [headerBuilder] 构建器
            headerStateBuilder: (BuildContext context, StickyState<int> state) {
              return Container(
                alignment: Alignment.center,
                width: 50,
                height: 50,
                child: Text("Header $index"),
                color: Colors.orange.withOpacity(state.position),
              );
            },

            /// 这只是一个示例
            ///
            /// 在你的应用程序中你应该使用或
            /// [headerBuilder] 或 [headerStateBuilder],
            /// 但不能同时使用两者
            ///
            /// 如果两者都被指定,这个调用者将被忽略
            headerBuilder: (BuildContext context) {
              return Container(
                alignment: Alignment.center,
                width: 50,
                height: 50,
                child: Text("Header $index"),
                color: Colors.orange,
              );
            },

            /// 内容构建器
            contentBuilder: (BuildContext context) {
              return Container(
                height: 300,
                child: Text(
                  "Content $index",
                  style: TextStyle(
                    color: Colors.white,
                  ),
                ),
                color: Colors.blueAccent,
              );
            },

            /// 在底部边缘粘贴到容器后的最小偏移量
            minOffsetProvider: (StickyState<int> state) => 50,

            /// 头部相对于主轴的对齐方式
            ///
            /// 默认为 [HeaderMainAxisAlignment.start]
            mainAxisAlignment: HeaderMainAxisAlignment.start,

            /// 头部相对于主轴的对齐方式
            ///
            /// 默认为 [HeaderCrossAxisAlignment.start]
            crossAxisAlignment: HeaderCrossAxisAlignment.start,

            /// 头部相对于滚动轴的位置
            ///
            /// 默认为 [HeaderPositionAxis.mainAxis]
            positionAxis: HeaderPositionAxis.mainAxis,
          );
        });
  }
}

需要更多的覆盖?

在大多数情况下,只需使用InfiniteListItem就足够了。

但在某些情况下,你可能需要为每个项目添加额外的功能。

幸运的是,你可以扩展并覆盖基本的InfiniteListItem类。

/// 通用类型 `I` 是索引类型,默认列表项使用 `int`

class SomeCustomListItem<I> extends InfiniteListItem<I> {
  /// 头部相对于主轴方向的对齐方式
  ///
  /// 有关更多信息,请参阅 [HeaderMainAxisAlignment]
  @override
  final HeaderMainAxisAlignment mainAxisAlignment = HeaderMainAxisAlignment.start;

  /// 头部相对于交叉轴方向的对齐方式
  ///
  /// 有关更多信息,请参阅 [HeaderCrossAxisAlignment]
  @override
  final HeaderCrossAxisAlignment crossAxisAlignment = HeaderCrossAxisAlignment.start;

  /// 相对定位头部相对于滚动轴的位置
  ///
  /// 有关更多信息,请参阅 [HeaderPositionAxis]
  @override
  final HeaderPositionAxis positionAxis = HeaderPositionAxis.mainAxis;

  /// 如果头部应该覆盖内容或不覆盖
  @override
  final bool overlayContent = false;

  /// 让项目构建器知道它是否应该监视
  /// 头部位置的变化
  ///
  /// 如果此值为 `true` - 它将在每次头部位置变化时调用 [buildHeader]
  @override
  bool get watchStickyState => true;

  /// 让项目构建器知道此类
  /// 提供头部
  ///
  /// 如果返回 `false` - [buildHeader] 将被忽略
  /// 并且永远不会被调用
  @override
  bool get hasStickyHeader => true;

  /// 此方法构建头部
  ///
  /// 如果 [watchStickyState] 为 `true`,
  /// 它将在每次头部位置变化时被调用
  /// 并提供 `state` 参数
  ///
  /// 否则,它将在初始渲染时仅被调用一次
  /// 并且每次头部位置变化都不会调用此方法。
  ///
  /// 在这种情况下,`state` 将为 `null`
  @override
  Widget buildHeader(BuildContext context, [StickyState<I> state]) {}

  /// 内容项构建器
  ///
  /// 此方法仅被调用一次
  @override
  Widget buildContent(BuildContext context) {}

  /// 在 initState 中调用(参见 [Statefull] 小部件 [State.initState])
  ///
  /// 关于 [Statefull] 小部件 `initState` 生命周期的更多信息,请参阅 Flutter 文档
  @protected
  @mustCallSuper
  void initState() {}

  /// 在 dispose 中调用(参见 [Statefull] 小部件 [State.dispose])
  ///
  /// 关于 [Statefull] 小部件 `dispose` 生命周期的更多信息,请参阅 Flutter 文档
  @protected
  @mustCallSuper
  void dispose() {}
}

更复杂的例子

除了覆盖列表项以在InfiniteList构建器中使用外,你还可以使用或扩展此包提供的StickyListItem

此类使用Stream来通知其父项头部位置的变化。

它还需要在Scrollable小部件和Viewport中呈现,因为它订阅了滚动事件并根据Viewport坐标计算位置(参见StickyListItemRenderObject类以获取更多信息)。

例如:

Widget build(BuildContext context) {
  return SingleChildScrollView(
    child: Column(
      children: <Widget>[
        Container(
          height: height,
          color: Colors.lightBlueAccent,
          child: Placeholder(),
        ),
        StickyListItem<String>(
          streamSink: _headerStream.sink, /// 流以更新滚动期间的头部
          header: Container(
            height: _headerHeight,
            width: double.infinity,
            color: Colors.orange,
            child: Center(
              child: StreamBuilder<StickyState<String>>(
                stream: _headerStream.stream, /// 流以更新滚动期间的头部
                builder: (_, snapshot) {
                  if (!snapshot.hasData) {
                    return Container();
                  }

                  final position = (snapshot.data.position * 100).round();

                  return Text('Positioned relative. Position: $position%');
                },
              ),
            ),
          ),
          content: Container(
            height: height,
            color: Colors.blueAccent,
            child: Placeholder(),
          ),
          itemIndex: "single-child",
        ),
        StickyListItem<String>.overlay(
          streamSink: _headerOverlayStream.sink, /// 流以更新滚动期间的头部
          header: Container(
            height: _headerHeight,
            width: double.infinity,
            color: Colors.orange,
            child: Center(
              child: StreamBuilder<StickyState<String>>(
                stream: _headerOverlayStream.stream, /// 流以更新滚动期间的头部
                builder: (_, snapshot) {
                  if (!snapshot.hasData) {
                    return Container();
                  }

                  final position = (snapshot.data.position * 100).round();

                  return Text('Positioned overlay. Position: $position%');
                },
              ),
            ),
          ),
          content: Container(
            height: height,
            color: Colors.lightBlueAccent,
            child: Placeholder(),
          ),
          itemIndex: "single-overlayed-child",
        ),
        Container(
          height: height,
          color: Colors.cyan,
          child: Placeholder(),
        ),
      ],
    ),
  );
}

更多关于Flutter无限滚动粘性列表插件sticky_infinite_list_plus的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter无限滚动粘性列表插件sticky_infinite_list_plus的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


sticky_infinite_list_plus 是一个用于 Flutter 的插件,它可以帮助你实现无限滚动和粘性头部效果的列表。这个插件非常适合需要展示大量数据并且希望在某些条件下保持头部固定的场景。

安装

首先,你需要在 pubspec.yaml 文件中添加依赖:

dependencies:
  sticky_infinite_list_plus: ^1.0.0

然后运行 flutter pub get 来安装依赖。

基本用法

以下是一个简单的示例,展示如何使用 sticky_infinite_list_plus 来创建一个无限滚动并且带有粘性头部的列表。

import 'package:flutter/material.dart';
import 'package:sticky_infinite_list_plus/sticky_infinite_list.dart';

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

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Sticky Infinite List Example'),
        ),
        body: InfiniteListExample(),
      ),
    );
  }
}

class InfiniteListExample extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return InfiniteList(
      builder: (BuildContext context, int index) {
        // 每个项的内容
        return ListTile(
          title: Text('Item $index'),
        );
      },
      itemCount: 1000, // 设置列表项的数量
      sticky: (int index) {
        // 设置哪些项是粘性头部
        return index % 10 == 0;
      },
      stickyBuilder: (BuildContext context, int index) {
        // 粘性头部的内容
        return Container(
          color: Colors.blue,
          padding: EdgeInsets.all(8.0),
          child: Text(
            'Sticky Header $index',
            style: TextStyle(color: Colors.white, fontSize: 18.0),
          ),
        );
      },
    );
  }
}
回到顶部