Flutter学习-滚动的Widget

Posted GY-93

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习-滚动的Widget相关的知识,希望对你有一定的参考价值。

前言: 列表是移动端经常使用的一种视图展示方式,在Flutter中提供了ListView和GridView来实现列表和网格的布局展示

1. ListView

移动端数据量比较大时,我们都是通过列表来展示,比如 商品数据、聊天列表、通讯录、朋友圈等
ios中我们可以通过UITableView和UICollectionView来实现
在Flutter中我们也有对应的Widget,那就是ListView

1.1 ListView()初始化

ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。

一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。

官方文档对默认构造器的描述:
在这里插入图片描述

大概的意思: 默认构造函数显式接受List<Widget>的子元素。这个构造函数适用于只有少量子元素的列表视图,因为构造list需要为每个可能显示在列表视图中的子元素工作,而不仅仅是那些实际可见的子元素。(不管在屏幕上是不是可见的,都会把Item创建好,而不是现实Item的时候在创建)

flutter想要实现滚动效果,必须使用滚动的Widget

ListView({
    Key? key,
    Axis scrollDirection = Axis.vertical, //设置滑动方向
    bool reverse = false, //控制数据显示的顺序
    ScrollController? controller,
    bool? primary,
    ScrollPhysics? physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry? padding,
    this.itemExtent,// 设置Item的高度,不设置表示默认包裹内容
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double? cacheExtent,
    List<Widget> children = const <Widget>[],//Item
    int? semanticChildCount,
    ...
  }) 

1.1.1 基础使用

代码演练:

class GYHomeContent extends StatelessWidget {
  final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);

  @override
  Widget build(BuildContext context) {
    //实现圆角图片
    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
            child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle),
            color: Colors.orange,
          ),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
              child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。",
                  style: textStyle),color: Colors.green,),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
              child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: textStyle),color: Colors.red,),
        ),
      ],
    );
  }
}

效果图:
在这里插入图片描述

1.1.2 ListTitle

在开发中我们经常看见一种列表:有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。

示例代码:

class GYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //实现圆角图片
    return ListView(
        children: [
          ListTile(
            leading: Icon(Icons.people, size: 36,),
            title: Text("联系人"),
            subtitle: Text("联系人信息"),
            trailing: Icon(Icons.arrow_forward_ios),
          ),
          ListTile(
            leading: Icon(Icons.email, size: 36,),
            title: Text("邮箱"),
            subtitle: Text("邮箱地址信息"),
            trailing: Icon(Icons.arrow_forward_ios),
          ),
          ListTile(
            leading: Icon(Icons.message, size: 36,),
            title: Text("消息"),
            subtitle: Text("消息详情信息"),
            trailing: Icon(Icons.arrow_forward_ios),
          ),
          ListTile(
            leading: Icon(Icons.map, size: 36,),
            title: Text("地址"),
            subtitle: Text("地址详情信息"),
            trailing: Icon(Icons.arrow_forward_ios),
          )
        ]
    );
  }
}

在这里插入图片描述

1.1.3 List.generate 快速创建Item

代码演练:

class GYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //实现圆角图片
    return ListView(
        // 100 : 表示需要创建Item的个数, index: Item的下标
        children: List.generate(100, (index) {
      return ListTile(
        leading: Icon(
          Icons.people,
          size: 36,
        ),
        title: Text("联系人"),
        subtitle: Text("联系人信息 ${index + 1}"),
        trailing: Icon(Icons.arrow_forward_ios),
      );
    }));
  }
}

在这里插入图片描述

1.1.4 滚动方向

ListView默认是垂直方向滚动,我们也可以修改滚动方式,实现水平滚动

class GYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //实现圆角图片
    return ListView(
        scrollDirection: Axis.horizontal,
        // 100 : 表示需要创建Item的个数, index: Item的下标
        children: List.generate(100, (index) {
          return ListTile(
            leading: Icon(
              Icons.people,
              size: 36,
            ),
            title: Text("联系人"),
            // subtitle: Text("联系人信息 ${index + 1}"),
            // trailing: Icon(Icons.arrow_forward_ios),
          );
        }));
  }
}

在这里插入图片描述

1.2 ListView.build() 初始化

官网描述:
在这里插入图片描述

该构造方法创建ListView,适合Item比较多的情况, 并且是在ListView会在真正需要的时候去创建子Widget,而不是一开始就全部初始化好。

ListView.builder该方法有两个重要的参数:

  • itemBuilder:列表项创建的方法。当列表滚动到对应位置的时候,ListView会自动调用该方法来创建对应的子Widget。类型是IndexedWidgetBuilder,是一个函数类型
  • itemCount:表示列表项的数量,如果为空,则表示ListView为无限列表。
class GYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        itemCount: 100,
        itemExtent: 50,
        itemBuilder: (BuildContext ctx, int index) {
          return Text(
            "Hello World: $index",
            style: TextStyle(fontSize: 20),
          );
        });
  }
}

在这里插入图片描述

1.3 ListView.separated

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

  • ListView.separated没有参数itemExtent,所以不能够设置Item的高度, 只能自适应高度

示例代码:

class GYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.separated(
        itemBuilder: (BuildContext ctx, int index) {
          return Text(
            "Hello World: $index",
            style: TextStyle(fontSize: 20),
          );
        },
        separatorBuilder: (BuildContext ctx, int index){
          return Divider(
            color: Colors.red,
            height: 50, //设置分割线区域高度
            thickness: 10,//设置分割线的高度
            indent: 15, //设置分割线开始的距离
            endIndent: 15//设置分割线结束的距离,
          );
        },
        itemCount: 100
    );
  }
}

在这里插入图片描述

  • cacheExtent: 预加载高度,就设置高度越高,可能预加载的Item就越多

2. GridView

GridView用于展示多列的展示,在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。
在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。

2.1 GridView()

源码:

GridView({
    Key? key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController? controller,
    bool? primary,
    ScrollPhysics? physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry? padding,
    required this.gridDelegate, //必须传递一个参数,不能为null
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double? cacheExtent,
    List<Widget> children = const <Widget>[],
    int? semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
    Clip clipBehavior = Clip.hardEdge,
    ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
    String? restorationId,
  })

右上述源码可知,默认默认构造器方法中的参数列表中,有一个必传参数this.gridDelegate,该参数用于控制交叉轴的item数量或者宽度,参数的类型是SliverGridDelegate,但是该类是一个抽象类
我们查看该抽象类的子类如下:

在这里插入图片描述

除了SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent这两个子类示例外,其它的子类都是_开头属于私有子类

2.1.1 SliverGridDelegateWithFixedCrossAxisCount

const SliverGridDelegateWithFixedCrossAxisCount({
    required this.crossAxisCount, // 交叉轴的item个数
    this.mainAxisSpacing = 0.0, // 主轴的间距
    this.crossAxisSpacing = 0.0, // 交叉轴的间距
    this.childAspectRatio = 1.0, // 子Widget的宽高比
  })
  • crossAxisCount:我们只需要提供Item的个数,,然后Fluterr会根据屏幕的宽度自动计算出每个Item的宽度

  • mainAxisSpacing: 主轴方向Item和Item之间的距离

  • crossAxisSpacing:交叉轴轴方向Item和Item之间的距离

  • childAspectRatio: 这里不能够设置Item的明确的宽和高(没有提供相关参数),只能设置该参数来确定Item的宽度

  • 示例代码:

class GYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
        ),
      children:List.generate(100, (index) {
        return Container(
          color: Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1),
          child: Center(child: Text("Item:$index",style: TextStyle(fontSize: 24),)),
        );
      }) ,
    );
  }
}

在这里插入图片描述

2.1.2 SliverGridDelegateWithMaxCrossAxisExtent

源码

const SliverGridDelegateWithMaxCrossAxisExtent({
    required this.maxCrossAxisExtent,//交叉轴Item最大的宽度
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
    this.mainAxisExtent, // 主轴Item最大的高度
  })

代码示例:

class GYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView(
        gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 200,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
          mainAxisExtent: 200
        ),
      children:List.generate(100, (index) {
        return Container(
          color: Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1),
          child: Center(child: Text("Item:$index",style: TextStyle(fontSize: 24),)),
        );
      }) ,
    );
  }
}

在这里插入图片描述

  • 注意: 由运行结果可知,我们虽然设置maxCrossAxisExten的值是200,但是实际的运行结果Item的宽度是肯定没有200的,其实这里maxCrossAxisExten设置是只是Item的最大宽度,是一个范围值,并不是一个明确的值,所以Flutter会根据你设置的值,可能会调整布局,最终显示的Item的宽可能不是你想要的值。

2.2 GridView.count、GridView.extent

如果你觉得上面配置delegate比较麻烦,不想使用delegate,这里有其它的构造方法

  • GridView.count(): 构造方法创建相当于设置delegate为SliverGridDelegateWithFixedCrossAxisCount
  • GridView.extent():构造方法相当于设置delegate为SliverGridDelegateWithMaxCrossAxisExtent

2.3 GridView.build

ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget

示例代码:

class GYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
        ),
        itemCount: 100,
        itemBuilder: (BuildContext ctx, int index) {
          return Container(
            color: Color.fromRGBO(Random().nextInt(256), Random().nextInt(256),
                Random().nextInt(256), 1),
            child: Center(
                child: Text(
              "Item:$index",
              style: TextStyle(fontSize: 24),
            )),
          );
        });
  }
}

在这里插入图片描述

  • 注意:不管是ListView还是GridView,Item的点击需要我们自己添加收拾监听,构造器方法中并没有提供监听的属性(有点坑爹)
  • itemCount: 如果该属性没有设置,那么就是无限滚动,表示有无限个Item

2.4 屏幕四周间距问题

  • 如果希望两边也有间距的话,我们可以在外层包裹一个Padding,但是这里有一个问题,如果设置了上下的距离的话, 表格像上滑动的时候,会出现白色间距的问题
    在这里插入图片描述

3. Slivers

我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。
补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。

3.1 ListView、GridView的本质

通过查看源码我们知道不管是ListView还是GridView都是继承自BoxScrollView,而BoxScrollView 继承自ScrollView,而ScrollView继承自StateLessWidget
在这里插入图片描述

之前我们说过,想知道flutter最终渲染的是那个组件,我们可以查看build方法:

在这里插入图片描述

我们可以看到本质最终创建一个Scrollable的组件,然后调用了buildSlivers()方法获取sliver,而slivers里面才是我们真正可以滚动的东西

接下来我们查看buildSlivers()方法,可以发现该方法是一个抽象方法

 @protected
  List<Widget> buildSlivers(BuildContext context);

抽象方法子类肯定是需要实现, 那么我们可以查看BoxScrollView是否实现了该方法:
在这里插入图片描述

  • 通过上述代码我们可以得出一个结论:BoxScrollView只有一个sliver
  • BoxScrollView中的buildChildLayout是一个抽象法方法, ListView和GridView实现了这个抽象方法
  • ListView该方法代码:
@override
  Widget buildChildLayout(BuildContext context) {
    if (itemExtent != null) {
      return SliverFixedExtentList(
        delegate: childrenDelegate,
        itemExtent: itemExtent!,
      );
    }
    return SliverList(delegate: childrenDelegate);
  }
  • GridView的该方法代码:
@override
  Widget buildChildLayout(BuildContext context) {
    return SliverGrid(
      delegate: childrenDelegate,
      gridDelegate: gridDelegate,
    );
  }

我们发现该方法返回的都是 SliverXXX对象,我们在结合ListView和GridView的初始化方法

在这里插入图片描述

在这里插入图片描述

通过初始化方法可以看到 ,ListViewGridView 本质上其实是创建了对应的sliver对象

当你希望一个列表中既有ListView式的滚动,又有GridView的滚动, 那么使用BoxScrollView是实现不了的, 要想实现这种效果, 那么就需要CustomScrollView

3.2 Slivers的基本使用

因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver

const CustomScrollView({
    Key? key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController? controller,
    bool? primary,
    ScrollPhysics? physics,
    ScrollBehavior? scrollBehavior,
    bool shrinkWrap = false,
    Key? center,
    double anchor = 0.0,
    double? cacheExtent,
    this.slivers = const <Widget>[], //存放sliver的数组
    int? semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
    ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehaviorFlutter学习-滚动的Widget

Flutter学习 可滚动Widget 下

Flutter学习 可滚动Widget 上

Flutter开发之滚动Widget

Flutter学习-多子布局Widget

Flutter Stateful Widget 重新创建 State