Day08 - Flutter -滚动Widget

概述

  • ListView
  • GridView
  • sliver
  • 滚动的监听
一、ListView

移动端数据量比较大时,我们都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等。
Android中,我们可以使用ListViewRecyclerView来实现,在iOS中,我们可以通过UITableView来实现。
Flutter中,我们也有对应的列表Widget,就是ListView

  • 1.1、ListView 基本创建
    ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
    一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
    我们来看一下直接使用ListView的代码演练:

    • 1>、为了让文字之间有一些间距,我使用了Padding Widget


      ListView的基本创建
      class MyHomeBody extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
          return ListView(
             children: <Widget>[
                Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),),
                ),
                Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),),
                ),
                Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),),
                ),
              ],
            );
        }
      }
      

      提示:我们可以通过 List.generate创建子 Widget

      • List.generate(100, (index):第一个参数是加载多少个Widget, 第二个是第几个

        class MyHomeBody extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
        
             return ListView(
                children: List.generate(100, (index) {
                   return Text("Hello World $index");
                })
             );
          }
        }
        
    • 2>、ListTile的使用
      在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
      这个时候,我们可以使用ListTile来实现:


      class MyHomeBody extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
      
            return ListView(
                children: <Widget>[
                     ListTile(
                        leading: Icon(Icons.people, size: 20,),
                        title: Text("联系人"),
                        subtitle: Text("联系人信息"),
                        trailing: Icon(Icons.arrow_right),
                     ),
                     ListTile(
                        leading: Icon(Icons.people, size: 20,),
                        title: Text("邮箱"),
                        subtitle: Text("邮箱地址信息"),
                        trailing: Icon(Icons.arrow_right),
                     ),
                ],
            );
         }
      }
      
    • 3>、垂直方向滚动,默认是垂直方向
      我们可以通过设置 scrollDirection 参数来控制视图的滚动方向
      我们通过下面的代码实现一个水平滚动的内容:
      这里需要注意,我们需要给Container设置width,否则它是没有宽度的,就不能正常显示。或者我们也可以给ListView设置一个 itemExtent该属性会设置滚动方向上每个item所占据的宽度

      class MyHomeBody extends StatelessWidget {
      
        @override
        Widget build(BuildContext context) {
            return ListView(
                scrollDirection: Axis.horizontal,
                itemExtent: 200,
                children: <Widget>[
                    Container(color: Colors.red, width: 200),
                    Container(color: Colors.green, width: 200),
                    Container(color: Colors.blue, width: 200),
                    Container(color: Colors.purple, width: 200),
                    Container(color: Colors.orange, width: 200),
                ],
            );
        }
      }
      
  • 1.2、ListView.build 创建
    通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。
    但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。
    我们可以ListView.build来构建子Widget,提供性能。

    class MyHomeBody extends StatelessWidget {
    
         @override
         Widget build(BuildContext context) {
             return ListView.builder(
                // 创建多少个 row
                itemCount: 50,
                // 滚动方向的 row 宽度
                itemExtent: 100,
                // 生成 Widget
                itemBuilder: (BuildContext ctx, int index) {
                   return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
                }
             );
         }
    }
    
  • 1.3、ListView.separated 创建(带分割线)
    ListView.separated 可以生成列表项之间的分割器,它除了比ListView.builder多了一个separatorBuilder参数,该参数是一个分割器生成器。
    下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:

    ListView.separated 创建(带分割线)

    class MyHomeBody extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
            return ListView.separated(
                itemBuilder: (BuildContext context, int index) {
                   return ListTile(
                      leading: Icon(Icons.people),
                      trailing: Icon(Icons.arrow_right),
                      title: Text("联系人${index+1}"),
                      subtitle: Text("联系人电话${index+1}"),
                   );
                },
                itemCount: 10,
                separatorBuilder: (BuildContext context, int index) {
                   return Divider(
                     // 每个Widget 之间的距离
                     height: 30,
                     // 距离左边的距离
                     indent: 16,
                     // 距离右边的距离
                     endIndent: 16,
                     // 每条分割线的高度
                     thickness: 10,
                     color: index % 2 == 0 ? Colors.red : Colors.green,
                   );
                 },
             );
        }
    }
    
二、GridView 组件

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

  • 2.1、GridView构造函数
    使用GridView的方式就是使用构造函数来创建,和ListView对比有一个特殊的参数:gridDelegate
    gridDelegate用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:

    • SliverGridDelegateWithFixedCrossAxisCount

      SliverGridDelegateWithFixedCrossAxisCount({
         @requireddouble crossAxisCount, // 交叉轴的item个数
         double mainAxisSpacing = 0.0,   // 主轴的间距
         double crossAxisSpacing = 0.0,  // 交叉轴的间距
         double childAspectRatio = 1.0,  // 子Widget的宽高比
      })
      

      如下代码

      class MyHomeBody extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
             return GridView(
                 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 3,
                    crossAxisSpacing: 20,
                    mainAxisSpacing: 20,
                    // 宽 / 高
                    childAspectRatio: 2
                 ),
                 children: List.generate(100, (index) {
                      return Container(
                          color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                      );
                 }
             ),
          );
        }
      }
      
    • SliverGridDelegateWithMaxCrossAxisExtent

      SliverGridDelegateWithMaxCrossAxisExtent({
         double maxCrossAxisExtent, // 交叉轴的item宽度
         double mainAxisSpacing = 0.0, // 主轴的间距
         double crossAxisSpacing = 0.0, // 交叉轴的间距
         double childAspectRatio = 1.0, // 子Widget的宽高比
      })
      

      如下代码

      class MyHomeBody extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
              return GridView(
                  gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent (
                      maxCrossAxisExtent: 100,
                      mainAxisSpacing: 20,
                      crossAxisSpacing: 20,
                      childAspectRatio: 2
                  ),
                  children: List.generate(100, (index) {
                      return Container(
                         color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                      );
                  }
              ),
           );
         }
      }
      

    提示:前面两种方式也可以不设置delegate,可以分别使用:GridView.count构造函数和GridView.extent构造函数实现相同的效果

  • 2.2. GridView.build
    和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget。
    我们直接使用之前的数据来进行代码演练:


    GridView.build
    class MyHomeBody extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
          return Padding(
             padding: const EdgeInsets.all(5.0),
             child: GridView.builder(
                  shrinkWrap: true,
                  physics: ClampingScrollPhysics(),
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount (
                      crossAxisCount: 2,
                      mainAxisSpacing: 10,
                      crossAxisSpacing: 10,
                      childAspectRatio: 1.2
                  ),
                  itemCount: 10,
                  itemBuilder: (BuildContext context, int index) {
                      return Container(
                           child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: <Widget>[
                                  Image.network('http://image.xcar.com.cn/attachments/a/day_200323/2020032314_59939b0716c40f9be872JrmcP75B4KfO.jpg-app'),
                                  SizedBox(height: 5),
                                  Text('王三', style: TextStyle(fontSize: 6),), 
                              ],
                           ),
                      );
                  }
              ),
         );
      }
    }
    
三、Sliver
  • 3.1、Sliver 的简单介绍
    我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
    我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
    Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
    在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。
    补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。

  • 3.2、Slivers 的基本使用
    因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:不可以放弃他的

    • SliverList:类似于我们之前使用过的ListView;

    • SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;

    • SliverGrid:类似于我们之前使用过的GridView;

    • SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;

    • SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;

    • SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容),也就是可以滚动过安全区域

      class MyHomeBody1 extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
             return CustomScrollView(
                  slivers: <Widget>[
                      SliverGrid(
                           gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                               crossAxisCount: 2,
                               crossAxisSpacing: 16,
                               childAspectRatio: 2,
                               mainAxisSpacing: 16
                           ),
                           delegate: SliverChildBuilderDelegate(
                               (BuildContext context, int index) {
                                   return Container(
                                      color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                                   );
                               },
                               childCount: 10
                           )
                      ),
                  ],
            );
         }
      }
      
  • 3.3、Slivers的组合使用:SliverAppBar、SliverGrid、SliverList 的设置


    多个slivers的使用:SliverAppBar、SliverGrid、SliverList 的设置
    class MyHomeBody extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
            return CustomScrollView(
                slivers: <Widget>[
                    SliverAppBar(
                       // true: bar不动
                       // false: bar动
                       pinned: true,
                       // bar 的高度
                      expandedHeight: 200,
                      flexibleSpace: FlexibleSpaceBar(
                          title: Text("Hello World!"),
                          background: Image.asset("assets/images/iron.png", fit: BoxFit.cover,),
                      ),
                   ),
                   SliverSafeArea(
                      sliver: SliverPadding(
                         padding: EdgeInsets.all(16),
                         sliver: SliverGrid(
                              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                              crossAxisCount: 2,
                              crossAxisSpacing: 16,
                              childAspectRatio: 2,
                              mainAxisSpacing: 16
                         ),
                         delegate: SliverChildBuilderDelegate(
                             (BuildContext context, int index) {
                               return Container(
                                  color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                               );
                            },
                            childCount: 6
                         )
                      ),
                    ),
                  ),
                  SliverList(
                     delegate: SliverChildBuilderDelegate(
                         (BuildContext context, int index) {
                            return ListTile(
                               leading: Icon(Icons.people),
                               title: Text("联系人"),
                            );
                         },
                         childCount: 20
                     ),
                  )
             ],
         );
      }
    }
    
四、滚动的监听

对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
比如视图滚动到底部时,我们可能希望做上拉加载更多;
比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
比如监听滚动什么时候开始,什么时候结束;
Flutter 中监听滚动相关的内容由两部分组成ScrollControllerScrollNotification

  • 4.1、ScrollController 监听,可以预先设置offset,也可以监听滚动的位置,缺点是:无法检测股东开始和结束
    在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
    ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
    另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
    我们来做一个案例,当滚动到500位置的时候,显示一个回到顶部的按钮:

    • jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。

    • ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。



      代码如下

      class HomePage extends StatefulWidget {
         @override
         _HomePageState createState() => _HomePageState();
      }
      
      class _HomePageState extends State<HomePage> {
         // 设置变量 _controller 并设置偏移量
         ScrollController _controller = ScrollController(initialScrollOffset: 200);
         /* 默认设置为 false */
         bool _isFloatingActionButton = false;
         @override
         void initState() {
             // TODO: implement initState
             super.initState();
      
             _controller.addListener(() {
                print("监听到滚动");
                setState(() {
                    _isFloatingActionButton = _controller.offset > 500 ? true : false;
                });
            });
         }
      
         @override
         Widget build(BuildContext context) {
            return Scaffold(
               appBar: AppBar(
                   title: Text("列表滚动测试"),
               ),
               body: ListView.builder(
                   controller: _controller,
                   itemCount: 20,
                   itemBuilder: (BuildContext context, int index) {
                     return ListTile(
                        leading: Icon(Icons.people),
                        title: Text("测试 $index"),
                     );
                   }
               ),
               floatingActionButton: _isFloatingActionButton ? FloatingActionButton(
                  child: Icon(Icons.arrow_upward),
                  onPressed: () {
                    // 返回到顶部
                    _controller.animateTo(0, duration: Duration(milliseconds: 200), curve: Curves.easeIn);
                  },
               ) : null,
           );
      
         }
      }
      
  • 4.2、ScrollNotification
    如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener。

    • NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
    • NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。

    该回调可以返回一个布尔值,代表是 false 阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
    案例: 列表滚动, 并且在中间显示滚动进度

    class HomePage extends StatefulWidget {
       @override
        _HomePageState createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
        int _progress = 0;
    
        @override
        Widget build(BuildContext context) {
             return Scaffold(
                  appBar: AppBar(
                     title: Text("列表滚动测试"),
                  ),
                  body: NotificationListener(
                     onNotification: (ScrollNotification notification ) {
                          if (notification is ScrollStartNotification) {
                             print("----开始滚动----");
                          } else if (notification is ScrollUpdateNotification) {
                               // 当前滚动的位置和总长度
                               final currentPixel = notification.metrics.pixels;
                               final totalPixel = notification.metrics.maxScrollExtent;
                               double progress = currentPixel / totalPixel;
                               setState(() {
                                  _progress = (progress * 100).toInt();
                               });
                               print("正在滚动:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
                          } else if (notification is ScrollEndNotification) {
                               print("----结束滚动----");
                          }
                          return true;
                     },
                     child: Stack(
                          alignment: Alignment(0.9, 0.9),
                          children: <Widget>[
                              ListView.builder(
                                   itemCount: 100,
                                   itemExtent: 60,
                                   itemBuilder: (BuildContext context, int index) {
                                        return ListTile(title: Text("item$index"));
                                   }
                              ),
                              CircleAvatar(
                                   radius: 30,
                                   child: Text("$_progress%"),
                                   backgroundColor: Colors.black54,
                              )
                          ],
                     ),
               ),
           );
        }
     }
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350