美文网首页工具集Flutter初探
Day08 - Flutter -滚动Widget

Day08 - Flutter -滚动Widget

作者: IIronMan | 来源:发表于2020-05-21 16:32 被阅读0次

    概述

    • 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,
                                )
                            ],
                       ),
                 ),
             );
          }
       }
      

    相关文章

      网友评论

        本文标题:Day08 - Flutter -滚动Widget

        本文链接:https://www.haomeiwen.com/subject/kmduohtx.html