Flutter第十章(Scrollable ,Scrollbar 、CupertinoScrollbar ,SliverList、SliverFixedExtentList、 SliverG...

版权声明:本文为作者原创书籍。转载请注明作者和出处,未经授权,严禁私自转载,侵权必究!!!

情感语录: 如果你是对的,你没必要发脾气;如果你是错的,你没资格去发脾气。这才是真正的智慧。

欢迎来到本章节,上一章节我们讲了常用表单的使用,知识点回顾 戳这里 Flutter基础第九章

本章节主要讲解可以滚动的组件,很多时候一个页面内容会相当多;当组件内容超过当前显示视图范围时,如果没有特殊处理,Flutter 则会提示Overflow错误;在第二章中我们有讲到 ListViewGridView 可以实现滚动效果,利用这两个组件可以规避这类问题。但是有些复杂的界面交互效果,使用它们可能就变得非常不友好了。假如有一个页面,顶部需要一个GridView,底部需要一个ListView,而要求整个页面的滑动效果是统一的,即它们看起来是一个整体。此时如果使用GridView+ListView来实现的话,就不能保证一致的滑动效果,因为它们的滚动效果是分离的,所以这时就需要一个"胶水",把这些彼此独立的可滚动组件"粘"起来。

本章简要:

1、Scrollable 组件

2、Scrollbar 、CupertinoScrollbar 组件

3、SingleChildScrollView、 CustomScrollView 组件

4、SliverList、SliverFixedExtentList、 SliverGrid 组件

5、SliverPadding 、SliverAppBar 组件

6、ScrollController 滚动监听和控制

一、Scrollable 组件

可滚动组件都直接或间接包含一个Scrollable 组件,比如我们之前学的ListViewGridView 组件,而他们的父类 BoxScrollView 是继承与ScrollView实现的,而 ScrollView中就包含了 Scrollable 组件。 所以可滚动类组件包括一些共同的属性:

Scrollable构造函数:

    Scrollable({
      ...// 去除部分属性
      this.axisDirection = AxisDirection.down,
      this.controller,
      this.physics,
      @required this.viewportBuilder, //进阶篇讲
    })

axisDirection : 滚动方向。

physics :此属性接受一个ScrollPhysics类型的对象,Flutter会根据具体平台分别使用不同的ScrollPhysics对象,应用不同的显示效果:

  ClampingScrollPhysics:Android下微光效果。

  BouncingScrollPhysics:iOS下弹性效果。

controller:此属性接受一个ScrollController对象。ScrollController的主要作用是控制滚动位置和监听滚动事件。

二、Scrollbar 、 CupertinoScrollbar 组件

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

    Scrollbar(
      child: SingleChildScrollView(
        ...
      ),
    );

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

三、SingleChildScrollView、 CustomScrollView 组件

1、SingleChildScrollView 组件

SingleChildScrollView类似于Android中的 ScrollView,它只能接收一个子组件。它是继承 StatelessWidget 实现的,并非 继承 ScrollView。需要注意的是,通常SingleChildScrollView 只应在期望的内容不会超过屏幕太多时使用,因为SingleChildScrollView不支持基于Sliver的延迟实例化模型,所以如果预计视图可能包含超出屏幕尺寸太多的内容时,那么使用SingleChildScrollView将会 非常耗性能,此时应该使用一些支持Sliver延迟加载的可滚动组件,如: ListView。

构造函数:

  const SingleChildScrollView({
    Key key,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
    this.padding,
    bool primary,
    this.physics,
    this.controller,
    this.child,
    this.dragStartBehavior = DragStartBehavior.start,
  })

除了上面的通用属性外,它下面的两个属性也非常有用:

reverse:该属性表示是否反向,就是说初始滚动位置是在“头”还是“尾”,取false时,初始滚动位置在“头”,反之则在“尾”,默认为false。

primary:指是否使用widget树中默认的 PrimaryScrollController;当滑动方向为垂直方向(scrollDirection值为Axis.vertical)并且没有指定controller时,primary默认为true.

简单运用:

  import 'package:flutter/material.dart';

  class ScrollerViewPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
              appBar: AppBar(
                title: Text("ScrollerViewPage"),
              ),
              body: Container(
                  child: SingleChildScrollView(
                      reverse: false,
                      child: SingleChildScrollViewDemo()))));
    }
  }

  class SingleChildScrollViewDemo extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      List<Widget> listWidget = new List();

      for (int i = 0; i < 40; i++) {
        var text = Text("Item$i",
            style: TextStyle(fontSize: 16, color: Colors.redAccent));
        listWidget.add(text);
        listWidget.add(Divider());
      }
      return Column(children: listWidget);
    }
  }

效果如下:

SingleChildScrollView.gif

从效果可以看出 SingleChildScrollView 实现了 ListView 的滚动效果,但是需要注意的时 SingleChildScrollView 包裹的内容不能太多,否则引起性能问题。

2、CustomScrollView 组件

默认场景下,Scalfold 的导航栏都是固定写死的,如果要做一些交互性或者是沉浸式的交互比较困难。Flutter 提供了 CustomScrollView 来帮助实现跟随列表滑动发生一些变化的 AppBar 效果。CustomScrollView 继承自 ScrollView,它可以完成 SingleChildScrollView 能完成的工作,且 CustomScrollView 是可滚动模型。

构造函数:

  const CustomScrollView({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    Key center,
    double anchor = 0.0,
    double cacheExtent,
    this.slivers = const <Widget>[],
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  })

除了上面讲过的几个属性外,下面来看几个其他重要的属性。

shrinkWrap 配置可控制 AppBar 下的内容在滚动时,是否可以超过 AppBar 的边界。如果为true,则滑动内容区域 可覆盖 AppBar 直到屏幕顶端。如果 为 false ,则只能在 AppBar 以下区域滑动,即 AppBar 始终在顶部显示。

anchor 该属性不太好描述,它的的取值范围值0.0-1.0之间,这个属性迫使组件将其自身定位在父组件中的某个相对或绝对位置。后面演示观察下。

slivers List<Widget> 类型,用来承载滑动内容

四、 SliverList、SliverFixedExtentList、 SliverGrid 组件

1、 这三个组件你可以理解是 Sliver版 (ListView(SliverList、SliverFixedExtentList)、GridView(SliverGrid))组件。SliverList 就是一个 ListView,只不过在整个实现上,需要指定一个 delegate,比如通过 SliverChildBuilderDelegate 进行列表的构建。

   SliverList(
      delegate: new SliverChildBuilderDelegate(
              (BuildContext context, int index) {
            //创建列表项
            return new Container(
              alignment: Alignment.center,
              child: new Text('list item $index'),
            );
          },
          childCount: 50 //50个列表项
      ),
    ),

SliverFixedExtentList 你可以看做是 SliverList的加强版,它可以使用 itemExtent 属性来控制 item 的范围(高度)。

2、SliverGrid 如同 GridView 一样,只不过在整个实现上,需要指定一个 delegate 和 gridDelegate,比如通过 SliverChildBuilderDelegate 进行列表的构建,SliverGridDelegateWithFixedCrossAxisCount 来控制列数和行间距和列间距

  SliverPadding(
          padding:  EdgeInsets.all(8.0),
          sliver:  SliverGrid( //Grid
            gridDelegate:  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,
            ),
          ),
        ),

五、SliverPadding 、SliverAppBar 组件

1、SliverPadding 组件同 前面讲的 Padding 组件是一样的,只不过 SliverPadding 接收的子组件是 sliver Widget。

  const SliverPadding({
    Key key,
    @required this.padding,
    Widget sliver,
  })

2、SliverAppBar 组件 是专门服务于 CustomScrollView 的; SliverAppBar 支持的属性和 AppBar 支持的属性基本无异,SliverAppBar 因为需要跟随 ScrollView 的一些操作,属性会多一些。

构造函数:

  const SliverAppBar({
    Key key,
    this.leading,
    this.automaticallyImplyLeading = true,
    this.title,
    this.actions,
    this.flexibleSpace,
    this.bottom,
    this.elevation,
    this.forceElevated = false,
    this.backgroundColor,
    this.brightness,
    this.iconTheme,
    this.actionsIconTheme,
    this.textTheme,
    this.primary = true,
    this.centerTitle,
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,
    this.expandedHeight,
    this.floating = false,
    this.pinned = false,
    this.snap = false,
    this.shape,
  }) 

除开和AppBar 中属性外,下面来看几个重要属性:

floating: false 表示当列表往下滑动时,会先将列表内容滚动到顶部,然后再将 SliverAppBar 浮动出现,true表示当列表往下滑动时,会先将 SliverAppBar 浮动出现(与列表是否滚动到顶部无关),然后再继续列表的滑动。

pinned 属性能够决定是否将导航栏部分固定。true 表示导航栏不会完全消失,否则随滚动逐渐消失。

expandedHeight 属性可以配置AppBar 展开后的高度。

flexibleSpace 属性可以定制 AppBar 展开后的样式,结合 FlexibleSpaceBar 控制。

下面来综合运用下,这几个新组建:

  import 'package:flutter/material.dart';

  class ScrollerViewPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {

      return CustomScrollViewDemo();
    }
  }

  class CustomScrollViewDemo extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Material(
        color: Colors.green,
        child: CustomScrollView(
          //距离 AppBar 百分之10 高度
          anchor: 0.1,
          slivers: <Widget>[
            //AppBar,包含一个导航栏
            SliverAppBar(
              pinned: true,
              expandedHeight: 200.0,
              flexibleSpace: FlexibleSpaceBar(
                title: Text('ScrollerViewPage'),
               //centerTitle: true,
                background: Image.asset(
                  "images/mm.jpg",
                  fit: BoxFit.cover,
                ),
              ),
            ),

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

效果如下:

综合运用.gif

可以看出 在设置 anchor 属性后 底部的 SliverGrid 和 SliverAppBar 之间产生了整个父容器的百分之10距离,且上滑动时 SliverGrid 滚动超过这段距离后才会把 SliverAppBar 往上顶。下滑时 SliverAppBar 完全展开后才逐渐显示这段距离。

六、ScrollController 滚动监听和控制

本章和前面章节讲解的 ListView 、 GridView;都是可滚动组件,在开发中很多时候需要监听 这些可滚动组件的滚动位置信息,比如,滚动到某一个距离后要显示一个视图,再或者需要从某一位置需要跳到另一个位置时等。在 Flutter 中 ScrollController 能帮助我们解决这类问题。

构造函数:

  ScrollController({
    double initialScrollOffset = 0.0,
    this.keepScrollOffset = true,
    this.debugLabel,
  }) 

ScrollController 中常用的属性和方法:

initialScrollOffset : 初始滚动位置。

keepScrollOffset : 是否保存滚动位置。

addListener() : 添加滚动位置信息监听。

removeListener() : 删除滚动位置信息监听。

offset: 返回滑动距离的像素单位值。

dispose(): 销毁 ScrollController 控制器。

jumpTo(): 跳转到某一位置,不带动画。

animateTo(): 跳转到某一位置,带动画效果,Curves 给我们提供了 几十种动画效果可供选择。

下面来将上面的例子进行改造下,我需要监听 CustomScrollView 的滑动信息,当滑动距离超过500个像素后 我要在屏幕上显示一个回到顶部的按钮,然后点击按钮直接回到顶部去。这里面设计到界面模板的刷新 需要继承 StatefulWidget 来改造,且需要结合前面学的 FloatingActionButton 组件去实现(如果忘了,请回顾前面知识点)

  import 'package:flutter/material.dart';

  class ScrollerViewPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {

      return CustomScrollViewDemo();
    }
  }



  class CustomScrollViewDemo extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
      return Material(
          child: ScrollListenerDemo()
      );
    }
  }

  class ScrollListenerDemo extends StatefulWidget {
    @override
    _ScrollListenerDemoState createState() => _ScrollListenerDemoState();
  }

  class _ScrollListenerDemoState extends State<ScrollListenerDemo> {

    ScrollController _controller = new ScrollController();
    //是否显示“返回到顶部”按钮
    bool showToTopBtn = true;

    @override
    void initState() {
      //监听滚动事件,打印滚动位置
      _controller.addListener(() {
        print(_controller.offset); //打印滚动位置
        if (_controller.offset < 500 && showToTopBtn) {
          setState(() {
            showToTopBtn = false;
          });
        } else if (_controller.offset >= 500 && showToTopBtn == false) {
          setState(() {
            showToTopBtn = true;
          });
        }
      });
    }

    @override
    void dispose() {
      //为了避免内存泄露,需要调用_controller.dispose
      _controller.dispose();
      super.dispose();
    }

    @override
    Widget build(BuildContext context) {
      return Scaffold(

          floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
            backgroundColor: Colors.deepPurple,
              child: Icon(Icons.arrow_upward),
              onPressed: () {
                //返回到顶部时执行动画
                _controller.animateTo(0.0,
                    duration: Duration(milliseconds: 1000),
                    curve: Curves.easeInBack
                );
              }
          ),

        body: CustomScrollView(
          controller: _controller,
          slivers: <Widget>[
            //AppBar,包含一个导航栏
            SliverAppBar(
              pinned: true,
              expandedHeight: 200.0,
              flexibleSpace: FlexibleSpaceBar(
                title: Text('ScrollerViewPage'),
                //centerTitle: true,
                background: Image.asset(
                  "images/mm.jpg",
                  fit: BoxFit.cover,
                ),
              ),
            ),

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

效果如下:

ScrollController监听控制.gif

本章节的东西不多,但是值得去研究的内容还有很多,更多高级用法还需自己去探索。

好了本章节到此结束,又到了说再见的时候了,如果你喜欢请留下你的小红星,你们的支持才是创作的动力,如有错误,请热心的你留言指正, 谢谢大家观看,下章再会 O(∩_∩)O

实例源码地址: https://github.com/zhengzaihong/flutter_learn

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

推荐阅读更多精彩内容