为页面切换加入动画效果

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

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

  1. 搭建一个 PageRouteBuilder

  2. 创建一个 Tween

  3. 添加一个 AnimatedWidget

  4. 使用 CurveTween

  5. 组合这两个 Tween

1. 搭建一个 PageRouteBuilder

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

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

import 'package:flutter/material.dart';

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

class Page1 extends StatelessWidget {
  const Page1({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.of(context).push(_createRoute());
          },
          child: const Text('Go!'),
        ),
      ),
    );
  }
}

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

class Page2 extends StatelessWidget {
  const Page2({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const Center(
        child: Text('Page 2'),
      ),
    );
  }
}

2. 创建一个 Tween

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

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

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

3. 使用 AnimatedWidget

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

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

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

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

4. 使用 CurveTween

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

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

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

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

5. 结合两个 Tween

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

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

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

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

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

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

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

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

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

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

transitionsBuilder: (context, animation, secondaryAnimation, child) {
  const begin = Offset(0.0, 1.0);
  const end = Offset.zero;
  const curve = Curves.ease;

  final tween = Tween(begin: begin, end: end);
  final curvedAnimation = CurvedAnimation(
    parent: animation,
    curve: curve,
  );

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

交互式样例

import 'package:flutter/material.dart';

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

class Page1 extends StatelessWidget {
  const Page1({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.of(context).push(_createRoute());
          },
          child: const Text('Go!'),
        ),
      ),
    );
  }
}

Route _createRoute() {
  return PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const Page2(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(0.0, 1.0);
      const end = Offset.zero;
      const 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 {
  const Page2({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const Center(
        child: Text('Page 2'),
      ),
    );
  }
}