跳到主内容

动画化页面路由过渡

如何实现页面间的转场动画。

设计语言(例如 Material)定义了路由(或屏幕)之间转换时的标准行为。然而有时,自定义屏幕间的转场可以让你的应用更具独特性。为了实现这一点,PageRouteBuilder 提供了一个 Animation 对象。该 Animation 对象可以与 TweenCurve 对象结合使用,以自定义转场动画。本篇指南将展示如何通过从屏幕底部滑入的方式来实现路由间的转场动画。

要创建自定义页面路由转场,本指南采用以下步骤:

  1. 设置 PageRouteBuilder
  2. 创建 Tween
  3. 添加 AnimatedWidget
  4. 使用 CurveTween
  5. 组合两个 Tween

1. 设置 PageRouteBuilder

#

首先,使用 PageRouteBuilder 来创建一个 RoutePageRouteBuilder 有两个回调函数:一个用于构建路由内容 (pageBuilder),另一个用于构建路由的转场效果 (transitionsBuilder)。

以下示例创建了两个路由:一个带有“Go!”按钮的主页路由,以及第二个标题为“Page 2”的路由。

dart
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<void> _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 构造函数定义)。在这种情况下,Offset 是 FractionalTranslation 组件的一个 2D 向量。将 dy 参数设置为 1,代表在垂直方向上平移整个页面高度的距离。

transitionsBuilder 回调函数有一个 animation 参数。这是一个产生 0 到 1 之间值的 Animation<double>。使用 Tween 将 Animation<double> 转换为 Animation<Offset>

dart
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 的组件,它们会在动画值发生变化时自动重建自身。例如,SlideTransition 接收一个 Animation<Offset>,并在动画值改变时平移其子组件(使用 FractionalTranslation 组件)。

AnimatedWidget:返回一个带有 Animation<Offset> 和子组件的 SlideTransition

dart
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.easeOut 使动画开始时较快,结束时较慢。

要使用曲线,创建一个新的 CurveTween 并传入一个 Curve

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

这个新的 Tween 产生的仍然是 0 到 1 的值。在下一步中,它将与第 2 步中的 Tween<Offset> 组合使用。

5. 组合两个 Tween

#

要组合这些 Tween,请使用 chain()

dart
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 组件:

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

这个新的 Tween(或 Animatable)通过先评估 CurveTween,再评估 Tween<Offset> 来产生 Offset 值。当动画运行时,数值会按以下顺序计算:

  1. 动画(提供给 transitionsBuilder 回调)产生 0 到 1 之间的值。
  2. CurveTween 根据其曲线将这些值映射为 0 到 1 之间的新值。
  3. Tween<Offset> 将这些 double 值映射为 Offset 值。

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

dart
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<void> _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')),
    );
  }
}