七、Flutter_List组件

作者: 爱玩游戏的iOS菜鸟 | 来源:发表于2020-11-16 16:41 被阅读0次

    一、ListView组件

    Android中,我们可以使用ListViewRecyclerView来实现,在iOS中,我们可以通过UITableView来实现。

    Flutter中,我们也有对应的列表Widget,就是ListView

    1.1 ListView基础

    ListView的内部继承顺序:

    ListView extends BoxScrollView —> extends ScrollView —> extends StatelessWidget

    在ListView中,有4种构造方法:

    • ListView<Widget>,适合于具有少量子元素的列表视图
    • ListView.builder,利用IndexedWidgetBuilder来按需构造.适合于具有大量子视图的列表视图,构建器只对那些实际可见的子视图调用
    • ListView.separated,采用两个IndexedWidgetBuilder:itemBuilder根据需要构建子项separatorBuilder类似地构建出现在子项之间的分隔符子项。适用于具有固定数量的子控件的列表视图
    • ListView.custom,使用SliverChildDelegate构造,它提供了定制子模型的其他方面的能力。 例如,SliverChildDelegate可以控制用于估计实际上不可见的孩子的大小的算法

    1.1.1 ListView<Widget>

    ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget

    最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性

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

    类似通讯录的列表,我们可以通过ListTile类实现

    class ListViewDemo extends StatelessWidget {
      const ListViewDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return ListView(
          scrollDirection: Axis.vertical,
          children: List.generate(255, (index) {
            return ListTile(
              leading: Icon(Icons.favorite),
              trailing: Icon(Icons.pets),
              title: Text("联系人 ${index + 1}"),
              subtitle: Text("联系人电话号码"),
            );
          })
        );
      }
    }
    
    1.1.1.2 比较重要的属性 scrollDirection、 itemExtent

    scrollDirection:控制列表的滚动方向

    itemExtent:设置每一个item的高度(如果是Axis.horizontal,则为宽度)

    reverse:翻转属性,默认为false(从最底部开始排列)

    更多的看源码,尝试。不做过多介绍

    通过上面的两个示例,想必你已知晓。默认会创建出所有的childWidget,这样无疑会增加性能的开销. 对于更多数量未知的情况,并不适用

    1.1.2 ListView.builder

    ListView.builder方法有两个重要的参数:

    • itemBuilder(必传) 按需构造
    • itemCount 数量
    class ListViewBuilderDemo extends StatelessWidget {
      const ListViewBuilderDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return ListView.builder(
            itemBuilder:(BuildContext context, int index){
              return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
            },
          itemCount: 20,
          itemExtent: 30,
        );
      }
    }
    

    1.1.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
        );
      }
    }
    

    示例2:在指定区域内,以Icon为分隔器

    class ListViewSeparatedDemo extends StatelessWidget {
      const ListViewSeparatedDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 300,
          child: ListView.separated(
            itemBuilder: (BuildContext ctx, int index) {
              return Container(
                height: 40,
                child: Padding(
                  padding: const EdgeInsets.only(left: 20),
                  child: Text(
                    "helloworld $index",
                    style: TextStyle(fontSize: 20),
                  ),
                ),
              );
            },
            separatorBuilder: (BuildContext ctx, int index) {
              return Icon(Icons.pets,size: 40,);
            },
            itemCount: 100,
          ),
        );
      }
    }
    

    二、GridView组件

    iOS中,我们可以通过UICollectionView来实现多列。在Flutter中也有对应的列表Widget,就是GridView,使用方式和ListView也比较相似。

    2.1 GridView基础

    GridView的内部继承顺序:

    GridView extends BoxScrollView —> extends ScrollView —> extends StatelessWidget

    可以对比得知,GridView与ListView继承于BoxScrollView,所以在很多方面二者是极其相似的

    在GridView中,有4种构造方法:

    • GridView<Widget>,相对于ListView多gridDelegate这个非常特殊的参数
    • GridView.count,GridView.extent(类比上面,可以不用设置delegate)
    • GridView.builder,
    • GridView.custom,

    2.1.1 GridView<Widget>

    gridDelegate:控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate

    SliverGridDelegate是一个抽象类,我们找到它的两个子类:

    • SliverGridDelegateWithFixedCrossAxisCount控制交叉轴的item数量
    • SliverGridDelegateWithMaxCrossAxisExtent控制交叉轴的item的最大宽度

    SliverGridDelegateWithFixedCrossAxisCount:包含参数

    @required this.crossAxisCount,//
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
    

    SliverGridDelegateWithMaxCrossAxisExtent:包含参数

    @required this.maxCrossAxisExtent,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
    

    代码演示:

    //SliverGridDelegateWithMaxCrossAxisExtent示例
    class GridViewDelegateDemo extends StatelessWidget {
      const GridViewDelegateDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
          child: GridView(
            gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 200,
              childAspectRatio: 1.5,
              crossAxisSpacing: 10,
              mainAxisSpacing: 10,
            ),
            children: List.generate(100, (index) {
              return Container(
                color: Color.fromARGB(255, Random().nextInt(256),
                    Random().nextInt(256), Random().nextInt(256)),
              );
            }),
          ),
        );
      }
    }
    
    //SliverGridDelegateWithFixedCrossAxisCount示例
    class GridViewDemo extends StatelessWidget {
      const GridViewDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: EdgeInsets.symmetric(horizontal: 8, vertical: 5),
          child: GridView(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
              childAspectRatio: 15 / 9.0,
              mainAxisSpacing: 10,
              crossAxisSpacing: 10,
            ),
            children: List.generate(100, (index) {
              return Container(
                color: Color.fromARGB(255, Random().nextInt(256),
                    Random().nextInt(256), Random().nextInt(256)),
              );
            }),
          ),
        );
      }
    }
    

    2.1.2 GridView.count, GridView.extent

    👆上面这两个构造函数,有这对应的简写方式,即GridView.count, GridView.extent构造函数内部实现了对应的delegate

    没有什么好讲的,直接上代码:

    class GridViewCountDemo extends StatelessWidget {
      const GridViewCountDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
          child: GridView.count(
            crossAxisCount: 3,
            crossAxisSpacing: 10,
            mainAxisSpacing: 10,
            childAspectRatio: 0.9,
            children: List.generate(100, (index) {
              return Container(
                color: Color.fromARGB(255, Random().nextInt(256),
                    Random().nextInt(256), Random().nextInt(256)),
              );
            }),
          ),
        );
      }
    }
    
    class GridViewExtentDemo extends StatelessWidget {
      const GridViewExtentDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Padding(
            padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
            child: GridView.extent(
              maxCrossAxisExtent: 200,
              crossAxisSpacing: 10,
              mainAxisSpacing: 10,
              childAspectRatio: 0.9,
              children: List.generate(100, (index) {
                return Container(
                  color: Color.fromARGB(255, Random().nextInt(256),
                      Random().nextInt(256), Random().nextInt(256)),
                );
              }),
            ));
      }
    }
    

    2.1.3 GridView.builder

    类似ListView.builder,可以使用GridView.build来交给GridView自己管理需要创建的子Widget,降低性能消耗

    class GrideViewBuilderDemo extends StatelessWidget {
      const GrideViewBuilderDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
              mainAxisSpacing: 8,
              crossAxisSpacing: 8,
            ),
            itemBuilder: (BuildContext ctx, int index) {
              return Container(
                height: 40,
                child: Padding(
                  padding: const EdgeInsets.only(left: 20),
                  child: Text(
                    "helloworld $index",
                    style: TextStyle(fontSize: 20),
                  ),
                ),
              );
            });
      }
    }
    

    2.1.4 GridView.custom

    在源码中,我们可以看到上面的构造方法,设置了SliverChildListDelegate,而GridView.custom则是需要自己去设置

    class GrideViewCustomDemo extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return GridView.custom(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            mainAxisSpacing: 8,
            crossAxisSpacing: 8,
          ),
          childrenDelegate: SliverChildListDelegate(
              List.generate(100, (index) {
                return Container(
                    color: Color.fromARGB(255, Random().nextInt(256),
                    Random().nextInt(256), Random().nextInt(256))
                );
              }),
              addAutomaticKeepAlives: false,
          ),
        );
      }
    }
    

    三、Slivers(裂片)

    设想一下平常很常见的视图布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView),如何让它们做到统一滑动呢?

    Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图

    CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。

    3.1 Slivers的使用

    需要通过CustomScrollView来管理Slivers通过slivers属性,放数量不定的Sliver:

    Sliver的种类:

    • SliverList:类似于我们之前使用过的ListView;
    • SliverGrid:类似于我们之前使用过的GridView;
    • SliverFixedExtentList:类似于SliverList,只是可以设置item的高度;
    • SliverAppBar:添加一个AppBar,包裹Slive,作为CustomScrollView的HeaderView;

    给Sliver修改一些显示区域布局:

    • SliverPadding:包裹Slive,设置Sliver的内边距;
    • SliverSafeArea:包裹Slive,设置内容显示安全区域(比如不让齐刘海挡住我们的内容)

    示例:

    class SliverSingleDemo extends StatelessWidget {
      const SliverSingleDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return CustomScrollView(
            slivers: [
              SliverSafeArea(
                sliver: SliverPadding(
                  padding: EdgeInsets.only(top: 10,left: 10,right: 10),
                  sliver: SliverGrid(
                    delegate: SliverChildBuilderDelegate(
                          (BuildContext ctx, int index) {
                        return Container(
                          height: 40,
                          color: Color.fromARGB(255, Random().nextInt(256),
                              Random().nextInt(256), Random().nextInt(256)),
                          child: Padding(
                            padding: const EdgeInsets.only(left: 20),
                            child: Text(
                              "helloworld $index",
                              style: TextStyle(fontSize: 20),
                            ),
                          ),
                        );
                      },
                      childCount: 100,
                    ),
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                        crossAxisSpacing: 10,
                        mainAxisSpacing: 10,
                        crossAxisCount: 5,
                        childAspectRatio: 1.5),
                  ),
                ),
              )
            ],
          );
      }
    }
    

    示例:SliverAppBar + SliverGrid + SliverFixedExtentList + SliverPadding+SliverSafeArea

    class MutiSliverDemo extends StatelessWidget {
      const MutiSliverDemo({
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return CustomScrollView(
          slivers: [
            SliverAppBar(
              pinned: true,//悬停效果
              expandedHeight: 300,//高度
              flexibleSpace: FlexibleSpaceBar(//灵活的headview
                title: Text('Sliver demo'),
                background: Image.network('https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg',fit: BoxFit.cover,),
              ),
            ),
            SliverGrid(delegate: SliverChildBuilderDelegate(
                  (BuildContext ctx, int index) {
                return Container(
                  alignment: Alignment.center,
                  color: Colors.teal[100 * (index % 9)],
                  /*color: Color.fromARGB(255, Random().nextInt(256),
                      Random().nextInt(256), Random().nextInt(256)),*/
                  child: new Text('grid item $index'),
                );
              },
              childCount: 10,
            ), gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 200.0,
              mainAxisSpacing: 10.0,
              crossAxisSpacing: 10.0,
              childAspectRatio: 4.0,),
            ),
            SliverFixedExtentList(
                itemExtent: 50,
                delegate: SliverChildBuilderDelegate(
                  (BuildContext ctx, int index) {
                    return Container(
                      alignment: Alignment.center,
                      color: Colors.lightBlue[100 * (index % 9)],
                      child: new Text('list item $index'),
                    );
                  },
                  childCount: 20,
                ))
          ],
        );
      }
    }
    

    SliverAppBar有很多属性,有时间可以自己查看源码及官方文档,尝试一下

    四、监听滚动事件

    在Flutter中监听滚动相关的内容由两部分组成:ScrollControllerScrollNotification

    4.1 ScrollController

    1. 在Flutter中,Widget并不是最终渲染到屏幕上的元素(渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
    2. 通常情况下,根据滚动的位置来改变一些Widget的状态信息,ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
    3. ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
    4. 手动设置offset通过以下两个方法:
    • jumpTo(double offset)animateTo(double offset,...):这两个方法用于跳转到指定的位置,不同之处: 后者在跳转时会执行一个动画,而前者不会。

    案例1:当滚动到1000位置的时候,显示一个回到顶部的按钮

    class ZQHomePage extends StatefulWidget {
      @override
      _ZQHomePageState createState() => _ZQHomePageState();
    }
    
    class _ZQHomePageState extends State<ZQHomePage> {
      ScrollController _controller = ScrollController(initialScrollOffset: 300);
      bool _isShowFloatButton = false;
      @override
      void initState() {
        super.initState();
        _controller.addListener(() {
          print("监听滚动。。。。${_controller.offset}");
          setState(() {
            _isShowFloatButton = _controller.offset >= 1000;
          });
        });
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('列表测试'),
          ),
          body: ListView.builder(
            controller: _controller,
            itemBuilder: (BuildContext ctx, int index) {
              return ListTile(
                leading: Icon(Icons.pets),
                title: Text("联系人$index"),
              );
            },
            itemCount: 300,
          ),
          floatingActionButton: _isShowFloatButton ? FloatingActionButton(
            child: Icon(Icons.arrow_upward),
            onPressed: () {
              //controller.jumpTo(0);
              _controller.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
            },
          ) : null,
          floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
          floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
        );
      }
    

    4.2 NotificationListener

    通过NotificationListener,可以监听什么时候开始滚动,什么时候结束滚动

    • NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
    • NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
    • 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。

    案例: 列表滚动, 并且在中间显示滚动进度

    class ZQNewHomePage extends StatefulWidget {
      @override
      _ZQNewHomePageState createState() => _ZQNewHomePageState();
    }
    
    class _ZQNewHomePageState extends State<ZQNewHomePage> {
      ScrollController _controller = ScrollController(initialScrollOffset: 300);
      bool _isShowFloatButton = false;
      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 ScrollEndNotification){
                print("结束滚动..");
              }else if(notification is ScrollUpdateNotification){
                print("正在滚动..");
                // 当前滚动的位置和总长度
                final currentPixel = notification.metrics.pixels;
                final totalPixel = notification.metrics.maxScrollExtent;
                double progress = currentPixel / totalPixel;
                setState(() {
                  _isShowFloatButton = notification.metrics.pixels >= 1000;
                  _progress = (progress * 100).toInt();
                });
    
                print("当前滚动位置:${notification.metrics.pixels}");
                print("总滚动位置:${notification.metrics.maxScrollExtent}");
              }
    
              return true;
            },
            child: Stack(
              alignment: Alignment.center,
              children:[
                ListView.builder(
                  controller: _controller,
                  itemBuilder: (BuildContext ctx, int index) {
                    return ListTile(
                      leading: Icon(Icons.pets),
                      title: Text("联系人$index"),
                    );
                  },
                  itemCount: 300,
                ),
                CircleAvatar(
                  radius: 30,
                  child: Text("$_progress%"),
                  backgroundColor: Colors.black54,
                )
              ],
            ),
          ),
          floatingActionButton: _isShowFloatButton ? FloatingActionButton(
            child: Icon(Icons.arrow_upward),
            onPressed: () {
              //controller.jumpTo(0);
              _controller.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
            },
          ) : null,
          floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
          floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
        );
      }
    
      @override
      void dispose() {
        // TODO: implement dispose
        super.dispose();
        _controller.dispose();
      }
    }
    

    相关文章

      网友评论

        本文标题:七、Flutter_List组件

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