Flutter学习 可滚动Widget 中

Posted RikkaTheWorld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习 可滚动Widget 中相关的知识,希望对你有一定的参考价值。

5. AnimatedList

AnimatedList 和 ListView 功能差不多, 顾名思义,它在列表中插入节点或删除节点时会执行一些动画

它是一个 StatefulWidget ,对应的 State 是 AnimatedListState,添加、删除元素的方法是:

void insetItem(int index,  Duration duration = mDuration );
void removeItem(int index, AnimatedListRemovedItemBuilder builder,  Duration duration = mDuration );

5.1 实例代码

class _AnimatedListRouteState extends State<AnimatedListRoute> 
  var data = <String>[];
  int counter = 5;

  final globalKey = GlobalKey<AnimatedListState>();

  @override
  void initState() 
    for (var i = 0; i < counter; i++) 
      data.add("$i + 1");
    
    super.initState();
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Stack(
        children: [
          AnimatedList(
              key: globalKey,
              initialItemCount: data.length,
              itemBuilder: (BuildContext context, int index,
                  Animation<double> animation) 
                // 添加列表项时会执行渐显动画
                return FadeTransition(
                    opacity: animation, child: buildItem(context, index));
              ),
          // 创建一个添加按钮
          buildAddBtn(),
        ],
      ),
    );
  

  Widget buildAddBtn() 
    return Positioned(
      child: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () 
          // 添加一个列表项
          data.add("$++counter");
          // 告诉列表项有添加的列表项
          globalKey.currentState!.insertItem(data.length - 1);
        ,
      ),
      bottom: 30,
      left: 0,
      right: 0,
    );
  

  // 构建列表项
  Widget buildItem(context, index) 
    String char = data[index];
    return ListTile(
      key: ValueKey(char),
      title: Text(char),
      trailing: IconButton(
          icon: const Icon(Icons.delete),
          // 点击时进行删除
          onPressed: () => onDelete(context, index)),
    );
  

  void onDelete(context, index) 
    setState(() 
      globalKey.currentState!.removeItem(index, (context, animation) 
        // 删除过程执行的是反向动画, animation.value 会从 1 变成0
        var item = buildItem(context, index);
        data.removeAt(index);
        return FadeTransition(
          opacity: CurvedAnimation(
            parent: animation,
            curve: const Interval(0.5, 1.0),
          ),
          // 不断缩小列表项的高度
          child: SizeTransition(
            sizeFactor: animation,
            axisAlignment: 0.0,
            child: item,
          ),
        );
      , duration: const Duration(milliseconds: 200));
    );
  

6. GridView

GridView 用于构建网格列表,构造函数如下:

class GridView extends BoxScrollView 
  GridView(
    Key? key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController? controller,
    bool? primary,
    ScrollPhysics? physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry? padding,
    required this.gridDelegate,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double? cacheExtent,
    List<Widget> children = const <Widget>[],
    ...
  

GridView 也包含了大多数的 ListView 有的通用参数,比较重要的是 gridDelegate 这个属性,它接受一个 SliverGridDelegate,控制 GridView 子组件如何排列

  • SliverGridDelegate
    是一个抽象类, 定义了 GridView Layout相关接口,子类实现它来实现布局算法。 Flutter已经提供两个实现类,分别是:SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent

这两个用的应该会比较多,我们来使用并介绍它:

6.1 SliverGridDelegateWithFixedCrossAxisCount

横轴为固定数量子元素的布局算法,构造函数为:

SliverGridDelegateWithFixedCrossAxisCount(
    required this.crossAxisCount,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
    this.mainAxisExtent,
  
  • crossAxisCount
    横轴子元素数量, 此属性确定后, 子元素在横轴的长度就确定了,即 ViewPort 横轴长度 / crossAxisCount
  • mainAxisSpacing
    主轴方向的间距
  • crossAxisSpacing
    横轴方向子元素的间距
  • childAspecRatio
    子元素在横轴长度和主轴长度的比例, 用于 crossAxisCount 指定后,子元素横轴长度就确定了,然后通过此参数值可以确定子元素在主轴的长度
  • mainAxisExtent
    主轴上每个子元素的具体长度

这里看一个例子:

 GridView(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          // 横轴三个子Widget
          crossAxisCount: 4,
          // 宽高比为 1:1
          childAspectRatio: 1.0),
      children: const [
        Icon(Icons.add),
        Icon(Icons.eleven_mp),
        Icon(Icons.ten_k),
        Icon(Icons.cake),
        Icon(Icons.beach_access),
        Icon(Icons.free_breakfast),
        Icon(Icons.all_inclusive),
      ],
    ))

6.2 GridView.count

GridView.count 构造函数内部使用了 SliverGridDelegateWithFixedCrossAxisCount ,我们通过它可以快速的创建横轴固定数量子元素的 GridView, 上面的示例代码其实就等价于:

GridView.count(
      crossAxisCount: 4,
      childAspectRatio: 1.0,
      children: const [
        Icon(Icons.add),
        Icon(Icons.eleven_mp),
        Icon(Icons.ten_k),
        Icon(Icons.cake),
        Icon(Icons.beach_access),
        Icon(Icons.free_breakfast),
        Icon(Icons.all_inclusive),
      ],
    )

6.3 SliverGridDelegateWithMaxCrossAxisExtent

实现了横轴子元素为固定最大长度的布局算法,构造函数为:

  const SliverGridDelegateWithMaxCrossAxisExtent(
    required this.maxCrossAxisExtent,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
    this.mainAxisExtent,
  

属性和之前所学基本一模一样,而 maxCrossAxisExtent 为子元素在横轴上的最大长度, 如果 ViewPort 的横轴长度是 450,那么当 maxCrossAxisExtent 的值在区间 [450/4, 450/3] 的话,子元素最终实际长度都是 112.5 而 childAspectRatio 是指子元素横轴和主轴的长度比

下面看一个例子:

 GridView(
        padding: EdgeInsets.zero,
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 120.0, childAspectRatio: 2.0 // 宽高比为2
            ),
        children: const [
          Icon(Icons.add),
          Icon(Icons.eleven_mp),
          Icon(Icons.ten_k),
          Icon(Icons.cake),
          Icon(Icons.beach_access),
          Icon(Icons.free_breakfast),
          Icon(Icons.all_inclusive),
        ],
      ),

6.4 GridView.extent

上面的代码等价于:

GridView.extent(
        padding: EdgeInsets.zero,
        maxCrossAxisExtent: 120.0, childAspectRatio: 2.0,
        // 宽高比为2
        children: const [
          Icon(Icons.add),
          Icon(Icons.eleven_mp),
          Icon(Icons.ten_k),
          Icon(Icons.cake),
          Icon(Icons.beach_access),
          Icon(Icons.free_breakfast),
          Icon(Icons.all_inclusive),
        ],
      ),

6.5 GridView.builder

上面我们介绍 GridView 都需要一个 widget 数组作为其子元素,这些方式都会提前将所有子 widget 都构建好,所以只适用于子Widget数量比较少的时候,当使用较多的时候,和 ListView一样, 使用 GridView.builder 来构建子 Widget, GridView.builder 必须指定两个参数:

GridView.builder(
 ...
 required SliverGridDelegate gridDelegate, 
 required IndexedWidgetBuilder itemBuilder,
)

6.5.1 范例

class _GridViewRouteRouteState extends State<GridViewRoute> 
  // icon 数据源
  List<IconData> _icons = [];

  @override
  void initState() 
    super.initState();
    // 初始化数据
    _retrieveIcons();
  

   // 模拟异步加载数据
  void _retrieveIcons() 
    Future.delayed(const Duration(milliseconds: 200)).then((value) 
      setState(() 
        _icons.addAll([
          Icons.add,
          Icons.eleven_mp,
          Icons.ten_k,
          Icons.cake,
          Icons.beach_access,
          Icons.free_breakfast,
          Icons.all_inclusive
        ]);
      );
    );
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              // 每行4列
              crossAxisCount: 3,
              childAspectRatio: 1.0),
          itemCount: _icons.length,
          itemBuilder: (context, index) 
            if (index == _icons.length -1 && _icons.length < 200) 
              _retrieveIcons();
            
            return Icon(_icons[index]);
          ),
    );
  

7. PageView 与 页面缓存

7.1 PageView

android中,如果需要实现页面的切换,可以使用 PageView,Flutter也有同名同作用的 Widget, 下面是它的构造函数:

  PageView(
    Key? key,
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
    PageController? controller,
    this.physics,
    this.pageSnapping = true,
    this.onPageChanged,
    List<Widget> children = const <Widget>[],
    this.dragStartBehavior = DragStartBehavior.start,
    this.allowImplicitScrolling = false,
    this.restorationId,
    this.clipBehavior = Clip.hardEdge,
    this.scrollBehavior,
    this.padEnds = true,
  
  • pageSnapping
    每次滑动是否强制切换整个画面,如果为false,会根据实际的滑动距离显示页面
  • this.allowImplicitScrolling
    主要是配合辅助功能使用
  • padEnds
    下面会讲解

我们看一个 Tab 切换的实例,每个Tab都只显示一个数字,然后总共有6个Tab:

class _PageViewRouteState extends State<PageViewRoute> 
  @override
  Widget build(BuildContext context) 
    var children = <Widget>[];
    // 设置六个Tab
    for (int i = 0; i < 6; i++) 
      children.add(Page(
        text: "$i",
      ));
    
    return Scaffold(body: PageView(children: children));
  


class Page extends StatefulWidget 
  const Page(Key? key, required this.text) : super(key: key);

  final String text;

  @override
  State<StatefulWidget> createState() => _PageState();


class _PageState extends State<Page> 
  @override
  Widget build(BuildContext context) 
    print("build $widget.text");
    return Center(
        child: Text(
      widget.text,
      textScaleFactor: 5,
    ));
  

接下来就可以正常滑动Tab了,实现还是比较简单的

7.2 页面缓存

在上面的示例中,每次页面的切换,可都会触发新 Page 页的 build,这说明 PageView 默认没有缓存功能,一旦某个Page画出页面就会被销毁

这是因为 PageView 没有透传 cacheExtent 给 Viewport,所以 Viewport 默认 cacheExtent为1, 但是却在 allowImplicitScrolling 为 true 时设置了预渲染区域, 此时将会设置缓存类型为 CacheExtentStyle.viewport ,则 cacheExtent 则表示缓存的长度是几个 Viewport 的宽度, cacheExtent 为1.0,则代表前后各缓存一页。
也就是说,将 PageView 的 allowImplicitScrolling 设置为 true 时,就会缓存前后两页的Page

问题的根源貌似是 在 PageView 中设置 cacheExtent 会和 ios的辅助功能有冲突,没有更好的解决方法,Flutter就带着这个问题。但是国内基本不用考虑用辅助功能,所以想到的解决方案就是 拷贝一份PageView 的源码,然后透传 cacheExtent 即可。

当然,Flutter还提供了更通用的解决方案,就是缓存子项的解决方案

8. 可滚动组件子项缓存 KeepAlive

在 ListView 的构造函数中有一个 addAutomaticKeepAlives 属性没有介绍,如果为 true, ListView就会为其每一个子项添加一个 AutomaticKeepAlive 父组件。 虽然 PageView 的默认构造函数和 PageView.build 构造函数中没有该参数,但他们最终都会生成一个 SliverChildDelegate,这个组件会在每个列表子项构建完成时,为其添加一个 AutomaticKeepAlive 的父组件,下面来介绍一个这个父组件。

8.1 AutomaticKeepAlive

AutomaticKeepAlive 组件的主要作用是将列表项的 根RenderObject 的 keepAlive 按需自动标记为true或false。
就是 列表项的根 Widget, 这里将 Viewport+cacheExtent称为 加载区域

  1. 当 keepAlive 为false时,如果列表项滑出加载区域,列表组件会被销毁
  2. 当 keepAlive 为true时,当列表项滑出加载区域后,Viewport会将列表项组件缓存起来,当列表项进入加载区域时,Viewport先从缓存中查找是否已经缓存,如果有直接复用,如果没有重新创建列表项

而 AutomaticKeepAlive 这个标识位的设置,其实全靠我们开发者来控制的。

所以为了让 PageView 实现多页面的缓存,我们的思路就是让 PageView 来讲 AutomaticKeepAlive 置为true,Flutter 的做法就是让列表项组件 混入一个 AutomaticKeepAliveClientMixin,然后实现 wantKeepAlive() 就可以了,代码如下:

class _PageState extends State<Page> with AutomaticKeepAliveClientMixin 
  @override
  Widget build(BuildContext context) 
    super.build(context);
    ...
  

  @override
  bool get wantKeepAlive => true; // 需要缓存

在实现了 wantKeepAlive 后,还必须要在 build 方法中调用一下 super.build(context),它会将 keepAlive 的信息通知出去。

需要注意的是,如果我们采用 PageView.cutom 构建页面时,没有给列表项包装 AutomaticKeepAlive 父组件,则上述方案不能正常工作。

9. TabBarView

TabBarView 是 Marterial 提供的 Tab 组件,通常和 TabBar 搭配使用

9.1 TabBarView

TabBarView 封装了 PageView,构造函数如下:

  const TabBarView(
    Key? key,
    required this.children, // Tab 页面
    this.controller, // TabController
    this.physics,
    this.dragStartBehavior = DragStartBehavior.start,
  )

这里的 TabController 是用来监听和控制 TabBarView 的页面切换,通常是和 TabBar 来联动的,如果没有指定会默认在组件树上查找最近一个使用的 DefaultTabController

9.2 TabBar

TabBar 的许多属性都是用来配置 指示器 和 label 的,我们来看下其构造函数:

  const TabBar(
    Key? key,
    // 具体的 Tab 数组,需要我们来创建
    required this.tabs,
    // TabController,用于和 TabBarView 联动
    this.controller,
    // 是否可以滑动
    this.isScrollable = false,
    this.padding,
    // 指示器颜色
    this.indicatorColor,
    this.automaticIndicatorColorAdjustment = true,
    // 指示器高度,默认为2
    this.indicatorWeight = 2.0,
    this.indicatorPadding = EdgeInsets.zero,
    // 指示器 Decoration
    this.indicator,
    // 指示器长度,两个可选值,一个是 Tab 长度,一个是 label 长度
    this.indicatorSize,
    this.labelColor,
    this.labelStyle,
    this.labelPadding,
    ...
  )

TabBar 和 TabBarView 是靠 TabController 进行联动的, 需要注意的是, TabBar 和 TabBarView 的孩子数量需要一致。

tab 是 TabBar 的孩子,可以是任意 Widget, 不过 Material 已经实现了默认的 Tab 组件给我们使用:

const Tab(
  Key? key,
  this.text, //文本
  this.icon, // 图标
  this.iconMargin = const EdgeInsets.only(bottom: 10.0),
  this.height,
  this.child, // 自定义 widget
)

其中 text 和 child 是互斥的

9.3 示例

代码如下:

class _TabBarViewRouteState extends State<TabBarViewRoute>
    with SingleTickerProviderStateMixin 
  final ScrollController _controller = ScrollControllerFlutter学习 可滚动Widget 下

Flutter学习 可滚动Widget 上

Flutter学习-滚动的Widget

08-可滚动Widget

Flutter了解之可滑动组件

一统天下 flutter