跳至主要內容

為頁面路由轉換加入動畫效果

設計語言,例如 Material,定義了在路由(或螢幕)之間轉換時的標準行為。然而,有時在螢幕之間進行自訂轉換可以使應用程式更獨特。為了協助,PageRouteBuilder 提供了一個 Animation 物件。此 Animation 可以與 TweenCurve 物件一起使用,以自訂轉換動畫。本食譜展示如何透過從螢幕底部將新路由動畫移入視圖來在路由之間轉換。

要建立自訂頁面路由轉換,本食譜使用以下步驟

  1. 設定 PageRouteBuilder
  2. 建立 Tween
  3. 新增 AnimatedWidget
  4. 使用 CurveTween
  5. 合併兩個 Tween

1. 設定 PageRouteBuilder

#

首先,使用 PageRouteBuilder 建立一個 RoutePageRouteBuilder 有兩個回呼函數,一個用於建立路由的內容 (pageBuilder),另一個用於建立路由的轉換 (transitionsBuilder)。

以下範例建立兩個路由:一個帶有「前往!」按鈕的首頁路由,以及一個標題為「第 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 _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>,並在動畫的值變更時翻譯其子元件(使用 FractionalTranslation Widget)。

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 並將曲線傳遞給它

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