美文网首页Flutter 学习之路
Flutter 中的 ListView 的使用、嵌套及监听

Flutter 中的 ListView 的使用、嵌套及监听

作者: Eren丶耶格尔 | 来源:发表于2019-08-03 14:55 被阅读7次

前言

在 Android、和 iOS 开发中,列表分别使用的是 Android 的 ListView 或 RecyclerView,iOS 的 UITableView 实现的。而在 Flutter 中实现这种需求使用的则是 ListView。


(一)ListView 的基本使用

在 Flutter 中,ListView 可以沿一个方向(垂直或水平)来排列其所有子 Widget,比如通讯录、优化卷、商品列表等。

ListView 提供了一个默认构造函数 ListView,我们可以通过设置它的 children 参数,很方便地将所有的子 Widget 包含到 ListView 中。

不过,这种创建方式要求提前将所有子 Widget 一次性创建好,而不是等到它们真正在屏幕上需要显示时才创建。所有性能会很差。因此,这种方式仅适用于列表中含有少量元素的场景。

代码如下所示:

body: ListView(
          children: <Widget>[
            ListTile(
              leading: Icon(Icons.map),
              title: Text('Map'),
              subtitle: Text('Map'),
            ),
            ListTile(
              leading: Icon(Icons.mail),
              title: Text('Mail'),
              subtitle: Text('Mail'),
            ),
            ListTile(
              leading: Icon(Icons.message),
              title: Text('Message'),
              subtitle: Text('Message'),
            ),
            ...
          ],
        )

其中,ListTile 是 Flutter 提供的用于快速构建列表项元素的一个组件元素,参数包含 leading(主导)、title(文本)、subtitle(副文本)等。

具体 ListTile 的使用细节,可以参考官方文档

效果图如下所示:


ListView 默认构造函数

除了默认的垂直方向布局外,ListView 还可以通过设置 scrollDirection 参数设置水平布局。

代码如下所示:

body: ListView(
  scrollDirection: Axis.horizontal,
  itemExtent: 140, // 宽度 140
  children: <Widget>[
    Container(
      color: Colors.pink,
    ),
    Container(
      color: Colors.black,
    ),
    Container(
      color: Colors.blue,
    ),
    Container(
      color: Colors.red,
    ),
    Container(
      color: Colors.green,
    ),
    Container(
      color: Colors.orange,
    ),
  ],
)

效果图如下所示:


水平滚动的 ListView

(二)ListView.builder 的使用

考虑到创建子 Widget 产生的性能问题,更好的方式是抽象出创建子 Widget 的方法,交由 ListView 统一管理,在真正需要展示该子 Widget 时再去创建。

ListView 的另一个构造函数 ListView.builder,则适用于子 Widget 比较多的场景。这个构造函数有两个关键参数:

  • itemBuilder,是列表项的创建方法。当列表滚动到相应位置时,ListView 会调用该方法创建对应的子 Widget。
  • itemCount,表示列表项的数量,如果为空,则表示 ListView 为无限列表。

代码如下所示:

body: ListView.builder(
    itemCount: 20,
    itemExtent: 50.0,
    itemBuilder: (BuildContext context, int index) => ListTile(
          title: Text("这是第$index个条目"),
        ))

效果图如下所示:


ListView.builder 构造函数

(三)ListView 分割线

在 ListView 中,有两种方式支持分割线:

  • 一种是,在 itemBuilder 中,根据 index 的值动态创建分割线,也就是将分割线视为列表项的一部分;
  • 另一种是,使用 ListView 的另一个构造方法 ListView.separated,单独设置分割线的样式。

与 ListView.builder 抽离出了子 Widget 的构造方法类似,ListView.separated 抽离出了分割线的创建方法 separatorBuilder,以便根据 index 设置不同样式的分割线。

代码如下所示:

body: ListView.separated(
    separatorBuilder: (BuildContext context, int index) =>
        index % 2 == 0
            ? Divider(
                height: 1,
                color: Colors.orange,
              )
            : Divider(
                height: 1,
                color: Colors.blue,
              ),
    itemCount: 20,
    itemBuilder: (BuildContext context, int index) => ListTile(
          title: Text("这是第$index个条目"),
        ))

效果图如下所示:


ListView.separated 构造函数

(四)CustomScrollView

在使用 ListView 时,对于某些特殊交互场景,比如多个效果联动、嵌套滚动、精细滑动、视图跟随手势操作等,还需要嵌套多个 ListView 来实现。这时,各自视图的滚动和布局模型就是相互独立、分离的,就很难保证整个页面统一一致的滑动效果。

在 Flutter 中有一个专门的控件 CustomScrollView,用来处理多个需要自定义滚动效果的 Widget。在 CustomScrollView 中,这些彼此独立的、可滚动的 Widget 被统称为 Sliver

比如,ListView 的 Sliver 实现为 SliverList,AppBar 的 Sliver 实现为 SliverAppBar。这些 Sliver 不再维护各自的滚动状态,而是交由 CustomScrollView 统一管理,最终实现滑动效果的一致性。

下面以滚动视差为例,演示 CustomScrollView 的使用方法。

视差滚动是指让多层背景以不同的速度移动,在形成立体滚动效果的同时,还能保证良好的视觉体验。作为移动应用交互设计的热点趋势,越来越多的移动应用使用了这项技术。

以一个有着封面头图的列表为例,我们希望封面头图和列表这两层视图的滚动联动起来,当用户滚动列表时,头图会根据用户的滚动手势,进行缩小和展开。

经分析得出,要实现这样的需求,我们需要两个 Sliver:作为头图的 SliverAppBar,与作为列表的 SliverList。具体的实现思路是:

  • 在创建 SliverAppBar 时,把 flexibleSpace 参数设置为悬浮头图背景。flexibleSpace 可以让背景图显示在 AppBar 下方,高度和 SliverAppBar 一样;
  • 而在创建 SliverList 时,通过 SliverChildBuilderDelegate 参数实现列表项元素的创建;
  • 最后,将它们一并交由 CustomScrollView 的 slivers 参数统一管理。

代码如下所示:

body: CustomScrollView(
  slivers: <Widget>[
    SliverAppBar(//SliverAppBar 作为头图控件
      title: Text('CustomScrollView Demo'),// 标题
      floating: true,// 设置悬浮样式
      flexibleSpace: Image.network('https://upload.jianshu.io/users/upload_avatars/7534136/e21b56cd-6ac5-4ec9-ab00-4de058f63ae2.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/300/format/webp',fit:BoxFit.cover),// 设置悬浮头图背景
      expandedHeight: 300,// 头图控件高度
    ),
    SliverList(//SliverList 作为列表控件
      delegate: SliverChildBuilderDelegate(
            (context, index) => ListTile(title: Text('Item #$index')),// 列表项创建方法
        childCount: 100,// 列表元素个数
      ),
    ),
  ])

效果图如下所示:


CustomScrollView 视差滚动示例

(五)ScrollController 与 ScrollNotfication

在某些情况下,我们希望获取视图的滚动信息,并进行相应的控制,比如,列表是否已经滑到底(顶)部?如果快速回到列表顶部?列表滚动是否已经开始,是否已经停止?

对于前两个问题,可以使用ScrollController 进行滚动信息的监听,以及相应的滚动控制;最后一个问题,需要接受 ScrollNotfication 通知进行滚动事件的获取。

(1)ScrollController

在 Flutter 中,因为 Widget 并不是渲染到屏幕的最终视觉元素(RenderObject 才是),所以我们无法像原生的 Android 或 iOS 系统那样,向持有的 Widget 对象获取或设置最终渲染相关的视觉信息,而必须通过对应的组件控制器才能实现。

ListView 的组件控制器则是 ScrollControler,我们可以通过它来获取视图的滚动信息,更新视图的滚动位置。

一般而言,获取视图的滚动信息往往是为了进行界面的状态控制,因此 ScrollController 的初始化、监听及销毁需要与 StatefulWidget 的状态保持同步。

代码如下所示:

class _MyHomePageState extends State<MyHomePage> {
  ScrollController _controller; // listView 控制器
  bool isToTop = false; // 标示目前是否需要启动 Top 按钮

  @override
  void initState() {
    print('调用了 + initState');
    _controller = ScrollController();
    _controller.addListener(() {   // 为控制器注册滚动监听方法
      if (_controller.offset > 1000) {
        // 如果 ListView 已经向下滚动了 1000,则开启 Top 按钮
        setState(() {
          isToTop = true;
        });
      } else if (_controller.offset < 300) {
        // 如果 ListView 向下滚动距离不足 300,则禁用
        setState(() {
          isToTop = false;
        });
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        // 顶部 Top 按钮,根据 isToTop 变量判断是否需要注册滚动到顶部的方法
        body: Column(
          children: <Widget>[
            Container(
              child: RaisedButton(
                onPressed: (isToTop
                    ? () {
                        if (isToTop) {
                          _controller.animateTo(.0,
                              duration: Duration(milliseconds: 500),
                              curve: Curves.ease); // 做一个滚动到顶部的动画
                        }
                      }
                    : null),
                child: Text('Top'),
              ),
            ),
            Expanded(
                child: ListView.builder(
                    controller: _controller, // 初始化传入控制器
                    itemCount: 100, // 列表总数
                    itemBuilder: (context, index) => ListTile(
                          title: Text('Index:$index'),
                        )))
          ],
        ));
  }

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
    print('调用了 + dispose');
  }
}

效果图如下所示:


ScrollController 示例
  • 首先,我们在 State 的初始化方法里,创建了 ScrollController,并通过 _controller.addListener 注册了滚动监听方法回调,根据当前视图的滚动位置,判断当前是否需要展示“Top”按钮。
  • 随后,在视图构建方法 build 中,我们将 ScrollController 对象与 ListView 进行了关联,并且在 RaisedButton 中注册了对应的回调方法,可以在点击按钮时通过 _controller.animateTo 方法返回列表顶部。
  • 最后,在 State 的销毁方法中,我们对 ScrollController 进行了资源释放。
(2)ScrollNotfication

Flutter 中为了感知 ListView 的各类滚动事件,需要获取 ScrollNotification 通知。

ScrollNotification 通知的获取是通过 NotificationListener 来实现的。与 ScrollController 不同的是,NotificationListener 是一个 Widget,为了监听滚动类型的事件,需要将 NotificationListener 添加为 ListView 的父容器,从而捕获 ListView 中的通知。而这些通知,需要通过 onNotification 回调函数实现监听逻辑:

代码如下所示:

  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'ScrollController Demo',
        home: Scaffold(
            appBar: AppBar(title: Text('ScrollController Demo')),
            body: NotificationListener<ScrollNotification>(
              // 添加 NotificationListener 作为父容器
              onNotification: (scrollNotification) {
                // 注册通知回调
                if (scrollNotification is ScrollStartNotification) {
                  // 滚动开始
                  print('Scroll Start');
                } else if (scrollNotification is ScrollUpdateNotification) {
                  // 滚动位置更新
                  print('Scroll Update');
                } else if (scrollNotification is ScrollEndNotification) {
                  // 滚动结束
                  print('Scroll End');
                }
              },
              child: ListView.builder(
                itemCount: 30, // 列表元素个数
                itemBuilder: (context, index) =>
                    ListTile(title: Text("Index : $index")), // 列表项创建方法
              ),
            )));
  }

打印结果如下所示:


NotificationListener 监听结果

相比于 ScrollController 只能和具体的 ListView 关联后才可以监听到滚动信息;通过 NotificationListener 则可以监听其子 Widget 中的任意 ListView,不仅可以得到这些 ListView 的当前滚动位置信息,还可以获取当前的滚动事件信息 。

总结

在处理用于展示一组连续、可滚动的视图元素的场景,Flutter 提供了比原生系统更加强大的列表组件 ListView 和 CustomScrollView,不仅可以支持单一视图下可滚动 Widget 的交互模型及 UI 控制模型,对于某些特殊交互,需要嵌套多重可滚动 Widget 的场景,也提供了统一管理机制,最终实现体验一致的滑动效果。这些强大的组件,不仅可以开发出样式丰富的界面,更可以实现复杂的交互。

相关文章

网友评论

    本文标题:Flutter 中的 ListView 的使用、嵌套及监听

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