Flutter 动效实战 —— 视差轮播效果

5,837 阅读3分钟

首先我们来看一下本文要实现的效果:

第一眼看到这个效果时,按照我们的惯性思维,通常会这样做:

  1. 获取到 ViewPager
  2. 设置监听器或代理来响应滚动事件
  3. 获取 onscreen 的 View,计算并设置其 translation

但是在 Flutter 中我们要转变思路,因为 Flutter 是声明式 UI 框架,每一帧的变化都需要生成一个新的 Widget Tree,所以我们只需要关心视图与视图之间的关系,然后在构造 Widget Tree 时计算好数值带入构造方法中即可。

要达到上面的效果,小字 Text 在页面滑动时要比整体移动速度快一倍,所以小字的 translate X 为 \tt{pageWidth / 2 * progress}

这里涉及两个要点:在 Flutter 里如何获取 Widget 的大小,如何监听页面滚动。

首先是获取 Widget 大小,由于 Flutter 中的 Widget 都是自适应大小的,所以对于一个 Widget,它的大小要么尽可能大(如 PageView),要么尽可能小(如 Text),所以我们能获取到的只是一个范围(约束)。如要获得某一 Widget 的尺寸范围,只需给它包裹一层 LayoutBuilder 的 Widget,就像这样:

new LayoutBuilder(builder: (context, constraints) => new XXXWidget());

通过 constrains 即可获得 XXXWidget 的最大和最小尺寸了。由于 PageView 会尽可能大地占满屏幕,所以 constrains.maxWidth 即为 PageView 在运行时真实的宽度。

其次是如何获取滚动事件,Flutter 内建了 Notification 机制,一个 Widget 可以通过 Notification 将一个事件冒泡到 Widget Tree 的上层节点。

监听一个 Notification 的方法是用 NotificationListener 包裹需要监听事件的子视图,具体的使用过程在下文会讲到。

Flutter 会根据平台自动应用一些动画和视觉效果,例如 PageView 在 iOS 上会产生越界回弹效果(也就是视频所展示的),而在 Android 上会呈现波纹的效果,这个效果可以通过指定滚动视图的 ScrollPhysics 来实现。

下面我们来看一下最终实现的代码:

class PageDemo extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new _PageDemoState();
}

class _PageDemoState extends State<PageDemo> {
  final PageController _pageController = new PageController();
  double _currentPage = 0.0;

  @override
  Widget build(BuildContext context) => new Scaffold(
    appBar: new AppBar(
      title: const Text('Page Demo'),
    ),
    body: new LayoutBuilder(builder: (context, constraints) => new NotificationListener(
      onNotification: (ScrollNotification note) {
        setState(() {
          _currentPage = _pageController.page;
        });
      },
      child: new PageView.custom(
        physics: const PageScrollPhysics(parent: const BouncingScrollPhysics()),
        controller: _pageController,
        childrenDelegate: new SliverChildBuilderDelegate(
          (context, index) => new _SimplePage(
            '$index',
            parallaxOffset: constraints.maxWidth / 2.0 * (index - _currentPage),
          ),
          childCount: 10,
        ),
      ),
    )),
  );
}

为了获取滚动的进度,我们可以指定 PageController,它是 ScrollController 的子类,用于管理滚动视图的状态,它同时也暴露了一系列属性和方法用于控制滚动视图,在构建 PageView 时手动指定一个 PageController 便可以之后获取 PageView 的状态。

需要注意的是,NotificationListener 在调用 onNotification 时并不会刷新视图,因此我们还需要调用根视图的 setState 来刷新视图状态。

接下来实现轮播页面:

class _SimplePage extends StatelessWidget {
  _SimplePage(this.data, {
    Key key,
    this.parallaxOffset = 0.0
  }) : super(key: key);

  final String data;
  final double parallaxOffset;

  @override
  Widget build(BuildContext context) => new Center(
    child: new Center(
      child: new Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          new Text(
            data,
            style: const TextStyle(fontSize: 60.0),
          ),
          new SizedBox(height: 40.0),
          new Transform(
            transform: new Matrix4.translationValues(parallaxOffset, 0.0, 0.0),
            child: const Text('Yet another line of text'),
          ),
        ],
      ),
    ),
  );
}

这里我们直接使用 StatelessWidget 即可,通过接受构造方法的参数,将 translation 传递给 Transform 元素即可。

小结

通过这个例子我们可以看出,在 Flutter 中,所有的效果都是以声明式的方法实现的,很少会用到 OOP 的方式,你几乎不会看到像 setOpacity、setScale 的这类方法。另外 Flutter 广泛地使用了各种层次结构来实现各种扩展效果,比如变换、滤镜、大小约束这样的效果都是通过为内容视图包裹一层装饰视图来实现的,具体的就要多看官方文档了。总而言之,在 Flutter 中实现复杂的界面效果还是非常容易的。