flutter-ListView

535 阅读6分钟

前言

flutter开发过程中用的除了文本等组件,用的比较多的就是滚动视图了,其中 ListView 使用频率是非常高的,这里介绍一下 ListView常用功能,以及简单定制一个下拉刷新、上拉加载更多

案例demo(list_view文件夹)

顺道上一张演示图片,下面案例中采用 tabbar 多种情况分开

image.png

ListView 常用属性

  1. scrollDirection: 列表的滚动方向,可选值有Axishorizontalvertical,可以看到默认是垂直方向上滚动;
  2. shrinkWrap: 列表都存在该属性,该属性默认为false,表示内部滚动方向无穷长,不会完全计算全部内容长度;如果该属性设置为true,会尽可能缩放滚动视图,一次性计算出所有内容,然后布局;一般嵌套滚动视图时内部设置为true,避免内部计算有问题,根据实际情况使用,一般不推荐,长列表过多计算可能会造成卡顿
  3. padding: 列表内边距;
  4. controller: 控制器,与列表滚动相关,比如监听列表的滚动事件(一般很少用);
  5. physics: 列表滚动至边缘后继续拖动的物理效果
  • AndroidiOS效果不同。默认Android会呈现出一个波纹状(对应ClampingScrollPhysics),而iOS上有一个回弹的弹性效果(对应BouncingScrollPhysics),需要设置成一样的,可以设置,注意BouncingScrollPhysics不满屏不支持滚动。
  • 如果想不同的平台上呈现各自的效果,且无论内容是否占满,都支持滚动的话,可以使用AlwaysScrollableScrollPhysics(默认也是这个,如果两端都像ios一样滚动,参数parent: BouncingScrollPhysics即可),它会根据不同平台自动选用各自的物理效果。
  • 如果想禁用在边缘的拖动效果,那可以使用NeverScrollableScrollPhysics
  1. shrinkWrap: 该属性将决定列表的长度是否仅包裹其内容的长度。当ListView嵌在一个无限长的容器组件中时,shrinkWrap必须为true,否则Flutter会给出警告;
  2. itemExtent: 子元素长度。当列表中的每一项长度是固定的情况下可以指定该值,有助于提高列表的性能(因为它可以帮助ListView在未实际渲染子元素之前就计算出每一项元素的位置);
  3. cacheExtent: 预渲染区域长度,ListView会在其可视区域的两边留一个cacheExtent长度的区域作为预渲染区域(对于ListView.buildListView.separated构造函数创建的列表,不在可视区域和预渲染区域内的子元素不会被创建或会被销毁);
  4. children: 容纳子元素的组件数组

基础ListView

ListView 就和 iosScollView 一样,就是一个普通的滚动式图,用于相对比短的列表展示,避免出屏无法显示等问题(如果点击去就会发现,都用到了 SliverChildListDelegate,后面在介绍 Sliver)

基础 ListView 使用如下所示,使用比较简单,往 ListView 里面塞就是了

ListView(
  //默认是垂直方向滑动,也可以竖直方向滑动,可以详细查看其它属性
  scrollDirection: Axis.vertical,
  //设置padding
  padding: const EdgeInsets.all(0),
  //设置滚动效果,这是比较常用的几个参数了
  //都和ios一样支持回弹,不填默认ios回弹,android水波纹
  physics: const AlwaysScrollableScrollPhysics(
    //两端都拥有弹性效果,若不填这里和默认physics不写一样
    parent: BouncingScrollPhysics()
  ),
  //回弹效果,但不满屏不能滑动
  // physics: const BouncingScrollPhysics(),
  children: [
    // const CardView(),
    Padding(
      padding: const EdgeInsets.all(10),
      child: Container(
        color: Colors.green,
        height: 300,
      ),
    ),
    Padding(
      padding: const EdgeInsets.all(10),
      child: Container(
        color: Colors.blue,
        height: 400,
      ),
    ),
  ],
);

image.png

ListView.builder

ListView.builderiosUITableView 一样,是支持复用的 ListView,长列表下性能有明显提升,且支持复用,我们一般的长列表就是由多个不同的 item 组成的 builder

psflutter 里面没有 section 这个东西,需要的话,可以通过 builder 自行封装,或者是用一个 builder 解决也行

ListView.builder(
  physics: const AlwaysScrollableScrollPhysics(
      parent: BouncingScrollPhysics() //两端都拥有弹性效果
  ),
  //这里是ListView的基本属性,都在
  padding: const EdgeInsets.all(5),
  //数量以及items,必须选项
  itemCount: 10,
  //单个 item 必填,可以根据不同类型来返回不同的 item
  //且 item 可以封装号传入
  itemBuilder: (context, index) {
    return SizedBox(
      height: 60,
      child: ListTile(
        leading: const SizedBox(
          width: 50,
          height: 50,
          child: CircleAvatar(
            backgroundColor: Colors.cyanAccent,
          ),
        ),
        title: Text(
          "标题:$index",
          style: const TextStyle(color: Colors.black, fontSize: 16),
        ),
        subtitle: Text(
          "我是第$index条内容",
          style:
          const TextStyle(color: Colors.black54, fontSize: 13),
        ),
      ),
    );
  },
);

image.png

ListView.separated

ListView.separatedListView.builder类似,可以理解为item外面多套了一层column罢了(实际实现不是这样的),我们可以根据需要进行选择使用哪一个组件(比较懒,肯定选择代码少的呀😂)

ListView.separated(
  physics: const AlwaysScrollableScrollPhysics(
      parent: BouncingScrollPhysics() //两端都拥有弹性效果
  ),
  //这里是ListView的基本属性,都在
  padding: const EdgeInsets.all(5),
  itemCount: 100,
  itemBuilder: (context, index) {
    return Container(
      color: Colors.white,
      alignment: Alignment.centerLeft,
      height: 60,
      margin: const EdgeInsets.only(left: 6, right: 6),
      child: Row(
        children: [
          Container(
            width: 50,
            height: 50,
            decoration: const BoxDecoration(
                color: Colors.red,
                borderRadius: BorderRadius.all(Radius.circular(10))),
          ),
          Expanded(
            child: Container(
              margin: const EdgeInsets.symmetric(horizontal: 20),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    "标题:$index",
                    style: const TextStyle(color: Colors.black, fontSize: 16),
                  ),
                  Text(
                    "我是第$index条内容",
                    style:
                    const TextStyle(color: Colors.black54, fontSize: 13),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  },
  separatorBuilder: (context, index) {
    //返回分割线,widget,自定义container都可以
    return const Divider(
      height: 0.5,
      indent: 60, //距离左侧
      endIndent: 0, //距离右侧
      color: Colors.grey,
    );
  },
);

image.png

下拉刷新、上拉加载更多

平常我们使用普通的 ListView 也就算了,一旦使用到 builder、separated那大概率会用到下拉刷新、上拉加载了,这里面我们讲解一个直接的简要的实现方式,只是提供一个方便,方便我们在使用类似的功能时,心里面有点数

ps: 实际上这种组件一般是使用三方,简单方便,侵入性差,而不用自己再次封装了走一遍流程了当然封装也没事😂,自定义可以参考这篇文章 juejin.cn

下面使用系统给定的 RefreshIndicator 构建下拉刷新,上拉就是底部多了一个 加载更多字样 提示而已

class RefreshListView extends StatefulWidget {
  const RefreshListView({Key? key}) : super(key: key);

  @override
  State<RefreshListView> createState() => _RefreshListViewState();
}

class _RefreshListViewState extends State<RefreshListView> {
  int count = 20;
  bool isLoading = false;

  Future<void> onRefresh() async {
    await Future.delayed(const Duration(seconds: 1));
    setState(() {
      count = 20;
    });
  }

  //直接加载更多
  void loadMore() {
    //加载标识,用于标识需要加载更多、加载中
    if (isLoading) return;
    setState(() {
      isLoading = true;
    });
    Future.delayed(const Duration(seconds: 1), () {
      setState(() {
        count += 10;
        isLoading = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    //使用系统给定的 RefreshIndicator 来构建下拉刷新
    return RefreshIndicator(
      //回调返回一个 Future 用来构建标记请求是否已经结束了
      onRefresh: onRefresh,
      child: NotificationListener(
        //滑动监听,通过更新类名来区分监听信息
        onNotification: (ScrollNotification notification) {
          //采用通知的方式来监听滑动,避免手势冲突等问题
          //下面通过 metrics 参数来判断,当前滑动位置是否已经接近底部,用来及时加载更多
          if (notification.metrics.maxScrollExtent - notification.metrics.pixels <= 100) {
             loadMore();
          }
          return false;
        },
        child: ListView.builder(
          physics: const AlwaysScrollableScrollPhysics(
              parent: BouncingScrollPhysics() //两端都拥有弹性效果
          ),
          //这里是ListView的基本属性,都在
          padding: const EdgeInsets.all(5),
          //数量以及items,必须选项
          itemCount: count + 1,
          itemBuilder: (context, index) {
            //当滑道最后一个时,显示加载更多item
            if (index >= count) {
              return renderBottom();
            }
            //默认显示我们自己的 item
            return SizedBox(
              height: 60,
              child: ListTile(
                leading: const SizedBox(
                  width: 50,
                  height: 50,
                  child: CircleAvatar(
                    backgroundColor: Colors.blueAccent,
                  ),
                ),
                title: Text(
                  "标题:$index",
                  style: const TextStyle(color: Colors.black, fontSize: 16),
                ),
                subtitle: Text(
                  "我是第$index条内容",
                  style:
                  const TextStyle(color: Colors.black54, fontSize: 13),
                ),
              ),
            );
          },
        ),
      ),
    );
  }

  Widget renderBottom() {
    if(isLoading) {
      return Container(
        padding: const EdgeInsets.symmetric(vertical: 15),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text(
              '加载中...',
              style: TextStyle(
                fontSize: 15,
                color: Color(0xFF333333),
              ),
            ),
            Padding(padding: EdgeInsets.only(left: 10)),
            SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(strokeWidth: 3),
            ),
          ],
        ),
      );
    } else {
      return Container(
        padding: const EdgeInsets.symmetric(vertical: 15),
        alignment: Alignment.center,
        child: const Text(
          '上拉加载更多',
          style: TextStyle(
            fontSize: 15,
            color: Color(0xFF333333),
          ),
        ),
      );
    }
  }
}

最后

快来试一下吧,后面我们撸其他的