为页面路由过渡设置动画

设计语言,例如 Material,在路由(或屏幕)之间切换时定义了标准行为。但是,有时自定义屏幕之间的过渡可以使应用程序更独特。为了帮助,PageRouteBuilder 提供了一个 Animation 对象。此 Animation 可以与 TweenCurve 对象一起使用以自定义过渡动画。此食谱展示了如何通过从屏幕底部动画新路由进入视图来在路由之间进行过渡。

要创建自定义页面路由过渡,此食谱使用以下步骤

  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 构造函数定义)。在这种情况下,Offset 是 'FractionalTranslation' 小部件的二维向量。将 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 的小部件,当动画的值发生变化时,它们会重新构建自身。例如,SlideTransition 接受一个 Animation<Offset>,并在动画的值发生变化时平移其子级(使用 FractionalTranslation 小部件)。

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

飞镖
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 并向其传递一个曲线。

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

这个新的 Tween 仍然产生 0 到 1 之间的值。在下一步中,它将与步骤 2 中的 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));

然后通过将其传递给 animation.drive() 来使用此 tween。这将创建一个新的 Animation<Offset>,可以将其提供给 SlideTransition 小部件。

飞镖
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

飞镖
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'),
      ),
    );
  }
}