iOS- Flutter 可滚动组件-SingleChildScrollView&PageView

442 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 26 天,点击查看活动详情

SingleChildScrollView

SingleChildScrollView类似于iOS中的UIScrollView,它只接收一个子组件,定义如下:

SingleChildScrollView({
  this.scrollDirection = Axis.vertical, //滚动方向,默认是垂直方向
  this.reverse = false, 
  this.padding, 
  bool primary, 
  this.physics, 
  this.controller,
  this.child,
})

除了可滚动组件的通用属性外,重点关注primary属性:它表示是否使用widget树中默认的PrimaryScrollController(MaterialApp组件树中已经默认包含一个PrimaryScrollController); 当滑动方向为垂直方向(scrollDirection值为Axis.vertical)并且没有指定controller时,primary默认为true。

需要注意的是,通常SingleChildScrollView只应在期望的内容不会超过屏幕太多时使用,因为SingleChildScrollView不支持Sliver的延迟加载模型,也就是和iOS的UIScrollView一样,在初始化的同时,会一次性加载其子组件。所以如果预计视图窗口可能包含超出屏幕尺寸太多内容时,那么使用SingleChildScrollView将会非常昂贵(性能差),此时应该使用一些支持Sliver延迟加载的可滚动组件,比如ListView或者GirdView。

实例

class SingleChildScrollViewTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return Scrollbar( // 显示进度条
      child: SingleChildScrollView(
        padding: EdgeInsets.all(16.0),
        child: Center(
          child: Column( 
            //动态创建一个List<Widget>  
            children: str.split("") 
                //每一个字母都用一个Text显示,字体为原来的两倍
                .map((c) => Text(c, textScaleFactor: 2.0,)) 
                .toList(),
          ),
        ),
      ),
    );
  }
}

PageView

要实现页面切换和Tab布局,可以使用PageView组件。在大多数App都包含Tab换页效果、图片轮动以及抖音上下滑页切换视频功能等等,都可以通过PageView轻松实现。

PageView({
  Key? key,
  this.scrollDirection = Axis.horizontal, // 滑动方向
  this.reverse = false,
  PageController? controller,
  this.physics,
  List<Widget> children = const <Widget>[],
  this.onPageChanged,
  
  //每次滑动是否强制切换整个页面,如果为false,则会根据实际的滑动距离显示页面
  this.pageSnapping = true,
  //主要是配合辅助功能用的,后面解释
  this.allowImplicitScrolling = false,
  //后面解释
  this.padEnds = true,
})
// Tab 页面 
class Page extends StatefulWidget {
  const Page({
    Key? key,
    required this.text
  }) : super(key: key);

  final String text;

  @override
  _PageState createState() => _PageState();
}

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

@override
Widget build(BuildContext context) {
  var children = <Widget>[];
  // 生成 6 个 Tab 页
  for (int i = 0; i < 6; ++i) {
    children.add( Page( text: '$i'));
  }

  return PageView(
    // scrollDirection: Axis.vertical, // 滑动方向为垂直方向
    children: children,
  );
}

如果将PageView的滑动方向指定为垂直方向,则会变为上下滑动切换页面。

页面缓存

上面运行时,每当页面切换时会触发新Page页的build,比如我们从第一页滑到第二页,再滑回第一页时,打印如下:

flutter: build 0
flutter: build 1
flutter: build 0

可见PageView默认并没有缓存功能,一旦页面滑出屏幕它就会销毁,这和ListView/GridView不一样,在创建ListView/GridView时可以手动指定Viewport之外多大范围内的组件需要预渲染和缓存。只有当组件滑出屏幕后又滑出预渲染区域,组件才会被销毁,但是不幸的是PageView并没有cacheExtent参数。看PageView的源码:

child: Scrollable(
  ...
  viewportBuilder: (BuildContext context, ViewportOffset position) {
    return Viewport(
      // TODO(dnfield): we should provide a way to set cacheExtent
      // independent of implicit scrolling:
      // https://github.com/flutter/flutter/issues/45632
      cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
      cacheExtentStyle: CacheExtentStyle.viewport,
      ...
    );
  },
)

发现虽然没有cacheExtent,但是在allowImplicitScrolling为true时设置了预渲染区域,此时缓存类型为CacheExtentStyle.viewport,则cacheExtent表示缓存长度是几个Viewport的宽度,cacheExtent为1.0,则代表前后各缓存一个页面宽度,即前后各一页。既然如此,那我们将PageView的allowImplicitScrolling置为true就可以缓存前后两页了,运行后,控制台打印信息如下:

flutter: build 0
flutter: build 1
滑到第二页时:
flutter: build 0
flutter: build 1
flutter: build 2

当滑回第一页时,控制台信息不变,也就是缓存成功了。