Flutter 建立自定義路由過渡動畫

冷石_發表於2019-12-26

前言

Flutter 中使用 Navigator.of(context).push(Route route); 方法進行路由跳轉時就需要傳一個 Route 物件,通常使用 MaterialPageRoute(builder: () {}); 建立,使用時會在路由跳轉過程中新增預設的過渡動畫。當需要自定義路由過渡動畫時,就要使用 PageRouteBuilder,它是 Flutter 提供的用來建立自定義的路由的一個類,例項化這個類會得到一個路由物件 Route,要做的就是建立一個自定義的 Route

PageRouteBuilder

使用 PageRouteBuilder 建立自定義路由過渡動畫時需要傳入兩個回撥函式作為引數,一個必要引數 pageBuilder,這個函式用來建立跳轉的頁面,另一個函式 transitionsBuilder,這個函式就是實現過渡動畫的地方。

transitionsBuilderchild 引數是 pageBuilder 函式返回的一個 transitionsBuilder widget 部件, pageBuilder 方法僅會在第一次構建路由的時候被呼叫,Flutter 能夠自動避免做額外的工作,整個過渡期間 child 儲存了同一個例項。

PageRouteBuilder(
  pageBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
    ) {
      return widget;
    },
    transitionsBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
    ) {
      return child;
    },
);
複製程式碼

建立自定義路由需要繼承 PageRouteBuilder,然後實現自定義路由的建構函式。

// 定義
class YourRoute extends PageRouteBuilder {
  final Widget page;

  YourRoute(this.page)
      : super(
          pageBuilder: (
            context,
            animation,
            secondaryAnimation,
          ) {
            return page;
          },
          transitionsBuilder: (
            context,
            animation,
            secondaryAnimation,
            child,
          ) {
            return child;
          },
        );
}

// 使用
Navigator.of(context).push(YourRoute(NewPage()));
複製程式碼

示例

使用 FirstPageSecondPage 兩個頁面展示效果

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: FirstPage(),
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Page'),
        elevation: 0.0,
        backgroundColor: Colors.purple,
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Center(
            child: RaisedButton(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) => SecondPage()),
                );
              },
              child: Text('Next Page'),
            ),
          )
        ],
      ),
      backgroundColor: Colors.purple,
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
        elevation: 0.0,
        backgroundColor: Colors.deepPurpleAccent,
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Center(
            child: RaisedButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: Text('Go Back'),
            ),
          )
        ],
      ),
      backgroundColor: Colors.deepPurpleAccent,
    );
  }
}

複製程式碼

FadeTransition

class FadeRoute extends PageRouteBuilder {
  final Widget page;

  FadeRoute(this.page)
      : super(
          pageBuilder: (
            context,
            animation,
            secondaryAnimation,
          ) {
            return page;
          },
          transitionsBuilder: (
            context,
            animation,
            secondaryAnimation,
            child,
          ) {
            return FadeTransition(
              opacity: animation,
              child: child,
            );
          },
        );
}
複製程式碼

ScaleTransition

class ScaleRoute extends PageRouteBuilder {
  final Widget page;

  ScaleRoute(this.page)
      : super(
          pageBuilder: (
            context,
            animation,
            secondaryAnimation,
          ) {
            return page;
          },
          transitionsBuilder: (
            context,
            animation,
            secondaryAnimation,
            child,
          ) {
            return ScaleTransition(
              alignment: Alignment.bottomLeft,
              scale: Tween(
                begin: 0.0,
                end: 1.0,
              ).animate(
                CurvedAnimation(
                  parent: animation,
                  curve: Curves.easeInOut,
                ),
              ),
              child: child,
            );
          },
          transitionDuration: Duration(seconds: 1),
        );
}

Navigator.of(context).push(ScaleRoute(SecondPage()));
複製程式碼

RotationTransition

class RotationRoute extends PageRouteBuilder {
  final Widget page;

  RotationRoute(this.page)
      : super(
          pageBuilder: (
            context,
            animation,
            secondaryAnimation,
          ) {
            return page;
          },
          transitionsBuilder: (
            context,
            animation,
            secondaryAnimation,
            child,
          ) {
            Animation myAnimation = CurvedAnimation(
              parent: animation,
              curve: Curves.easeInBack,
            );

            return RotationTransition(
              turns: myAnimation,
              child: child,
            );
          },
          transitionDuration: Duration(seconds: 1),
        );
}

Navigator.of(context).push(RotationRoute(SecondPage()));
複製程式碼

ScaleRotationRoute

結合兩個過渡動畫

class ScaleRotationRoute extends PageRouteBuilder {
  final Widget page;

  ScaleRotationRoute(this.page)
      : super(
          pageBuilder: (
            context,
            animation,
            secondaryAnimation,
          ) {
            return page;
          },
          transitionsBuilder: (
            context,
            animation,
            secondaryAnimation,
            child,
          ) {
            return ScaleTransition(
              scale: animation,
              child: RotationTransition(
                turns: Tween(
                  begin: 0.0,
                  end: 1.0,
                ).animate(
                  CurvedAnimation(parent: animation, curve: Curves.linear),
                ),
                child: child,
              ),
            );
          },
          transitionDuration: Duration(milliseconds: 800),
        );
}

Navigator.of(context).push(ScaleRotationRoute(SecondPage()));
複製程式碼

TransformRoute

使用 Transform 部件創造 3D 效果

import 'dart:math' show pi;

class TransformRoute extends PageRouteBuilder {
  final Widget page;

  TransformRoute(this.page)
      : super(
          pageBuilder: (
            context,
            animation,
            secondaryAnimation,
          ) {
            return page;
          },
          transitionsBuilder: (
            context,
            animation,
            secondaryAnimation,
            child,
          ) {
            return Transform(
              transform: Matrix4.identity()
                // 類似於 CSS 裡面 `perspective` 屬性,確定 z=0 平面與使用者之間的距離
                ..setEntry(3, 2, 0.0001)
                ..rotateX(animation.value * pi * 2)
                ..rotateY(animation.value * pi * 2),
              alignment: FractionalOffset.center,
              child: child,
            );
          },
          transitionDuration: Duration(seconds: 2),
        );
}

Navigator.of(context).push(TransformRoute(SecondPage()));
複製程式碼

EnterExitRoute

同時為進入頁面和退出頁面新增動畫

class EnterExitRoute extends PageRouteBuilder {
  final Widget enterPage;
  final Widget exitPage;

  EnterExitRoute(this.enterPage, this.exitPage)
      : super(
          pageBuilder: (
            context,
            animation,
            secondaryAnimation,
          ) {
            return exitPage;
          },
          transitionsBuilder: (
            context,
            animation,
            secondaryAnimation,
            child,
          ) =>
              Stack(
            children: [
              SlideTransition(
                position: Tween<Offset>(
                  begin: Offset(0.0, 0.0),
                  end: Offset(-1.0, 0.0),
                ).animate(
                  CurvedAnimation(parent: animation, curve: Curves.easeIn),
                ),
                child: enterPage,
              ),
              SlideTransition(
                position: Tween<Offset>(
                  begin: Offset(1.0, 0.0),
                  end: Offset.zero,
                ).animate(
                  CurvedAnimation(parent: animation, curve: Curves.easeInOut),
                ),
                child: exitPage,
              )
            ],
          ),
        );
}

Navigator.of(context).push(
  EnterExitRoute(FirstPage(), SecondPage()),
);
複製程式碼

使用 Navigator.pushNamed 方法跳轉

onGenerateRoute 對跳轉路由的 name 進行判斷,對特定的路由新增過渡動畫。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: FirstPage(),
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case '/second':
            return ScaleRoute(SecondPage());
            break;
          default:
            return null;
        }
      },
    );
  }
}

Navigator.pushNamed(context, '/second', arguments: {});
複製程式碼

設定全域性的路由過渡動畫

Flutter 的預設路由過渡動畫是由 buildTransitions 方法建立的,它使用的是 Theme.of(context).pageTransitionsTheme方法,因此可以定義全域性的路由跳轉過渡動畫。

@override
Widget buildTransitions(context, animation, secondaryAnimation, child) {
    final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
    return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}
複製程式碼

首先自定義一個 TransitionBuilderbuildTransitions 方法返回跳轉頁面。然後配置 themepageTransitionsTheme,設定對應的平臺,最後在使用 MaterialPageRoute 或者 CupertinoPageRoute 進行頁面跳轉時就會有自定義的過渡動畫了。

class ScaleTransitionBuilder extends PageTransitionsBuilder {
  @override
  Widget buildTransitions<T>(
    route,
    context,
    animation,
    secondaryAnimation,
    child,
  ) {
    return ScaleTransition(
      scale: CurvedAnimation(parent: animation, curve: Curves.easeIn),
      child: child,
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: FirstPage(),
      theme: ThemeData(
        pageTransitionsTheme: PageTransitionsTheme(
          builders: {
            TargetPlatform.android: ScaleTransitionBuilder(),
            TargetPlatform.iOS: ScaleTransitionBuilder(),
          },
        ),
      ),
    );
  }
}

Navigator.push(context, MaterialPageRoute(builder: (ctx) => SecondPage()));
複製程式碼

將動畫封裝成一個庫

將自定義的路由過渡動畫封裝起來方便使用。

enum TransitionType {
  fade,
  scale,
  rotate,
  transform,
}

class PageTransition extends PageRouteBuilder {
  PageTransition(TransitionType type, Widget page, Duration time)
      : super(
          pageBuilder: (
            context,
            animation,
            secondaryAnimation,
          ) {
            return page;
          },
          transitionsBuilder: (
            context,
            animation,
            secondaryAnimation,
            child,
          ) {
            switch (type) {
              case TransitionType.fade:
                return FadeTransition(opacity: animation, child: child);
                break;
              case TransitionType.scale:
                return ScaleTransition(
                  scale: Tween(begin: 0.0, end: 1.0).animate(
                    CurvedAnimation(parent: animation, curve: Curves.easeInOut),
                  ),
                  child: child,
                );
                break;
              case TransitionType.rotate:
                return RotationTransition(
                  turns: CurvedAnimation(
                    parent: animation,
                    curve: Curves.easeInBack,
                  ),
                  child: child,
                );
                break;
              case TransitionType.transform:
                return Transform(
                  transform: Matrix4.identity()
                    ..setEntry(3, 2, 0.0001)
                    ..rotateX(animation.value * pi * 2)
                    ..rotateY(animation.value * pi * 2),
                  alignment: FractionalOffset.center,
                  child: child,
                );
                break;
              default:
                return child;
            };
          },
          transitionDuration: time,
        );
}

// 使用
Navigator.push(
  context,
  PageTransition(
    TransitionType.rotate,
    SecondPage(),
    Duration(milliseconds: 800),
  ),
}
複製程式碼

原文地址

參考文章

為頁面切換加入動畫效果

Everything you need to know about Flutter page route transition

Perspective on Flutter

相關文章