七、Flutter_List组件

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

推荐阅读更多精彩内容