美文网首页
Flutter开发之滚动Widget

Flutter开发之滚动Widget

作者: 得_道 | 来源:发表于2020-08-27 09:59 被阅读0次

    一、ListView组件

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

    1.1 ListView基础

    1.1.1 ListView基本使用

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

    • 为了让文字之间有一些间距,我使用了Padding Widget
    ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
    一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
    我们来看一下直接使用ListView的代码演练:
    为了让文字之间有一些间距,我使用了Padding Widget
    
    1.1.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: 36,),
              title: Text("联系人"),
              subtitle: Text("联系人信息"),
              trailing: Icon(Icons.arrow_forward_ios),
            ),
            ListTile(
              leading: Icon(Icons.email, size: 36,),
              title: Text("邮箱"),
              subtitle: Text("邮箱地址信息"),
              trailing: Icon(Icons.arrow_forward_ios),
            ),
            ListTile(
              leading: Icon(Icons.message, size: 36,),
              title: Text("消息"),
              subtitle: Text("消息详情信息"),
              trailing: Icon(Icons.arrow_forward_ios),
            ),
            ListTile(
              leading: Icon(Icons.map, size: 36,),
              title: Text("地址"),
              subtitle: Text("地址详情信息"),
              trailing: Icon(Icons.arrow_forward_ios),
            )
          ],
        );
      }
    }
    
    image.png
    1.1.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),
          ],
        );
      }
    }
    
    image.png

    1.2 ListView.build

    通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。
    但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。
    我们可以ListView.build来构建子Widget,提供性能。

    1.2.1 ListView.build基本使用

    ListView.build适用于子Widget比较多的场景,该构造函数将创建子Widget交给了一个抽象的方法,交给ListView进行管理,ListView会在真正需要的时候去创建子Widget,而不是一开始就全部初始化好。
    该方法有两个重要参数:
    itemBuilder:列表项创建的方法。当列表滚动到对应位置的时候,ListView会自动调用该方法来创建对应的子Widget。类型是IndexedWidgetBuilder,是一个函数类型。
    itemCount:表示列表项的数量,如果为空,则表示ListView为无限列表。
    通过一个简单的案例来认识它:

    class MyHomeBody extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return ListView.builder(
          itemCount: 100,
          itemExtent: 80,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
          }
        );
      }
    }
    
    image.png
    1.2.2 ListView.build动态数据

    在之前,我们搞了一个yz.json数据,我们现在动态的来通过JSON数据展示一个列表。
    思考:这个时候是否依然可以使用StatelessWidget:
    答案:不可以,因为当前我们的数据是异步加载的,刚开始界面并不会展示数据(没有数据),后面从JSON中加载出来数据(有数据)后,再次展示加载的数据。

    • 这里是有状态的变化的,从无数据,到有数据的变化。
    • 这个时候,我们需要使用StatefulWidget来管理组件。
      展示代码如下:
    import 'model/anchor.dart';
    
    ...省略中间代码
    class MyHomeBody extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return MyHomeBodyState();
      }
    }
    
    class MyHomeBodyState extends State<MyHomeBody> {
      List<Anchor> anchors = [];
    
      // 在初始化状态的方法中加载数据
      @override
      void initState() {
        getAnchors().then((anchors) {
          setState(() {
            this.anchors = anchors;
          });
        });
    
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return ListView.builder(
          itemBuilder: (BuildContext context, int index) {
            return Padding(
              padding: EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Image.network(
                    anchors[index].imageUrl,
                    fit: BoxFit.fitWidth,
                    width: MediaQuery.of(context).size.width,
                  ),
                  SizedBox(height: 8),
                  Text(anchors[index].nickname, style: TextStyle(fontSize: 20),),
                  SizedBox(height: 5),
                  Text(anchors[index].roomName)
                ],
              ),
            );
          },
        );
      }
    }
    
    1.2.3 ListView.separated

    ListView.separated可以生成列表项之间的分割器,它除了比ListView.builder多了一个separatorBuilder参数,该参数是一个分割器生成器。
    下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:

    class MySeparatedDemo extends StatelessWidget {
      Divider blueColor = Divider(color: Colors.blue);
      Divider redColor = Divider(color: Colors.red);
    
      @override
      Widget build(BuildContext context) {
        return ListView.separated(
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              leading: Icon(Icons.people),
              title: Text("联系人${index+1}"),
              subtitle: Text("联系人电话${index+1}"),
            );
          },
          separatorBuilder: (BuildContext context, int index) {
            return index % 2 == 0 ? redColor : blueColor;
          },
          itemCount: 100
        );
      }
    }
    
    image.png

    二、GridView组件

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

    2.1 GridView构造函数

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

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

    代码演练:

    class MyGridCountDemo extends StatelessWidget {
    
      List<Widget> getGridWidgets() {
        return List.generate(100, (index) {
          return Container(
            color: Colors.purple,
            alignment: Alignment(0, 0),
            child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
          );
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return GridView(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            mainAxisSpacing: 10,
            crossAxisSpacing: 10,
            childAspectRatio: 1.0
          ),
          children: getGridWidgets(),
        );
      }
    }
    
    image.png

    SliverGridDelegateWithMaxCrossAxisExtent

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

    代码演练:

    class MyGridExtentDemo extends StatelessWidget {
    
      List<Widget> getGridWidgets() {
        return List.generate(100, (index) {
          return Container(
            color: Colors.purple,
            alignment: Alignment(0, 0),
            child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
          );
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return GridView(
          gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 150,
            mainAxisSpacing: 10,
            crossAxisSpacing: 10,
            childAspectRatio: 1.0
          ),
          children: getGridWidgets(),
        );
      }
    }
    
    image.png

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

    2.2 GridView.build

    和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget。

    class _GridViewBuildDemoState extends State<GridViewBuildDemo> {
      List<Anchor> anchors = [];
    
      @override
      void initState() {
        getAnchors().then((anchors) {
          setState(() {
            this.anchors = anchors;
          });
        });
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: GridView.builder(
            shrinkWrap: true,
            physics: ClampingScrollPhysics(),
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              mainAxisSpacing: 10,
              crossAxisSpacing: 10,
              childAspectRatio: 1.2
            ),
            itemCount: anchors.length,
            itemBuilder: (BuildContext context, int index) {
              return Container(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Image.network(anchors[index].imageUrl),
                    SizedBox(height: 5),
                    Text(anchors[index].nickname, style: TextStyle(fontSize: 16),),
                    Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,)
                  ],
                ),
              );
            }
          ),
        );
      }
    }
    

    三、Slivers

    我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
    我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
    Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
    在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。

    3.1 Slivers的基本使用

    因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:

    • SliverList:类似于我们之前使用过的ListView;
    • SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;
    • SliverGrid:类似于我们之前使用过的GridView;
    • SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
    • SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
    • SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)
      我们简单演示一下:SliverGrid+SliverPadding+SliverSafeArea的组合
    class HomeContent extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return CustomScrollView(
          slivers: <Widget>[
            SliverSafeArea(
              sliver: SliverPadding(
                padding: EdgeInsets.all(8),
                sliver: SliverGrid(
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    crossAxisSpacing: 8,
                    mainAxisSpacing: 8,
                  ),
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                      return Container(
                        alignment: Alignment(0, 0),
                        color: Colors.orange,
                        child: Text("item$index"),
                      );
                    },
                    childCount: 20
                  ),
                ),
              ),
            )
          ],
        );
      }
    }
    
    
    image.png

    Slivers的组合使用

    官方的示例程序,将SliverAppBar+SliverGrid+SliverFixedExtentList做出如下界面:

    class HomeContent extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return showCustomScrollView();
      }
    
      Widget showCustomScrollView() {
        return new CustomScrollView(
          slivers: <Widget>[
            const SliverAppBar(
              expandedHeight: 250.0,
              flexibleSpace: FlexibleSpaceBar(
                title: Text('Coderwhy Demo'),
                background: Image(
                  image: NetworkImage(
                    "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
                  ),
                  fit: BoxFit.cover,
                ),
              ),
            ),
            new SliverGrid(
              gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 200.0,
                mainAxisSpacing: 10.0,
                crossAxisSpacing: 10.0,
                childAspectRatio: 4.0,
              ),
              delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  return new Container(
                    alignment: Alignment.center,
                    color: Colors.teal[100 * (index % 9)],
                    child: new Text('grid item $index'),
                  );
                },
                childCount: 10,
              ),
            ),
            SliverFixedExtentList(
              itemExtent: 50.0,
              delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  return new Container(
                    alignment: Alignment.center,
                    color: Colors.lightBlue[100 * (index % 9)],
                    child: new Text('list item $index'),
                  );
                },
                childCount: 20
              ),
            ),
          ],
        );
      }
    }
    
    image.png

    四、监听滚动事件

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

    4.1 ScrollController

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

    • jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
    • ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
    class MyHomePage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => MyHomePageState();
    }
    
    class MyHomePageState extends State<MyHomePage> {
      ScrollController _controller;
      bool _isShowTop = false;
      
      @override
      void initState() {
        // 初始化ScrollController
        _controller = ScrollController();
        
        // 监听滚动
        _controller.addListener(() {
          var tempSsShowTop = _controller.offset >= 1000;
          if (tempSsShowTop != _isShowTop) {
            setState(() {
              _isShowTop = tempSsShowTop;
            });
          }
        });
        
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("ListView展示"),
          ),
          body: ListView.builder(
            itemCount: 100,
            itemExtent: 60,
            controller: _controller,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(title: Text("item$index"));
            }
          ),
          floatingActionButton: !_isShowTop ? null : FloatingActionButton(
            child: Icon(Icons.arrow_upward),
            onPressed: () {
              _controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
            },
          ),
        );
      }
    }
    
    image.png

    4.2 NotificationListener

    如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener。

    • NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
    • NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
    • 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
      案例: 列表滚动, 并且在中间显示滚动进度
    class MyHomeNotificationDemo extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => MyHomeNotificationDemoState();
    }
    
    class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
      int _progress = 0;
    
      @override
      Widget build(BuildContext context) {
        return NotificationListener(
          onNotification: (ScrollNotification notification) {
            // 1.判断监听事件的类型
            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 false;
          },
          child: Stack(
            alignment: Alignment(.9, .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,
              )
            ],
          ),
        );
      }
    }
    
    
    image.png

    相关文章

      网友评论

          本文标题:Flutter开发之滚动Widget

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