Flutter 学习 - 可滚动的 Widget

前言 -- 这是一篇陆陆续续写了三天的文章

在实际的开发中我们经常会遇到用列表展示数据,当内容超过一屏的时候可以进行滚动,来查看更多的内容,Android中这样的控件有ScrollView,ListView,RecycleView,GridView,ViewPager,那么在Flutter中都有哪些滚动的Widget呢,这篇文章我们将一一介绍。

正文 - Flutter中的可滚动Widget

  • SingleChildScrollView - 只能包含一个子Widget的可滚动Widget
    这个其实很像Android中的ScrollView,在Android中当我们用ScrollView包裹控件,如果控件很多的时候,我们一般会用LinearLayout或者RelativeLayout来包裹这些控件,相同的在Flutter中使用SingleChildScrollView来包裹很多子Widget的时候我们也需要用ListBody或者其他子Widget来嵌套,然后再在SingleChildScrollView中使用
    效果图
    演示3.gif

    下面看下使用方法:
SingleChildScrollView(
        padding: EdgeInsets.all(15),
        child: Text(
          "hello world 你好吗" * 500,
          style: TextStyle(fontSize: 18, color: Colors.green),
        ),
      ),

下面看下构造方法

const SingleChildScrollView({
    Key key,
    this.scrollDirection = Axis.vertical,//滑动方向,默认垂直方向,类型Axis
    this.reverse = false,//控制是从头开始滑动还是从尾开始滑动,false是从头开始滑动
    this.padding,//子Widget的内边距,
    bool primary,//是否是与父级关联的主滚动视图,当为true时,即使SingleChildScrollView中没有足够的内容也能滑动
    this.physics,//设置SingleChildScrollView滚动效果,类型ScrollPhysics
    this.controller,//可以控制 SingleChildScrollView 滚动的位置当 primary 为 true 时,controller 必须为 null,类型ScrollController
    this.child,//SingleChildScrollView 的列表项
    this.dragStartBehavior = DragStartBehavior.start,//确定处理拖动开始行为的方式
  }) : 
      ...;

ScrollPhysics

  • 如果想让 SingleChildScrollView 里没有足够的内容也能滑动,则设置为 AlwaysScrollableScrollPhysics()
  • 如果想让 SingleChildScrollView 在没有足够的内容的时候不能滑动,则设置为 ScrollPhysics()

DragStartBehavior

  • 如果设置为[DragStartBehavior.start],则在检测到拖动手势时将开始滚动拖动行为
  • 如果设置为[DragStartBehavior.down],它将在首次检测到向下事件时开始

ScrollController提供以下的几个功能:

  • 设置 SingleChildScrollView 滑动的初始位置
  • 可以控制 SingleChildScrollView 是否存储和恢复滑动的位置
  • 可以读取、设置当前滑动的位置
  • ListView - 可以线性排列子Widget的可滚动Widget
    ListView 共有四种使用方法,下面一一介绍
    1.使用默认的构造函数,给 children 属性赋值
    先看效果图
    演示4.gif

    看下代码实现
ListView(
          children: <Widget>[
              ListTile(
                  title: Text("title1"),
              ),
              ListTile(
                  title: Text("title2"),
              ),
              ListTile(
                  title: Text("title3"),
              ),
              ListTile(
                  title: Text("title4"),
              ),
              ListTile(
                  title: Text("title5"),
              ),
              ListTile(
                  title: Text("title6"),
              ),
              ListTile(
                  title: Text("title7"),
              ),
              ListTile(
                  title: Text("title8"),
              ),
              ListTile(
                  title: Text("title9"),
              ),
              ListTile(
                  title: Text("title10"),
              ),
              ListTile(
                  title: Text("title11"),
              ),
              ListTile(
                  title: Text("title12"),
              ),
              ListTile(
                  title: Text("title13"),
              ),
              ListTile(
                  title: Text("title14"),
              ),
              ListTile(
                  title: Text("title15"),
              ),
              ListTile(
                  title: Text("title16"),
              )    
          ],
      )

PS:这种方式只适用于那些只有少量子Widget的ListView,ListView在创建的时候,其子Widget也会一起创建
下面看下ListView的构造函数

ListView({
    Key key,
    Axis scrollDirection = Axis.vertical,//滑动的方向,默认为 Axis.vertical,垂直方向可滑动
    bool reverse = false,//控制 ListView 里列表项的排列顺序,是按照插入顺序排,还是按照插入顺序相反的方向排序
    ScrollController controller,//可以控制 ListView 滚动的位置
    bool primary,//是否是与父级关联的主滚动视图
    ScrollPhysics physics,//设置 ListView 的滚动效果
    bool shrinkWrap = false,//是否根据列表项的总长度来设置 ListView的长度
    EdgeInsetsGeometry padding,//ListView 的内边距
    this.itemExtent,//itemExtent 指的是列表项的大小
    bool addAutomaticKeepAlives = true,//是否用 AutomaticKeepAlive 来包列表项,默认为 true
    bool addRepaintBoundaries = true,// 是否用 RepaintBoundary 来包列表项,默认为 true
    bool addSemanticIndexes = true,//是否用 IndexedSemantics 来包列表项,默认为 true,使用 IndexedSemantics 是为了正确的用于辅助模式
    double cacheExtent,//ListView 可见部分的前面和后面的区域可以用来缓存列表项,这部分区域的 item 即使不可见,也会加载出来,所以当滑动到这个区域的时候,缓存的区域就会变的可见,cacheExtent 就表示缓存区域在可见部分的前面和后面有多少像素
    List<Widget> children = const <Widget>[],//ListView 的列表项
    int semanticChildCount,//提供语义信息的列表项的数量,默认为 ListView 的 item 的数量
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,//同SingleChildScrollView中解释
  }) : childrenDelegate = SliverChildListDelegate(
         children,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
       super(
        ...
       );

shrinkWrap: 当 shrinkWrap 为 false 时,ListView 会在滚动方向扩展到可占用的最大空间
当 shrinkWrap 为 true 时,ListView 在滚动方向占用的空间就是其列表项的总长度,但是这样会很耗性能,因为当其列表项发生变化时,ListView 的大小会重新计算

itemExtent: 如果滚动方向是垂直方向,则 itemExtent 代表的是 子Widget 的高度,
如果滚动方向为水平方向,则 itemExtent 代表的是 子Widget 的长度
如果 itemExtent 不为 null,则会强制所有 子Widget 在滑动方向的大小都为 itemExtent
指定 itemExtent 会比较高效,因为 子Widget 的高度就不需要在去计算,ListView 也可以提前知道列表的长度

addAutomaticKeepAlives: 在一个 lazy list 里,如果 子Widget 为了保证自己在滑出可视界面时不被回收,就需要把 addAutomaticKeepAlives 设为 true
当 子Widget 不需要让自己保持存活时,为了提升性能,请把 addAutomaticKeepAlives 设为 false
如果 子Widget 自己维护其 KeepAlive 状态,那么此参数必须置为false。

addRepaintBoundaries: 当 addRepaintBoundaries 为 true 时,可以避免列表项重绘,提高性能
但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加 RepaintBoundary 反而会更高效。

2.适用ListView.builder可用于和数据绑定实现大量或者无限的列表
先看效果图

演示5.gif

看下实现代码

class ScrollLayoutDemo extends StatefulWidget {
  String title;
  List<String> items = List<String>.generate(500, (i) => "item $i");
  ScrollLayoutDemo({Key key, this.title});
  @override
  _ScrollLayoutDemoState createState() => _ScrollLayoutDemoState();
}

class _ScrollLayoutDemoState extends State<ScrollLayoutDemo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          widget.title,
        ),
      ),
      body: ListView.builder(
          itemCount: widget.items.length,
          itemBuilder: (context,index){
              return ListTile(
                  title: Text(widget.items[index]),
              );
          },
      )
    );
  }
}

其构造函数大部分属性和ListView都一样,只有两个:

itemCount:代表 子Widget 的数量,虽然是可选的,但是还是建议赋值,可以让 ListView 预估最大滑动距离,从而提升性能。如果为null,则子节点数由[itemBuilder]返回null的最小索引确定。

itemBuilder:必传参数,itemBuilder 用于创建实际可见的 子Widget,只有索引大于或等于零且小于 itemCount 才会调用 itemBuilder。

3.使用 ListView.separated,具有分割项的 ListView.builder
ListView.separated相比ListView.builder多了一个separatorBuilder,separatorBuilder是用于构建分割项的,而且是必选的
先看下效果

演示6.gif

使用方法

ListView.separated(
          itemCount: widget.items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(widget.items[index]),
            );
          },
          separatorBuilder: (context, index) {
            return Container(
              constraints: BoxConstraints.tightFor(height: 1),
              color: Colors.greenAccent,
            );
          },
        ));

4.使用 ListView.custom,需要使用 SliverChildDelegate
先看下效果图

演示7.gif

看下代码实现

//使用方法
ListView.custom(
            childrenDelegate: SliverChildListDelegate(
                _getWidget()
            ),
        )

代码中添加widget的方法如下

_getWidget(){
    List<Widget> widgets = [];
    for (var i = 0;i < 100;i++) {
      widgets.add(ListTile(title: Text("item $i"),));
    }
 return widgets;  
}

先看下ListView.custom的构造函数,

const ListView.custom({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    this.itemExtent,
    @required this.childrenDelegate,
    double cacheExtent,
    int semanticChildCount,
  }) : assert(childrenDelegate != null),
       super(
         ...
       );

由构造函数可以看出ListView.custom跟ListView主要的区别就是childrenDelegate,这个是必传参数,类型是SliverChildDelegate
,这是一个抽象类,主要有两个实现类SliverChildBuilderDelegate和SliverChildListDelegate(我们demo中就是使用这个类来实现列表的)

  • GridView
    和ListView一样,共有五种方法来创建一个GridView,下面来一一介绍
    1、使用默认的构造函数,给children属性赋值 - 只适用于那些只有少量子Widget的GridView
    先看下效果图
    演示8.gif

    下面看下代码实现
GridView(
        gridDelegate:
            SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
        children: _getWidget(),
      )

下面看下GridView的源码:
我们主要看下GridView新增的属性,一些滚动公共的属性在前面在SingleChildScrollview中有介绍,这里就不再赘述

GridView({
    Key key,
    Axis scrollDirection = Axis.vertical,//滚动方向,默认垂直滚动,类型是Axis
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required this.gridDelegate,//控制GridView子Widget的布局的委托类型是SliverGridDelegate
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    List<Widget> children = const <Widget>[],
    int semanticChildCount,
  }) : assert(gridDelegate != null),
       ...,
       super(
        ...
       );
  • SliverGridDelegate:SliverGridDelegate 的实现有两个:
    SliverGridDelegateWithMaxCrossAxisExtent:横轴 子Widget 为固定长度的布局算法
    SliverGridDelegateWithFixedCrossAxisCount:横轴 子Widget 为固定数量的布局算法
    下面看下这两个Delegate的构造函数,以及参数的含义
    SliverGridDelegateWithFixedCrossAxisCount的构造函数
const SliverGridDelegateWithFixedCrossAxisCount({
    @required this.crossAxisCount,//交叉轴子view的数量
    this.mainAxisSpacing = 0.0,//主轴方向的的间距(主轴为垂直方向则为行间距,主轴为水平方向则为列间距)
    this.crossAxisSpacing = 0.0,//交叉轴方向的间距(交叉轴为垂直方向则为行间距,交叉轴为水平方向则为列间距)
    this.childAspectRatio = 1.0,//横轴和主轴的比例
  }) : ...;

SliverGridDelegateWithMaxCrossAxisExtent的构造函数

const SliverGridDelegateWithMaxCrossAxisExtent({
    @required this.maxCrossAxisExtent,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
  }) :...;

和上面构造函数的唯一区别就是maxCrossAxisExtent这个参数,其他的都一样
maxCrossAxisExtent:在交叉轴上每一项的的最大范围,例如当主轴是垂直的,GridView的宽度是500px,这个值为150px则Delegate就是自动计算一共可以显示4列,每列是125px。

2、使用GridView.count 创建GridView
这个实现的效果其实和GridView很像,就不上效果图了,我们直接看其构造函数

GridView.count({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required int crossAxisCount,
    double mainAxisSpacing = 0.0,
    double crossAxisSpacing = 0.0,
    double childAspectRatio = 1.0,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    List<Widget> children = const <Widget>[],
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  }) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
         crossAxisCount: crossAxisCount,
         mainAxisSpacing: mainAxisSpacing,
         crossAxisSpacing: crossAxisSpacing,
         childAspectRatio: childAspectRatio,
       ),
       childrenDelegate = SliverChildListDelegate(
         children,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
       super(
         ...
       );

从构造函数我们可以看到,这个方法其实就是将GridView中的SliverGridDelegateWithFixedCrossAxisCount,拆了出来,将SliverGridDelegateWithFixedCrossAxisCount中的属性直接放在构造函数中,使用方式其实没有差别,下面看下使用方式

GridView.count(
        crossAxisCount: 3,
        children: _getWidget(),
      )

就是这么简单。

同理既然SliverGridDelegateWithFixedCrossAxisCount这个可以拆,那么SliverGridDelegateWithMaxCrossAxisExtent是不是一样可以,答案是肯定的,这就有了第三种方式

3、使用 GridView.extent来创建GridView
看下其构造方法

GridView.extent({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required double maxCrossAxisExtent,
    double mainAxisSpacing = 0.0,
    double crossAxisSpacing = 0.0,
    double childAspectRatio = 1.0,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    List<Widget> children = const <Widget>[],
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  }) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
         maxCrossAxisExtent: maxCrossAxisExtent,
         mainAxisSpacing: mainAxisSpacing,
         crossAxisSpacing: crossAxisSpacing,
         childAspectRatio: childAspectRatio,
       ),
       childrenDelegate = SliverChildListDelegate(
         children,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
       super(
        ...
       );

下面看下实现

GridView.extent(
        maxCrossAxisExtent: 120,
        children: _getWidget(),
      )

4、使用GridView.builder,可用于和数据绑定实现大量或者无限的列表
这个和ListView.builder的使用就很相似了,我们先看下效果图

演示9.gif

下面看下实现代码

class GridViewLayoutDemo extends StatefulWidget {
  String title;
  List<Widget> items = _getWidget();
  GridViewLayoutDemo({Key key, this.title});
  @override
  _GridViewLayoutDemoState createState() => _GridViewLayoutDemoState();
}

_getWidget() {
  List<Widget> widgets = [];
  for (var i = 0; i < 100; i++) {
    widgets.add(ListTile(
      title: Text("item $i"),
    ));
  }
  return widgets;
}

class _GridViewLayoutDemoState extends State<GridViewLayoutDemo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: GridView.builder(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 5,
          ),
          itemCount: widget.items.length,
          itemBuilder: (context,index){
              return widget.items[index];
          },
      )
    );
  }
}

下面看下构造函数

GridView.builder({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required this.gridDelegate,
    @required IndexedWidgetBuilder itemBuilder,
    int itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    int semanticChildCount,
  }) : assert(gridDelegate != null),
       childrenDelegate = SliverChildBuilderDelegate(
         itemBuilder,
         childCount: itemCount,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
       super(
        ...
       );

这里主要是gridDelegate和itemBuilder这两个参数,和ListView中itemCount和itemBuilder属性很像。
5、使用 GridView.custom来创建GridView
先看下其实现方式

GridView.custom(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3
          ),
          childrenDelegate: SliverChildListDelegate(
              _getWidget()
          ),
      ),

下面看下构造函数

const GridView.custom({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required this.gridDelegate,
    @required this.childrenDelegate,//类型是SliverChildDelegate
    double cacheExtent,
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  }) : assert(gridDelegate != null),
       assert(childrenDelegate != null),
       super(
        ...
       );

这里其实是增加了childrenDelegate类型是SliverChildDelegate,可以定制子widget,和ListView.custom中的一样,用法也是一样的

  • CustomScrollView - 可以自定义滑动效果的可滚动的Widget
    首先先看下实现效果
    演示10.gif

    看下代码实现
CustomScrollView(
            slivers: <Widget>[
                SliverAppBar(
                    pinned: true,
                    expandedHeight: 100,
                    flexibleSpace: FlexibleSpaceBar(
                        title: Text("demo"),
                    ),
                ),
            SliverGrid(
                gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                    maxCrossAxisExtent: 150,
                    mainAxisSpacing: 10.0,
                    crossAxisSpacing: 10.0,
                    childAspectRatio: 4.0
                ),
                delegate: SliverChildBuilderDelegate((context,index){
                    return Container(
                        alignment: Alignment.center,
                        color: Colors.lightGreen[100*(index % 9)],
                        child: Text("grid item $index"),
                    );
                },
                childCount: 20),
            ),
            SliverFixedExtentList(
                itemExtent: 50.0,
                delegate: SliverChildBuilderDelegate((contxt,index){
                    return Container(
                        alignment: Alignment.center,
                        color: Colors.pink[100*(index % 9)],
                        child: Text("grid item $index"),
                    );
                },
                childCount: 100)
                ),
            ],
        
        )

下面看下CustomScrollView的构造函数
它里面的属性我们在介绍其他控件是都有介绍 ,这里就不做赘述

const CustomScrollView({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    Key center,//默认为空,当shrinkWrap=false时,必须为空
    double anchor = 0.0,
    double cacheExtent,
    this.slivers = const <Widget>[],
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  }) : super(
    ...
  );

这里主要看下slivers这个属性,这个属性的值只能是以Sliver开头的一系列Widget:

  • SliverList
    看下构造函数
 const SliverList({
    Key key,
    @required SliverChildDelegate delegate,
  }) : super(key: key, delegate: delegate);

用法和上面的ListView.custom类似,属性含义可以参考上文

  • SliverFixedExtentList
    看下SliverFixedExtentList的构造函数
  const SliverFixedExtentList({
    Key key,
    @required SliverChildDelegate delegate,
    @required this.itemExtent,
  }) : super(key: key, delegate: delegate);

用法和上面的ListView.custom类似,属性含义可以参考上文

  • SliverGrid
    看下SliverGrid的构造函数
 const SliverGrid({
    Key key,
    @required SliverChildDelegate delegate,
    @required this.gridDelegate,
  }) : super(key: key, delegate: delegate);

这个Widget跟GridView很像,除了这个还有SliverGrid.extent(),和SliverGrid.count()两种构造方法,用法分别和GridView.extent()和GridView.count()也是相似

  • SliverPadding
    看下SliverPadding的构造函数
  const SliverPadding({
    Key key,
    @required this.padding,//内边距
    Widget sliver,//这里widget也只能是Sliver开头的Widget
  }) : assert(padding != null),
       super(key: key, child: sliver);

  • SliverAppBar
    看下SliverAppBar的构造函数
const SliverAppBar({
    Key key,
    this.leading,//左侧的图标或文字,多为返回箭头
    this.automaticallyImplyLeading = true,//没有leading为true的时候,默认返回箭头,没有leading且为false,则显示title
    this.title,//标题
    this.actions,//标题右侧的操作
    this.flexibleSpace,//可以理解为SliverAppBar的背景内容区
    this.bottom,//SliverAppBar的底部区
    this.elevation,//阴影
    this.forceElevated = false,//是否显示阴影
    this.backgroundColor,//背景颜色
    this.brightness,//状态栏主题,默认Brightness.dark,可选参数light
    this.iconTheme,//SliverAppBar图标主题
    this.actionsIconTheme,//action图标主题
    this.textTheme,//文字主题
    this.primary = true,//是否显示在状态栏的下面,false就会占领状态栏的高度
    this.centerTitle,//标题是否居中显示
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,//标题横向间距
    this.expandedHeight,//合并的高度,默认是状态栏的高度加AppBar的高度
    this.floating = false,//滑动时是否悬浮
    this.pinned = false,//标题栏是否固定
    this.snap = false,//配合floating使用
  }): assert(automaticallyImplyLeading != null),
       assert(forceElevated != null),
       assert(primary != null),
       assert(titleSpacing != null),
       assert(floating != null),
       assert(pinned != null),
       assert(snap != null),
       assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
       super(key: key)
  • SliverSafeArea:-让内容显示在安全区域内
    看下其构造函数
const SliverSafeArea({
    Key key,
    this.left = true,//是否左侧在可是区域内
    this.top = true,//是否顶部在可是区域内
    this.right = true,//是否右侧在可是区域内
    this.bottom = true,//是否底部在可是区域内
    this.minimum = EdgeInsets.zero,//最小边距
    @required this.sliver,//这里widget也只能是Sliver开头的Widget
  }) : assert(left != null),
       assert(top != null),
       assert(right != null),
       assert(bottom != null),
       super(key: key);

这个Widget可以保证内容展示在可视区域内

  • PageView-可以一页一页滑动的Widget,子Widget会占据当前屏幕的所有可视区域,类似于Android中的ViewPager

PageView有三种实现方式,下面我们一一介绍
1、使用默认的构造函数,给 children 属性赋值
使用默认构造函数写 PageView,只适用于那些只有少量 子Widget 的 PageView。
先看效果图

演示11.gif

看下实现代码

PageView(
            onPageChanged: (index){
                
            },
            children: <Widget>[
                Center(
                    child: Text(
                        "title0",
                        style: TextStyle(
                            fontSize: 24,
                            color: Colors.purple
                        ),
                    ),
                ),
                Center(
                    child: Text(
                        "title1",
                         style: TextStyle(
                            fontSize: 24,
                            color: Colors.red
                        ),
                    ),
                ),
                Center(
                    child: Text(
                        "title2",
                         style: TextStyle(
                            fontSize: 24,
                            color: Colors.green
                        ),
                    ),
                ),
                Center(
                    child: Text(
                        "title3",
                         style: TextStyle(
                            fontSize: 24,
                            color: Colors.blue
                        ),
                    ),
                )
            ],
        )

可以看到使用很简单,接下来看下其构造函数

PageView({
    Key key,
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
    PageController controller,
    this.physics,
    this.pageSnapping = true,//默认值为 true,设置为false以禁用页面捕捉,对自定义滚动行为很有用。
    this.onPageChanged,//当 PageView 当前页面切换的时候调用,是个方法
    List<Widget> children = const <Widget>[],//PageView 的列表项
    this.dragStartBehavior = DragStartBehavior.start,
  }) : controller = controller ?? _defaultPageController,
       childrenDelegate = SliverChildListDelegate(children),
       super(key: key);

2、使用 PageView.builder
PageView.builder 可以和数据绑定,用于构建大量或无限的列表。而且只会构建那些实际可见的 子Widget
看下演示效果

演示12.gif

看下使用方法

PageView.builder(
            onPageChanged: (index){
                
            },
            itemCount: 10,
            itemBuilder: (context,index){
                return Center(
                    child: Text(
                        "title $index",
                        style: TextStyle(
                            fontSize: 24,
                            color: Colors.purple
                        ),
                    ),
                );
                
            },
        )

看下其构造方法

PageView.builder({
    Key key,
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
    PageController controller,
    this.physics,
    this.pageSnapping = true,
    this.onPageChanged,
    @required IndexedWidgetBuilder itemBuilder,
    int itemCount,
    this.dragStartBehavior = DragStartBehavior.start,
  }) : controller = controller ?? _defaultPageController,
       childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
       super(key: key);

可以看到多了和 ListView.builder 类似的 itemCount 和 itemBuilder 属性,用法也是一样的。
3、使用 PageView.custom
先看下演示效果

演示13.gif

看下代码实现

PageView.custom(
        onPageChanged: (index) {},
        childrenDelegate: SliverChildListDelegate(<Widget>[
          Center(
            child: Text(
              "title1",
              style: TextStyle(fontSize: 24, color: Colors.green),
            ),
          ),
          Center(
            child: Text(
              "title2",
              style: TextStyle(fontSize: 24, color: Colors.green),
            ),
          ),
          Center(
            child: Text(
              "title3",
              style: TextStyle(fontSize: 24, color: Colors.green),
            ),
          ),
        ]),
      ),

下面我们看下构造函数

PageView.custom({
    Key key,
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
    PageController controller,
    this.physics,
    this.pageSnapping = true,
    this.onPageChanged,
    @required this.childrenDelegate,
    this.dragStartBehavior = DragStartBehavior.start,
  }) : assert(childrenDelegate != null),
       controller = controller ?? _defaultPageController,
       super(key: key);

可以看到这里主要是childrenDelegate 这个属性,类型是SliverChildDelegate,它具有定制子Widget的能力,这个跟ListView.custom()中的childrenDelegate属性用法相同

结语

截止到此,关于可滚动的Widget就介绍完了。
以下是我的Flutter系列的链接,后续会持续更新,欢迎大家指正。

Flutter 系列文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容