为页面切换加入动画效果

在不同路由(或界面)之间进行切换的时候,许多设计语言,例如 Material 设计,都定义了一些标准行为。但有时自定义路由会让 app 看上去更加的独特。为了更好的完成这一点, PageRouteBuilder 提供了一个 Animation 对象。这个 Animation 能够通过结合 Tween 以及 Curve 对象来自定义路由转换动画。这篇指南将会展示如何在两个路由之间切换时使用从屏幕底部动画出来的路由。

A design language, such as Material, defines standard behaviors when transitioning between routes (or screens). Sometimes, though, a custom transition between screens can make an app more unique. To help, PageRouteBuilder provides an Animation object. This Animation can be used with Tween and Curve objects to customize the transition animation. This recipe shows how to transition between routes by animating the new route into view from the bottom of the screen.

要创建这个自定义路由动画,这篇指南使用了以下步骤:

To create a custom page route transition, this recipe uses the following steps:

  1. 搭建一个 PageRouteBuilder

    Set up a PageRouteBuilder

  2. 创建一个 Tween

    Create a Tween

  3. 添加一个 AnimatedWidget

    Add an AnimatedWidget

  4. 使用 CurveTween

    Use a CurveTween

  5. 组合这两个 Tween

    Combine the two Tweens

1. 搭建一个 PageRouteBuilder

1. Set up a PageRouteBuilder

我们从使用一个 PageRouteBuilder 来创建一个 Route 开始。 PageRouteBuilder 有两个回调,第一个是创建这个路由的内容(pageBuilder),另一个则是创建一个路由的转换器(transitionsBuilder)。

To start, use a PageRouteBuilder to create a Route. PageRouteBuilder has two callbacks, one to build the content of the route (pageBuilder), and one to build the route’s transition (transitionsBuilder).

下面的样例将会创建两个路由:一个主页路由,包含了 “Go!” 按钮,还有第二个路由,包含了一个显示 “Page 2 的标题。

The following example creates two routes: a home route with a “Go!” button, and a second route titled “Page 2”.

import 'package:flutter/material.dart';

main() {
  runApp(MaterialApp(
    home: Page1(),
  ));
}

class Page1 extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: RaisedButton(
          child: Text('Go!'),
          onPressed: () {
            Navigator.of(context).push(_createRoute());
          },
        ),
      ),
    );
  }
}

Route _createRoute() {
  return PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => Page2(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return child;
    },
  );
}

class Page2 extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text('Page 2'),
      ),
    );
  }
}

2. 创建一个 Tween

2. Create a Tween

为了使新页面从底部动画出来,它应该从 Offset(0,1)Offset(0, 0) 进行动画。(通常我们会使用 Offset.zero 构造器。)在这个情况下,对于 FractionalTranslation widget 来说偏移量是一个 2D 矢量值。将 dy 参数设为 1,这代表在竖直方向上切换整个页面的高度。

To make the new page animate in from the bottom, it should animate from Offset(0,1) to Offset(0, 0) (usually defined using the Offset.zero constructor). In this case, the Offset is a 2D vector for the FractionalTranslation widget. Setting the dy argument to 1 represents a vertical translation one full height of the page.

transitionsBuilder 的回调有一个 animation 参数。它其实是一个 Animation<double>,提供 0 到 1 的值。使用 Tween 来将 Animation 转为 Animation

The transitionsBuilder callback has an animation parameter. It’s an Animation<double> that produces values between 0 and 1. Convert the Animation into an Animation using a Tween:

transitionsBuilder: (context, animation, secondaryAnimation, child) {
  var begin = Offset(0.0, 1.0);
  var end = Offset.zero;
  var tween = Tween(begin: begin, end: end);
  var offsetAnimation = animation.drive(tween);
  return child;
},

3. 使用 AnimatedWidget

3. Use an AnimatedWidget

Flutter 有一堆继承至 AnimatedWidget 的 widget,它们能够在动画的值发生改变时自动重建自己。举个例子,SlideTransition 拿到一个 Animation<Offset> 并在动画改变时使用 FractionalTranslation widget 转换其子级。

Flutter has a set of widgets extending AnimatedWidget that rebuild themselves when the value of the animation changes. For instance, SlideTransition takes an Animation<Offset> and translates its child (using a FractionalTranslation widget) whenever the value of the animation changes.

AnimatedWidget 返回了一个带有 Animation<Offset>SlideTransition,以及 child widget:

AnimatedWidget Return a SlideTransition with the Animation<Offset> and the child widget:

transitionsBuilder: (context, animation, secondaryAnimation, child) {
  var begin = Offset(0.0, 1.0);
  var end = Offset.zero;
  var tween = Tween(begin: begin, end: end);
  var offsetAnimation = animation.drive(tween);

  return SlideTransition(
    position: offsetAnimation,
    child: child,
  );
},

4. 使用 CurveTween

4. Use a CurveTween

Flutter 提供了一系列缓和曲线,可以调整一段时间内的动画速率。 Curves 类提供了一个提前定义的用法相似的 curves。例如,Curves.easeOut 将会让动画开始很快结束很慢。

Flutter provides a selection of easing curves that adjust the rate of the animation over time. The Curves class provides a predefined set of commonly used curves. For example, Curves.easeOut will make the animation start quickly and end slowly.

要使用 Curve,创建一个 CurveTween 并传一个 Curve:

To use a Curve, create a new CurveTween and pass it a Curve:

var curve = Curves.ease;
var curveTween = CurveTween(curve: curve);

新的 Tween 依然提供 0 到 1 之间的值。在下一步中,它将会结合第二步中提到的 Tween<Offset>

This new Tween still produces values from 0 to 1. In the next step, it will be combined the Tween<Offset> from step 2.

5. 结合两个 Tween

5. Combine the two Tweens

为了结合两个 tween,请使用 chain():

To combine the tweens, use chain():

var begin = Offset(0.0, 1.0);
var end = Offset.zero;
var curve = Curves.ease;

var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

它们通过把这个 tween 传递给 animation.drive() 来创建一个新的 Animation<Offset>,然后你就能把它传给 SlideTransition widget:

Then use this tween by passing it to animation.drive(). This creates a new Animation<Offset> that can be given to the SlideTransition widget:

return SlideTransition(
  position: animation.drive(tween),
  child: child,
);

这个新的 Tween(或者是能够动画的东西)通过评估 CurveTween 来提供 Offset,然后评估 Tween<Offset>。当动画运行时,值都被这条命令计算出:

This new Tween (or Animatable) produces Offset values by first evaluating the CurveTween, then evaluating the Tween<Offset>. When the animation runs, the values are computed in this order:

  1. 这个动画提供了从 0 到 1 的值。(通过 transitionsBuilder 的值提供)

    The animation (provided to the transitionsBuilder callback) produces values from 0 to 1.

  2. 这个 CurveTween 根据其将这些值映射到介于 0 和 1 之间的新曲线值。

    The CurveTween maps those values to new values between 0 and 1 based on its curve.

  3. Tween<Offset>double 值映射为 Offset 值。

    The Tween<Offset> maps the double values to Offset values.

使用缓动曲线创建 Animation<Offset> 的另一种方法是使用 CurvedAnimation

Another way to create an Animation<Offset> with an easing curve is to use a CurvedAnimation:

transitionsBuilder: (context, animation, secondaryAnimation, child) {
  var begin = Offset(0.0, 1.0); 
  var end = Offset.zero;
  var curve = Curves.ease;
  
  var tween = Tween(begin: begin, end: end);
  var curvedAnimation = CurvedAnimation(
   parent: animation,
   curve: curve,
  );

  return SlideTransition(
   position: tween.animate(curvedAnimation),
   child: child,
  );
}

完整样例

Complete Example

import 'package:flutter/material.dart';

main() {
  runApp(MaterialApp(
    home: Page1(),
  ));
}

class Page1 extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: RaisedButton(
          child: Text('Go!'),
          onPressed: () {
            Navigator.of(context).push(_createRoute());
          },
        ),
      ),
    );
  }
}

Route _createRoute() {
  return PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => Page2(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      var begin = Offset(0.0, 1.0);
      var end = Offset.zero;
      var curve = Curves.ease;

      var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

      return SlideTransition(
        position: animation.drive(tween),
        child: child,
      );
    },
  );
}

class Page2 extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text('Page 2'),
      ),
    );
  }
}

Demo showing a custom page route transition animating up from the bottom of the screen