Flutter 滚动控件篇-->GridView、CustomScrollView

3,646 阅读4分钟

文章已同步至【个人博客】,欢迎访问【我的主页】😃
文章地址:blog.fanjunyang.zone/archives/fl…

前面我们说了---Flutter 滚动控件篇-->ListView

这里说一下GridView,他和ListView的不同之处在于,GridView可以构建一个二维网格列表。

顺便再说一下CustomScrollView

GridView

源码示例

构造函数如下:

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一样,并且他们的含义也都相同。如果不知道的话,请点击:Flutter 滚动控件篇-->ListView

gridDelegate

这里只说一下gridDelegate,他的类型是SliverGridDelegate作用是控制GridView的子组件如何排列(layout)。

SliverGridDelegate是一个抽象类,Flutter中提供了两个SliverGridDelegate的子类SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent,我们可以直接使用,下面来介绍一下它们。

SliverGridDelegateWithFixedCrossAxisCount

该子类实现了一个横轴为固定数量子元素的layout算法,

源码示例

构造函数如下:

SliverGridDelegateWithFixedCrossAxisCount({
  @required double crossAxisCount, 
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})

属性解释

crossAxisCount

该属性表示横轴子元素的数量。
此属性值确定后子元素在横轴的长度就确定了,即ViewPort横轴长度除以crossAxisCount的商。

mainAxisSpacing

该属性表示主轴方向的间距。

crossAxisSpacing

表示横轴方向子元素的间距。

childAspectRatio

表示子元素在横轴长度和主轴长度的比例。
由于crossAxisCount指定后,子元素横轴长度就确定了,然后通过此参数值就可以确定子元素在主轴的长度。

代码示例

GridView(
	gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
	  crossAxisCount: 3, //横轴三个子widget
	  childAspectRatio: 1.0 //宽高比为1时,子widget
	  ),
	children: <Widget>[
	Icon(Icons.ac_unit),
	Icon(Icons.airport_shuttle),
	Icon(Icons.all_inclusive,color: Colors.red,),
	Icon(Icons.beach_access),
	Icon(Icons.cake),
	Icon(Icons.free_breakfast)
])

运行效果:

GridView.count

GridView.count构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount,我们通过它可以快速的创建横轴固定数量子元素的GridView

上面的示例代码等价于:

GridView.count(
	crossAxisCount: 3,
	childAspectRatio: 1.0,
	children: <Widget>[
	  Icon(Icons.ac_unit),
	  Icon(Icons.airport_shuttle),
	  Icon(Icons.all_inclusive,color: Colors.red,),
	  Icon(Icons.beach_access),
	  Icon(Icons.cake),
	  Icon(Icons.free_breakfast),
	],
)

SliverGridDelegateWithMaxCrossAxisExtent

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

源码示例

构造函数如下:

SliverGridDelegateWithMaxCrossAxisExtent({
  double maxCrossAxisExtent,
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})

属性解释

maxCrossAxisExtent

maxCrossAxisExtent为子元素在横轴上的最大长度,之所以是“最大”长度,是因为横轴方向每个子元素的长度仍然是等分的

举个例子,如果ViewPort的横轴长度是450,那么当maxCrossAxisExtent的值在区间[450/4,450/3)内的话,子元素最终实际长度都为112.5,而childAspectRatio所指的子元素横轴和主轴的长度比为最终的长度比

其他的参数和SliverGridDelegateWithFixedCrossAxisCount相同。

代码示例

GridView(
	padding: EdgeInsets.zero,
	gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
		maxCrossAxisExtent: 100.0, //其中横轴排列组件的数量与这个值有关
		childAspectRatio: 2.0 //宽高比为2
	),
	children: <Widget>[
	  Icon(Icons.ac_unit),
	  Icon(Icons.airport_shuttle),
	  Icon(Icons.all_inclusive),
	  Icon(Icons.beach_access),
	  Icon(Icons.cake),
	  Icon(Icons.free_breakfast),
	],
)

运行效果:

GridView.extent

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

上面的示例代码等价于:

GridView.extent(
	maxCrossAxisExtent: 100.0,
	childAspectRatio: 2.0,
	children: <Widget>[
	  Icon(Icons.ac_unit),
	  Icon(Icons.airport_shuttle),
	  Icon(Icons.all_inclusive),
	  Icon(Icons.beach_access),
	  Icon(Icons.cake),
	  Icon(Icons.free_breakfast),
	],
)

GridView.builder

ListView一样,children只适合子组件较少的情况下,
而当子组件较多时,我们可以通过GridView.builder来动态创建子组件。

源码示例

构造函数如下:

GridView.builder(
 ...
 @required SliverGridDelegate gridDelegate, 
 @required IndexedWidgetBuilder itemBuilder,
)

属性解释

其中gridDelegate属性在上面说过,这里只有一个itemBuilder,其实itemBuilder就是子组件的构建器。

代码示例

假设我们需要从一个异步数据源(如网络)分批获取一些Icon,然后用GridView来展示:

import 'package:flutter/material.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  List<IconData> _icons = []; //保存Icon数据

  @override
  void initState() {
    // 初始化数据
    _retrieveIcons();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text("GridView")),
        body: GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3, //每行三列
                childAspectRatio: 1.0 //显示区域宽高相等
                ),
            itemCount: _icons.length,
            itemBuilder: (context, index) {
              //如果显示到最后一个并且Icon总数小于100时继续获取数据
              if (index == _icons.length - 1 && _icons.length < 100) {
                _retrieveIcons();
              }
              return Icon(_icons[index]);
            }));
  }

  //模拟异步获取数据
  void _retrieveIcons() {
    Future.delayed(Duration(milliseconds: 200)).then((e) {
      setState(() {
        _icons.addAll([
          Icons.ac_unit,
          Icons.airport_shuttle,
          Icons.all_inclusive,
          Icons.beach_access,
          Icons.cake,
          Icons.free_breakfast
        ]);
      });
    });
  }
}

_retrieveIcons():在此方法中我们通过Future.delayed来模拟从异步数据源获取数据,每次获取数据需要200毫秒,获取成功后将新数据添加到_icons,然后调用setState重新构建。

itemBuilder中,如果显示到最后一个时,判断是否需要继续获取数据,然后返回一个Icon

运行效果:

补充

Flutter 的GridView默认子元素显示空间是相等的,但在实际开发中,你可能会遇到子元素大小不等的情况。

如图:

这时候,在pub.dev上面有个包flutter_staggered_grid_view,它实现了一个交错GridView的布局模型,可以很轻松的实现这种布局。

CustomScrollView

CustomScrollView是可以使用Sliver来自定义滚动模型(效果)的组件,它可以包含多种滚动模型

假设有一个页面,顶部需要一个GridView,底部需要一个ListView,而要求整个页面的滑动效果是统一的,即它们看起来是一个整体。如果使用GridView+ListView来实现的话,就不能保证一致的滑动效果,因为它们的滚动效果是分离的,所以这时就需要一个"胶水",把这些彼此独立的可滚动组件"粘"起来,而CustomScrollView的功能就相当于“胶水”。

可滚动组件的Sliver版

Sliver在前面讲过,有细片、薄片之意,在Flutter中,Sliver通常指可滚动组件子元素(就像一个个薄片一样)。但是在CustomScrollView中,需要粘起来的可滚动组件就是CustomScrollView的Sliver了。

如果直接将ListViewGridView作为CustomScrollView是不行的,因为它们本身是可滚动组件而并不是Sliver!

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

需要注意一点:CustomScrollView的子组件必须都是Sliver。

代码示例

import 'package:flutter/material.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: CustomScrollView(
        slivers: <Widget>[
          //AppBar,包含一个导航栏
          SliverAppBar(
            pinned: true,
            expandedHeight: 250.0,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('Demo'),
              background: Image.asset(
                "images/Test.jpg",
                fit: BoxFit.cover,
              ),
            ),
          ),

          SliverPadding(
            padding: const EdgeInsets.all(8.0),
            sliver: new SliverGrid(
              //Grid
              gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2, //Grid按两列显示
                mainAxisSpacing: 10.0,
                crossAxisSpacing: 10.0,
                childAspectRatio: 4.0,
              ),
              delegate: new SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  //创建子widget
                  return new Container(
                    alignment: Alignment.center,
                    color: Colors.cyan[100 * (index % 9)],
                    child: new Text('grid item $index'),
                  );
                },
                childCount: 20,
              ),
            ),
          ),
          //List
          new SliverFixedExtentList(
            itemExtent: 50.0,
            delegate: new SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              //创建列表项
              return new Container(
                alignment: Alignment.center,
                color: Colors.lightBlue[100 * (index % 9)],
                child: new Text('list item $index'),
              );
            }, childCount: 50 //50个列表项
                ),
          ),
        ],
      ),
    );
  }
} 

代码分为三部分:

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

  • 中间的SliverGrid:它用SliverPadding包裹以给SliverGrid添加补白。SliverGrid是一个两列,宽高比为4的网格,它有20个子组件。

  • 底部SliverFixedExtentList:它是一个所有子元素高度都为50像素的列表。

运行效果:


X_X