設計語言(例如 Material Design)定義了路由(或螢幕)之間過渡的標準行為。然而,有時自定義的螢幕過渡可以使應用更具特色。為此,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' widget 的 2D 向量。將 dy 引數設定為 1 表示垂直平移一個完整頁面高度。

transitionsBuilder 回撥有一個 animation 引數。它是一個 Animation<double>,生成 0 到 1 之間的值。使用 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 的 widget,它們會在動畫值改變時自動重建。例如,SlideTransition 接受一個 Animation<Offset> 並在動畫值改變時平移其子 widget(使用 FractionalTranslation widget)。

AnimatedWidget 返回一個帶有 Animation<Offset> 和子 widget 的 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 使動畫開始快速,結束緩慢。

要使用 Curve,建立一個新的 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));

然後透過將其傳遞給 animation.drive() 來使用這個 tween。這會建立一個新的 Animation<Offset>,可以將其提供給 SlideTransition widget。

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