Flutter | 滚动组件,ListView,GridVIew等

2022-02-11 14:26:52 浏览数 (1)

可滚动组件

当组件内容超过当前显示视口(ViewPort)时,如果没有特殊处理,Flutter 就会提示 Overflow 错误,为此,Flutter 提供了多种可滚动组件,用于显示列表和长布局;

可滚动组件都直接或间接的包含一个 Scrollable 组件,因此他们都包含一些共同的属性:

代码语言:javascript复制
Scrollable({
  ...
  this.axisDirection = AxisDirection.down,
  this.controller,
  this.physics,
  @required this.viewportBuilder, //后面介绍
})
复制代码
  • axisDirection:滚动方向
  • physics:此属性接受一个 ScrollPhysics 类型对象,他觉得可滚动组件如何响应用户的操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界之后如何显示。默认情况下,Flutter 会根据具体的平台分别使用不同的 ScrollPhysics 对象,应用不同的显示效果,在 IOS 上会出现弹性效果,而在 android 上则会出现微光效果,如果你想在所有的平台下使用同一个效果,可以显式的指定一个固定的 ScrollPhysics 。Flutter SDK 中包含了两个 ScrollPhysics 的子类,他们可以直接使用
    • ClampingScrollPhysics:Android 下微光效果
    • BouncingScrollPhysics:IOS 下 弹性效果
  • controller:此属性接受一个 ScrollController 对象,该对象的主要作用是控制滚动位置和监听滚动事件 默认情况下,Widget 树中会有一个默认的 PrimaryScrollController ,如果子树中的滚动组件没有显示的指定,则会使用这个默认的。 这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold 正是使用这种机制在 IOS 上实现了点击导航栏回到顶部的功能
Scrollbar

Scrollbar 是一个 Material 风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需要将 Scroolbar 作为可滚动组件的任意一个父级组件即可,如:

代码语言:javascript复制
Scrollbar(
  child: SingleChildScrollView(
    ...
  ),
);
复制代码

Scrollbar 和 CupertinoScrollbar 都是通过监听滚动通知来确定滚动条的位置的

CupertinoScrollbar

CupertinoScorllbar 是 IOS 风格的滚动条,如果你是用的是 Scrollbar,那么在 IOS 平台会自动切换为 CupertinoScrollbar

ViewPort 视口

在很多布局中都有 ViewPort 的概念,在 Flutter 中,术语 ViewPort (视口) ,如无特别说明,则是指一个 Widget 的实际显示区域;

例如,一个 ListView 的显示区域的高度是 800 像素,虽然其列表项总高度可能远远超过 800 像素,但是 ViewPort 任然是 800 像素

基于 Sliver 的延时构建

通常可滚动的组件会非常多,占用的总高度也会非常大;如果一次性将子组件全部构建出将会非常昂贵!

为此,Flutter 中提出了一个 Sliver(薄片) 概念,只有当 Sliver 出现在视口时才会去构建他,这种模型也被称为 基于 Sliver 的延时构建模型 。可滚动组件中有很多都支持 Sliver 的延时构建模型,如 ListViewGridView ,但是也有不支持改模型的 SingleChildScrollView

主轴和纵轴

在滚动组件的坐标描述中,通常滚动的方向称为主轴,非滚动方向称为 纵轴。由于可滚动组件的默认方向一般都是沿垂直方向,所以默认情况下主轴就是指垂直方向,水平方向同理

SingleChildScrollView

SingleChildScrollView 类似于 Android 中的 ScrollView ,它只能够接受一个子组件,定义如下:

代码语言:javascript复制
SingleChildScrollView({
  this.scrollDirection = Axis.vertical, //滚动方向,默认是垂直方向
  this.reverse = false, 
  this.padding, 
  bool primary, 
  this.physics, 
  this.controller,
  this.child,
})
复制代码

除了上面说过的以外,重点看一下 Reverseprimary 两个属性

  • reverse:官方文档的解释是:是否安州阅读相反的方向滑动,如 scrollDirection 值为 Axis.horizontal ,如果阅读方向是从右到左(取决于语言环境,阿拉伯语就是从右到左)。reverse 为 true 时, 滑动方向就是从右往左。 其实此属性的本质上是决定可滚动组件的初始滚动位置是在 还是在 ,如 false 时,初始位置在头,反之则在 尾
  • primary:指是否使用 widget 树中默认的 PrimaryScrollController , 当滑动方向为垂直方向 (ScrollDirection 值为 Axis.vertical ) 并且没有指定 controller 时,primary 默认为 true。 需要注意的是,通常 SingleChildScrollView 只应用在期望内容不会超过屏幕太多时使用,这是因为 SingleChildScrollView 不支持 Sliver 的延时实例初始化模型,所以如果预计视口可能包含超出屏幕尺寸太多内容时,那么使用 SingleChildScrollView 将会非常昂贵(性能差),此时应该使用一些支持 Sliver 延时加载的可滚动组件,如 ListView
栗子
代码语言:javascript复制
class SingleChildScrollViewTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return Scrollbar(
      child: SingleChildScrollView(
        padding: EdgeInsets.all(16),
        child: Column(
          children: str.split("").map((e) => Text(e,textScaleFactor: 2,)).toList(),
        ),
      ),
    );
  }
}
复制代码

ListView

ListView 是最常用的可滚动组件之一,他可以沿一个方向线性排列所有子组件,并且他也支持基于 Sliver 的延时构建模型,ListView 的定义如下:

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

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

默认构造函数

默认构造函数有一个 children 参数,它接受一个 Widget 列表(List) 。这种方式只适合有少量的子组件的情况,因为这种需要将所有 children 都提前创建好(这需要大量的工作),而不是等子 widget 真正显示的时候在创建,也就是说默认构造函数构建的 ListView 没有应用基于 Sliver 的懒加载模型

实际上通过默认构造函数创建的 ListView 和使用 SingleChildScrolLView Column 的方式没有本质区别,下面看一个栗子:

代码语言:javascript复制
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'),
  ],
);
复制代码

可滚动组件通过一个 ist 来作为 children 属性时,只适用于组件较少的情况,这是一个通用的规律,并非 ListView 自己的特性,想 GridView 也是如此

ListView.builder

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

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

可滚动组件的构造函数如果需要一个列表项 Builder ,那么通过构造函数构建的通常就是支持 Sliver 的懒加载模型的,反正则不支持,这是个一般规律。

栗子:

代码语言:javascript复制
class ListTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 100, //列表项为100
      itemExtent: 50, //强制高度为50
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title:
              Text("$index", style: TextStyle(fontSize: 25, color: Colors.red)),
        );
      },
    );
  }
}
复制代码
ListView.separated

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

栗子:基数下面添加红色下划线,偶数下面添加蓝色分割线

代码语言:javascript复制
class ListTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Widget divider1 = Divider(
      color: Colors.blue,
    );
    Widget divider2 = Divider(
      color: Colors.red,
    );
    return getListViewSeparated(divider1,divider2);
  }

  Widget getListViewSeparated(Widget divider1, Widget divider2) {
    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;
      },
    );
  }
}
复制代码
无限加载列表

首先是模拟从异步获取数据,这里使用 english_words 包的 generateWordPairs 方法生成单词;当列表滑动到末尾时,判断是否有下一页,如果有则进行异步获取,并显示 loading,没有则显示没有更多了。

代码语言:javascript复制
class InfiniteListView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _InfiniteListViewState();
}

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

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: _words.length,
      itemBuilder: (BuildContext context, int index) {
        //如果到了末尾
        if (_words[index] == loadingTag) {
          //不足 100,继续获取数据
          if (_words.length - 1 < 100) {
            _retrieveData();
            return Container(
              padding: EdgeInsets.all(16),
              alignment: Alignment.center,
              child: SizedBox(
                width: 24,
                height: 24,
                child: CircularProgressIndicator(
                  strokeWidth: 2,
                ),
              ),
            );
          } else {
            return Container(
              alignment: Alignment.center,
              padding: EdgeInsets.all(16),
              child: Text(
                "没有更多了",
                style: TextStyle(color: Colors.grey),
              ),
            );
          }
        }
        return ListTile(
          title: Text(_words[index]),
        );
      },
      separatorBuilder: (BuildContext context, int index) {
        return Divider(
          height: .0,
          color: Colors.red,
        );
      },
    );
  }

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

效果如下:

实现下拉刷新
代码语言:javascript复制
 RefreshIndicator(
  onRefresh: _onRefresh,
  child: ListView.separated(
    itemCount: _words.length,
    itemBuilder://.....
  ),
)
     
Future<void> _onRefresh() async {
     return await Future.delayed(Duration(seconds: 3)).then((value) => {
         setState(() {
             _words.removeRange(0, _words.length - 1);
             _words.insertAll(
                 _words.length - 1,
                 //生成20个单词
                 generateWordPairs()
                 .take(20)
                 .map((e) => e.asPascalCase)
                 .toList());
         })
     });
 }
复制代码

onRefresh 接收一个返回值为 Future 的函数,

其中 async 表示这个函数是一部分,使用该关键字的函数必须返回一个 Future 对象

await 后面必须是一个 Fluture ,表示等等等异步执行完成,执行完成之后才会继续往下执行,then 是异步执行完成的回调

还有问题可以参考这篇文章

最终的效果如下:

添加固定列表头

很多时候我们需要给列表添加一个固定表头,比如实现一个商品列表,就需要在列表添加一个 商品列表 标题

以往的经验告诉我,直接使用一个线性组件,第一个为标题的头,第二个是 listView 即可,如下:

代码语言:javascript复制
Column(
  children: [
    Text("商品列表"),
    ListView.builder(
      itemCount: 100, //列表项为100
      itemExtent: 50, //强制高度为50
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("$index",
              style: TextStyle(fontSize: 25, color: Colors.red)),
        );
      },
    )
  ],
);
复制代码

结果如下:

代码语言:javascript复制
The following assertion was thrown during performResize():
Vertical viewport was given unbounded height.
复制代码

意思是 ListView 的高度无法确定,所以解决的办法就是给 ListView 设置边界,我们可以使用 SizedBox 指定具体的高度:

代码语言:javascript复制
children: [
  Text("商品列表"),
  SizedBox(
    height: 400,
    child: ListView.builder(
      itemCount: 100, //列表项为100
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("$index",
              style: TextStyle(fontSize: 25, color: Colors.red)),
        );
      },
    ),
  )
]
复制代码

这种方式是可以实现的,但是由于 listView 的高度是固定的,就会导致底部留白,这种情况可以使用屏幕的高度 减去状态类,导航栏,头部的高度。如果有用到其他的组件,则减去其高度即可

代码语言:javascript复制
SizedBox(
  //Material 中,状态类,导航栏,ListTile 高度分别是 24,56,,5
  height: MediaQuery.of(context).size.height - 24 - 56 - 56,
复制代码

使用这种方式可以达到效果,但是实现的方式并不好,如有有人任意一个高度发生变化,就要修改代码

那么有什么方法可以自动拉伸 ListView 以填充屏幕剩余空间的方法吗?

可以使用 Fix 来完成,在 弹性布局中,可以使用你Expanded 来自动拉伸组件的大小,并且 Column 是继承自 Fix,所以可以直接使用 Column Expanded 来实现

代码语言:javascript复制
Column(
  children: [
    Text("商品列表"),
    Expanded(
      child: ListView.builder(
        itemCount: 100, //列表项为100
        itemBuilder: (BuildContext context, int index) {
          return ListTile(
            title: Text("$index",
                style: TextStyle(fontSize: 25, color: Colors.red)),
          );
        },
      ),
    )
  ],
);
复制代码

使用这种方式,则可以实现 ListView 的自动拉伸,效果如下:

总结

上面主要介绍了 ListView 的公共参数和构造函数,不同的构造对应了不同列表的生成模型,如果需要自定义列表生成模型,可以通过 ListView.custom 来定义,他需要实现一个 SliverChildDelegate 用来给 ListView 生成列表项组件;并且实现了上拉刷新,下拉加载,列表头等常见的样式。

GridView

GridView 可以构建一个二维网格布局,其默认的构造函数定义如下:

代码语言:javascript复制
GridView({
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,
  @required SliverGridDelegate gridDelegate, //控制子widget layout的委托
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
})
复制代码

GridView 和 ListView 的参数大多数都是相同的,含义也都是相同的,有疑问的可以翻到上面查看

  • gridDelegate:类型是 SliverGridDelegate,他的作用是控制 GridView 如何排列(layout) SliverGridDelegate 是一个抽象类,定义类 GridView Layout 的相关接口,子类需要通过实现他们来实现具体的布局算法 Flutter 中提供了两个 SliverGridView 的子类 SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent ,我们可以直接使用这两个类
SliverGridDelegateWithFixedCrossAxisCount

该子类实现了一横轴Wie固定数量的子元素的 layout 算法,其构造函数为:

代码语言:javascript复制
SliverGridDelegateWithFixedCrossAxisCount({
  @required double crossAxisCount, 
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})
复制代码
  • crossAxisCount:横轴子元素的数量。此属性值确定后子元素在横轴上的长度就确定了,即 ViewPort 横轴长度除以 corssAxisCount 的商
  • mainAxisSpacing:主轴方向的间距
  • crosssAxisSpacing:横轴方向子元素间距
  • childAspectRatio:子元素在横轴长度和主轴长度的比例。由于 crossAxisCount 指定后,子元素横轴长度就会确定了,然后通过此参数值就可以确定子元素在主轴上的长度

可以看到,子元素的大小是通过 crossAxisCount 和 childAspectRatio 两个参数共同决定的。这里的子元素指的是子组件的最大显示空间,注意确保子组件的实际大小不要超出子元素的空间

栗子:

代码语言:javascript复制
class GridViewTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        child: GridView(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3, //横轴三个子 Widget
            childAspectRatio: 1.0, // 子组件宽高比
            mainAxisSpacing: 5, //主轴方向间距
            crossAxisSpacing: 10, //横轴方向间距
          ),
          children: [
            Container(
              color: Colors.red,
              child: Icon(Icons.accessible),
            ),
            Container(
              color: Colors.grey,
              child: Icon(Icons.map),
            ),
            Container(
              color: Colors.green,
              child: Icon(Icons.dashboard),
            ),
            Container(
              color: Colors.yellow,
              child: Icon(Icons.schedule),
            ),
            Container(
              color: Colors.blueAccent,
              child: Icon(Icons.translate),
            ),
            Container(
              color: Colors.brown,
              child: Icon(Icons.margin),
            ),
            Container(
              color: Colors.pinkAccent,
              child: Icon(Icons.fingerprint),
            )
          ],
        ),
      ),
    );
  }
}
复制代码

效果如下:

GridView.count

GridView.count 构造函数内部使用了 SliverGridDelegateWithFixedCrossAxisCount,通过这个构造方法可以款式的创建横轴固定数量子元素的 GridView,上面的代码等价于:

代码语言:javascript复制
GridView.count(
  crossAxisCount: 3,
  childAspectRatio: 1,
  mainAxisSpacing: 5,
  crossAxisSpacing: 10,
  children: [
    //..........
  ],
)
复制代码
SliverGridDelegateWithMaxCrossAxisExtent

该子类实现了一个横轴子元素为固定最大长度的 layout 算法,其构造函数为:

代码语言:javascript复制
SliverGridDelegateWithMaxCrossAxisExtent({
  double maxCrossAxisExtent,
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})
复制代码

maxCrossAxisExtent 为子元素在横轴上的最大长度,之所以是 最大长度,是应为横轴方向每个子元素的长度任然是等分的。

childAspectRatio:所指的子元素横轴和主轴的长度比为最终的长度比

其他的参数都和上面的一样

代码语言:javascript复制
GridView(
  gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
      maxCrossAxisExtent: 120, //最大长度不能超过120
      childAspectRatio: 2,
      mainAxisSpacing: 15,
      crossAxisSpacing: 10),
  children: [
    //.....
  ],
)
复制代码

效果如下:

GridView.extent

GridView.extent 构造函数内部使用了 SliverGridDelegateWithMaxCrossAxisExtent ,我们通过它可以快速的创建纵轴子元素为固定最大长度的 GridView,上面的代码等价于:

代码语言:javascript复制
GridView.extent(
   maxCrossAxisExtent: 120, //最大长度不能超过120
   childAspectRatio: 2,
   mainAxisSpacing: 15,
   crossAxisSpacing: 10
   children: <Widget>[
    //.....
   ],
 );
复制代码
GridView.builder

上面介绍的都需要一个子 Widget数组 作为其子元素,这些方式会提前创建好 widget,只适用于 widget 数量较小的时候,当 widget 比较多的时候,可以通过 GridView.builder 来动态创建子 Widget。GridView.builder 必须指定的构造参数有两个:

代码语言:javascript复制
GridView.builder(
 ...
 @required SliverGridDelegate gridDelegate, 
 @required IndexedWidgetBuilder itemBuilder,
)
复制代码

其中 itemBuilder 为子 Widget 的构建器

栗子

模拟从网络获取数据,然后使用 GridView 来展示

代码语言:javascript复制
class InfiniteGridView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _InfiniteGridViewState();
}

class _InfiniteGridViewState extends State<InfiniteGridView> {
  List<Widget> _widgets = [];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3, //每行三个
            childAspectRatio: 1, //宽高相等
            mainAxisSpacing: 15,
            crossAxisSpacing: 15),
        itemCount: _widgets.length,
        itemBuilder: (context, index) {
          if (index == _widgets.length - 1 && _widgets.length < 200) {
            _retrieveIcons();
          }
          return _widgets[index];
        },
      ),
    );
  }

  void _retrieveIcons() {
    Future.delayed(Duration(seconds: 1)).then((value) => {
          setState(() {
            _widgets.addAll([
              Container(
                color: Colors.yellow,
                child: Icon(Icons.schedule),
              ),
              Container(
                color: Colors.blueAccent,
                child: Icon(Icons.translate),
              ),
              Container(
                color: Colors.brown,
                child: Icon(Icons.margin),
              ),
            ]);
          })
        });
  }
}
复制代码

在 _retrieveIcons() 方法中模拟异步然后获取数据,成功后将数据保存,然后调用 setState 重新构建

在 itemBuilder 中,如果是最后一个,并且小于200 则加载数据,大于 200 之后则不加载数据

Pub 上有一个 flutter_staggered_grid_view 包,它实现了一个交错 GridView 的布局模型,可自行了解一下

CustomScrollView

CustomScrollView 是可以使用 Sliver 来自定义滚动模型的组件,他可以包含多种滚动模型

例如:一个页面,顶部是一个 GridView,底部是一个 ListView,需求是整个页面的滑动效果是统一的,即看起来他们是一个整体,如果单纯使用 GrdView ListView 来实现就不能保证统一的滑动效果,这个时候就可以使用 CustomScrollView,他相当于一个胶水,将这些彼此独立的可滚动组件粘起来。

可滚动组件的 Sliver

Sliver 通常指的是可滚动组件的子元素。但是在 Custom 中,需要粘起来可滚动的组件就是 CustomScrollView 的 Sliver 了,如果将 ListView 或者 GridView 作为 CustomScrollView 是不行的,因为他们本身是可滚动的组件,并不是 Sliver。

因此,为了能让可滚动组件能和 CustomScrollView 配合使用,Flutter 提供了一下可滚动组件的 Sliver 版,如 SliverList,SliverGrid 等,实际上 Sliver 版的可滚动组件和 非 Sliver 版的组件最大的区别就是前者不包含滚动模型(自身不能滚动),而后者包含滚动模型。,也正因为如此,CustomScrollView 才可以将多个 Sliver 粘在一起,这些 Sliver 共用 CustomScrollView 的 Scrollable,所以最终才实现了统一的滑动效果

Sliver 系列的 Widget 比较多,这里就不过多介绍,我们只需要记住他的特点即可,需要的时候查文档即可 上面说的 大多数 Sliver 都和可滚动组件对应,是由于还有一些如 SliverPadding,SliverAppBar 等是和可滚动组件无关的,他们主要是为了配合 CustomScrollView 一起使用,这是因为 CustomScrollView 的子组件都必须是 sliver

思考:在最开始的时候说过 sliver 是一种延时初始化的模型,只有当 Sliver 出现在视口时才会去构建他,但是 Sliver 版的 SliverList,SliverGrid 自身是不能滚动的,所以他们的子项就会失去延时初始化的作用 但是由于 SliverList 等本身是支持 Sliver 的,所以他们自己应该是支持 Sliver 的,而不是对应的子项

栗子:

代码语言:javascript复制
class CustomScrollViewTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            expandedHeight: 250, //展开高度
            flexibleSpace: FlexibleSpaceBar(
              title: Text("CustomScrollView"),
              background: Image.network(
                "https://gimg2.baidu.com/image_search/src=http://2e.zol-img.com.cn/product/52/870/ceFrX1n7iWFk6.jpg&refer=http://2e.zol-img.com.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1617810598&t=4b684cd92d7163552d610afba335282c",
                fit: BoxFit.cover,
              ),
            ),
          ),
          SliverPadding(
            padding: EdgeInsets.all(8),
            sliver: SliverGrid(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                  childAspectRatio: 4),
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Container(
                    alignment: Alignment.center,
                    color: Colors.cyan[100 * (index % 9)],
                    child: Text("grid item $index"),
                  );
                },
                childCount: 20,
              ),
            ),
          ),
          SliverFixedExtentList(
            itemExtent: 50,
            delegate:
                SliverChildBuilderDelegate((BuildContext context, int index) {
              return Container(
                alignment: Alignment.center,
                color: Colors.blue[100 * (index % 9)],
                child: Text("list item $index"),
              );
            }, childCount: 50),
          )
        ],
      ),
    );
  }
}
复制代码

代码分为三部分

1,SliverAppBar:SliverAppBar 对应 AppBar,两者不同之处在于 SliverAppBar 可以集成到 CustomScrollView 中,SliverAppBar 可以结合 FlexibleSpaceBar 实现 Material Design 中头部伸缩的模型

2,SliverGrid:被 SliverPadding 包裹,添加内边距

3,底部 SliverFiexdExtentList :是一个所有元素都为 50 像素的列表

运行效果如图:

0 人点赞