Flutter滚动组件之ListView使用方法详解

ListView

ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持基于Sliver的延迟构建模型。我们看看ListView的默认构造函数定义:

ListView({
  ...  
  //可滚动widget公共参数
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  EdgeInsetsGeometry padding,

  //ListView各个构造函数的共同参数  
  double itemExtent,
  bool shrinkWrap = false,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,

  //子widget列表
  List<Widget> children = const <Widget>[],
})

上面参数分为两组:第一组是可滚动组件的公共参数,前面已经介绍过,不再赘述;第二组是ListView各个构造函数(ListView有多个构造函数)的共同参数,我们重点来看看这些参数,:

  • itemExtent:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。
  • shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false。默认情况下,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
  • addAutomaticKeepAlives:该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。
  • addRepaintBoundaries:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。

注意:上面这些参数并非ListView特有,其它可滚动组件也可能会拥有这些参数,它们的含义是相同的。

默认构造函数

默认构造函数有一个children参数,它接受一个Widget列表(List)。这种方式适合只有少量的子组件的情况,因为这种方式需要将所有children都提前创建好(这需要做大量工作),而不是等到子widget真正显示的时候再创建,也就是说通过默认构造函数构建的ListView没有应用基于Sliver的懒加载模型。实际上通过此方式创建的ListView和使用SingleChildScrollView+Column的方式没有本质的区别。下面是一个例子:

ListView(
  shrinkWrap: true, 
  padding: const EdgeInsets.all(20.0),
  children: <Widget>[
    const Text('I\'m dedicating every day to you'),
    const Text('Domestic life was never quite my style'),
    const Text('When you smile, you knock me out, I fall apart'),
    const Text('And I thought I was so smart'),
  ],
);

再次强调,可滚动组件通过一个List来作为其children属性时,只适用于子组件较少的情况,这是一个通用规律,并非ListView自己的特性,像GridView也是如此。

ListView.builder

ListView.builder适合列表项比较多(或者无限)的情况,因为只有当子组件真正显示的时候才会被创建,也就说通过该构造函数创建的ListView是支持基于Sliver的懒加载模型的。下面看一下ListView.builder的核心参数列表:

ListView.builder({
  // ListView公共参数已省略  
  ...
  @required IndexedWidgetBuilder itemBuilder,
  int itemCount,
  ...
})
  • itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
  • itemCount:列表项的数量,如果为null,则为无限列表。

可滚动组件的构造函数如果需要一个列表项Builder,那么通过该构造函数构建的可滚动组件通常就是支持基于Sliver的懒加载模型的,反之则不支持,这是个一般规律。我们在后面在介绍可滚动组件的构造函数时将不再专门说明其是否支持基于Sliver的懒加载模型了。
下面看一个例子:

ListView.builder(
    itemCount: 100,
    itemExtent: 50.0, //强制高度为50.0
    itemBuilder: (BuildContext context, int index) {
      return ListTile(title: Text("$index"));
    }
);

运行效果如图所示:

ListView.separated

ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。

下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条绿色下划线。

class ListView3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //下划线widget预定义以供复用。  
    Widget divider1=Divider(color: Colors.blue,);
    Widget divider2=Divider(color: Colors.green);
    return ListView.separated(
        itemCount: 100,
        //列表项构造器
        itemBuilder: (BuildContext context, int index) {
          return ListTile(title: Text("$index"));
        },
        //分割器构造器
        separatorBuilder: (BuildContext context, int index) {
          return index%2==0?divider1:divider2;
        },
    );
  }
}

运行效果如图:

实例:无限加载列表

假设我们要从数据源异步分批拉取一些数据,然后用ListView展示,当我们滑动到列表末尾时,判断是否需要再去拉取数据,如果是,则去拉取,拉取过程中在表尾显示一个loading,拉取成功后将数据插入列表;如果不需要再去拉取,则在表尾提示"没有更多"。代码如下:

class InfiniteListView extends StatefulWidget {
  @override
  _InfiniteListViewState createState() => new _InfiniteListViewState();
}

class _InfiniteListViewState extends State<InfiniteListView> {
  static const loadingTag = "##loading##"; //表尾标记
  var _words = <String>[loadingTag];

  @override
  void initState() {
    super.initState();
    _retrieveData();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: _words.length,
      itemBuilder: (context, index) {
        //如果到了表尾
        if (_words[index] == loadingTag) {
          //不足100条,继续获取数据
          if (_words.length - 1 < 100) {
            //获取数据
            _retrieveData();
            //加载时显示loading
            return Container(
              padding: const EdgeInsets.all(16.0),
              alignment: Alignment.center,
              child: SizedBox(
                  width: 24.0,
                  height: 24.0,
                  child: CircularProgressIndicator(strokeWidth: 2.0)
              ),
            );
          } else {
            //已经加载了100条数据,不再获取数据。
            return Container(
                alignment: Alignment.center,
                padding: EdgeInsets.all(16.0),
                child: Text("没有更多了", style: TextStyle(color: Colors.grey),)
            );
          }
        }
        //显示单词列表项
        return ListTile(title: Text(_words[index]));
      },
      separatorBuilder: (context, index) => Divider(height: .0),
    );
  }

  void _retrieveData() {
    Future.delayed(Duration(seconds: 2)).then((e) {
      _words.insertAll(_words.length - 1,
          //每次生成20个单词
          generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
      );
      setState(() {
        //重新构建列表
      });
    });
  }

}

运行后效果如图所示:

代码比较简单,可以参照代码中的注释理解。需要说明的是,_retrieveData()的功能是模拟从数据源异步获取数据,我们使用english_words包的generateWordPairs()方法每次生成20个单词。

添加固定列表头

很多时候我们需要给列表添加一个固定表头,比如我们想实现一个商品列表,需要在列表顶部添加一个“商品列表”标题,期望的效果如图所示:

我们按照之前经验,写出如下代码:

@override
Widget build(BuildContext context) {
  return Column(children: <Widget>[
    ListTile(title:Text("商品列表")),
    ListView.builder(itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
    }),
  ]);
}

然后运行,发现并没有出现我们期望的效果,相反触发了一个异常;

Error caught by rendering library, thrown during performResize()。
Vertical viewport was given unbounded height ...

从异常信息中我们可以看到是因为ListView高度边界无法确定引起,所以解决的办法也很明显,我们需要给ListView指定边界,我们通过SizedBox指定一个列表高度看看是否生效:

... //省略无关代码
SizedBox(
    height: 400, //指定列表高度为400
    child: ListView.builder(itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
    }),
),
...

运行效果如图所示:

可以看到,现在没有触发异常并且列表已经显示出来了,但是我们的手机屏幕高度要大于400,所以底部会有一些空白。那如果我们要实现列表铺满除表头以外的屏幕空间应该怎么做?直观的方法是我们去动态计算,用屏幕高度减去状态栏、导航栏、表头的高度即为剩余屏幕高度,代码如下:

... //省略无关代码
SizedBox(
  //Material设计规范中状态栏、导航栏、ListTile高度分别为24、56、56 
  height: MediaQuery.of(context).size.height-24-56-56,
  child: ListView.builder(itemBuilder: (BuildContext context, int index) {
    return ListTile(title: Text("$index"));
  }),
)
...

运行效果如下图所示:

可以看到,我们期望的效果实现了,但是这种方法并不优雅,如果页面布局发生变化,比如表头布局调整导致表头高度改变,那么剩余空间的高度就得重新计算。那么有什么方法可以自动拉伸ListView以填充屏幕剩余空间的方法吗?当然有!答案就是Flex。前面已经介绍过在弹性布局中,可以使用Expanded自动拉伸组件大小,并且我们也说过Column是继承自Flex的,所以我们可以直接使用Column+Expanded来实现,代码如下:

@override
Widget build(BuildContext context) {
  return Column(children: <Widget>[
    ListTile(title:Text("商品列表")),
    Expanded(
      child: ListView.builder(itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
      }),
    ),
  ]);
}

运行后,和上图一样,完美实现了!

总结:

本节主要介绍了ListView的一些公共参数以及常用的构造函数。不同的构造函数对应了不同的列表项生成模型,如果需要自定义列表项生成模型,可以通过ListView.custom来自定义,它需要实现一个SliverChildDelegate用来给ListView生成列表项组件,更多详情请参考API文档。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Flutter实现文本滚动高亮效果的示例讲解

    目录 前言 功能实现 前言 最近有个需求是人工语音播放时文本能随语音朗读时像歌词滚动的效果. 原本第一考虑的时能随时间字体渐变成更改后的颜色, 有比较流畅的走马灯效果. 但最终实践了几次后发现要能够逐字逐行渐变有一些麻烦, 不好实现. 所以转而变为将字体直接将字体高亮, 一段文本区分成两个部分, 一个部分是高亮文本, 也就是已朗读的部分, 一个部分是剩下未朗读的非高亮文本. 通过时时渲染页面就能达成滚动高亮的效果. 功能实现 因为在Text中会存在两段文本, 所以就不能单只用Text组件, 而改

  • android使用flutter的ListView实现滚动列表的示例代码

    现如今打开一个 App,比如头条.微博,都会有长列表,随着我们不断地滑动,视窗内的内容也会不断地更新.今天就用 Flutter 实现一下这种效果. 这里的表现其实就相当于有一个固定长度的容器,然后超出的内容是不可见的,只有当你向上或向下滑动屏幕时,视窗外看不见的内容才会出现在视窗中.如果在 web 开发时,是需要容器加上样式 overflow: auto; 要想用 Flutter 实现,其实也是很简单的,因为 Flutter 为我们提供了 ListView 组件. ListView 主要有以下几

  • Flutter 滚动监听及实战appBar滚动渐变的实现

    介绍 在 Flutter 中滚动监听一般可以采用两种方式来实现,分别是ScrollController和NotificationListener这两种方式. ScrollController介绍 ScrollController 介绍一下ScrollController常用的属性和方法: offset:可滚动组件当前的滚动位置. jumpTo(double offset)跳转到指定位置,offset为滚动偏移量. animateTo(double offset,@required Duratio

  • Flutter Sliver滚动组件的演示代码

    目录 Flutter Sliver滚动组件 SliverList & SliverGrid SliverAppBar SliverPersistentHeader SliverToBoxAdapter CustomScrollView & NestedScrollView NestedScrollView+SliverAppBar+SliverFixedExtentList+ListView NestedScrollView+SliverAppBar+CustomScrollView 优化

  • Flutter滚动组件之SingleChildScrollView使用详解

    SingleChildScrollView SingleChildScrollView类似于Android中的ScrollView,它只能接收一个子组件.定义如下: const SingleChildScrollView({     Key key,     this.scrollDirection = Axis.vertical,     // 是否按照阅读方向相反的方向滑动,如:scrollDirection值为Axis.horizontal,如果阅读方向是从左到右(取决于语言环境,阿拉伯语

  • Flutter滚动组件之ListView使用方法详解

    ListView ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持基于Sliver的延迟构建模型.我们看看ListView的默认构造函数定义: ListView({   ...     //可滚动widget公共参数   Axis scrollDirection = Axis.vertical,   bool reverse = false,   ScrollController controller,   bool primary,   ScrollP

  • Android Flutter实现GIF动画效果的方法详解

    目录 前言 交错动画机制 代码实现 Interval 介绍 总结 前言 我们之前介绍了不少有关动画的篇章.前面介绍的动画都是只有一个动画效果,那如果我们想对某个组件实现一组动效,比如下面的效果,该怎么办? staggered animation 这个时候我们需要用到组合动效, Flutter 提供了交错动画(Staggered Animation)的方式实现.对于多个 Anmation 对象,可以共用一个 AnimationController,然后在不同的时间段执行动画效果.这就有点像 GIF

  • vuejs动态组件给子组件传递数据的方法详解

    通过子组件定义时候的props可以支持父组件给子组件传递数据,这些定义的props在子组件的标签中使用绑定属性即可,但是如果使用的是<component>动态组件,这个时候就没有显式的子组件标签,要给子组件传递数据需要在<component> 中进行绑定 <div class="app" id="deviceready"> <component :is="currentView" :user_name.s

  • vue 父组件通过$refs获取子组件的值和方法详解

    前言 在vue项目中组件之间的通讯是很常见的问题,同时也是很重要的问题,我们大致可以将其分为三种情况: 父传子:在父组件中绑定值,在子组件中用props接收 子传父:在父组件中监听一个事件,在子组件中利用$emit触发这个事件并带上数据作为第二个参数,这时父组件中监听事件的回调函数就会被调用,回调函数的参数就是子组件带上来的数据,这样就可以在父组件中使用子组件的数据了, 兄弟之间的传递:我们可以使用事件总线(eventBus)来轻松的解决,其实就是发布订阅者模式 今天我们要看的是父组件如何直接调

  • 在Vue2中注册全局组件的两种方法详解

    第一种:在main.js中直接注册 //引入 import FixedTop from '@/components/FixedTop //注册为全局组件 Vue.componet('FixedTop',FixedTop) //页面直接使用 <FixedTop /> 缺点:如果我们需要注册的全局组件非常多,那么需要一个一个引入,然后分别调用Vue.componet方法,main.js文件会变得很大很臃肿,不好维护,所以当需要注册的全局组件非常多的时候可以采用插件的形式注册 第二种:使用插件的形式

  • Vue父子组件属性传递实现方法详解

    目录 前言 组件之间属性的传递 父组件传递属性给子组件 子组件传递属性给父组件 前言 这节我们主要从案例出发,用Vue3的写法写父子组件之间的属性传递. 组件之间属性的传递 我们定义一个Rate组件,具有以下功能: 接收来自外部组件传入的参数,starCount代表星星个数.color代表星星颜色. 需要根据传入星星的个数,展示对应数量的星星. 父组件传递属性给子组件 那么在编写组件的时候,我们需要注意什么? 我们可以使用defineProps来规范传递数据的格式.可以结合withDefault

  • Android控件ListView使用方法详解

    Android控件ListView使用方法介绍,具体如下 一.ListView的简单用法 首先新建一个ListViewTest项目,并让Android Studio自动创建好活动.然后修改activity_main.xml中的代码,如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/re

  • 微信小程序图片轮播组件gallery slider使用方法详解

    本文实例为大家分享了微信小程序图片轮播组件的具体代码,供大家参考,具体内容如下 先上效果图: wxml <scroll-view scroll-y="true" style="height:200px" class="page-body" bindscrolltolower="loadMore"> <view class="swiper"> <swiper class=&quo

  • vue组件通信的多种方法详解

    目录 1. 父传子 2. 子传父 3. 非父子组件传值 4. vuex 5. refs 6. $children 7. $parent 8. provide & inject 9. $attrs 10. $listeners 总结 说起组件通信,可能大家耳熟能详的有以下5种: 1. 父传子 父组件通过自定义属性给子组件传值,子组件用props接收 2. 子传父 父组件在子组件标签上自定义事件,子组件通过$emit来触发自定义事件,并且可以传递参数 3. 非父子组件传值 eventBus事件总线:

  • Bootstrap滚动监听组件scrollspy.js使用方法详解

    其实滚动监听使用的情况还是很多的,比如导航居于右侧,当主题内容滚动某一块的时候,右侧导航对应的要高亮. 实现功能 1.当滚动区域内设置的hashkey距离顶点到有效位置时,就关联设置其导航上的指定项 2.导航必须是 .nav > li > a 结构,并且a上href或data-target要绑定hashkey 3.菜单上必须有.nav样式 4.滚动区域的data-target与导航父级Id(一定是父级)要一致. <div id="selector" class=&qu

随机推荐