システム開発部のTです。
前回、Flutterでの画面遷移の話をしましたが、今回のその画面遷移に対してアニメーションを実装する話です。実は前回でも遷移アニメーションについては、簡単に実装してはいましたが、本件はあらためて遷移時のアニメーションについて、つらつら書いていきたいと思います。

前回の話はこちらから。
Flutterの記事シリーズについてはこちらから。


アプリ全体のアニメーションを定義する

これは前回の記事でも紹介した方法になります。
MaterialAppのthemeに対して、PageTransitionsThemeで設定した画面遷移の定義をpageTransitionsThemeに設定することで、iOS、Android別に設定が可能となります。デフォルトでは、Android、iOSは別々に定義されていますね。

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        pageTransitionsTheme: const PageTransitionsTheme( //ここを追加
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
            TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
          },
        ),
      ),
      initialRoute: '/', 
      routes: {
        '/': (context) => FirstPage(),
        '/next': (context) => NextPage(),
      },
    );
  }
}

これを、AndroidもiOSと同様の動きにしたいのであれば、iOSに設定されているCupertinoPageTransitionsBuilder()をAndroidにも設定することで、同様の動きになります。逆もしかり。


各画面遷移ごとにアニメーションを設定する

ここまでは、前回の記事にもあった内容です。
では、各画面への遷移ごとにアニメーションを変えたい場合はどうすればいいでしょうか。以降では、その実装手段を書いていきたいと思います。

Navigator.pushに直接設定する方法

まずはお手軽?にNavigator.pushに直接設定するやり方です。
pushの第2引数にPageRouteBuilderのインスタンス生成し、それぞれpageBuilderには画面遷移先を、transitionsBuilderに画面遷移アニメーションを設定します。本件では、以降で紹介するFadeUpwardsPageTransitionsBuilderを設定しました。

Navigator.push(context, PageRouteBuilder(
	pageBuilder: (context, animation, secondaryAnimation) => NextPage(),
	transitionsBuilder: (context, animation, secondaryAnimation, child) {
                      return FadeUpwardsPageTransitionsBuilder()
                          .buildTransitions(
                            MaterialPageRoute(builder: (context)=>NextPage()),
                            context,
                            animation,
                            secondaryAnimation,
                            child
                          );
	},
))

上記を組み込んで、アプリを起動します。
どうでしょうか、画面遷移時に下からせり上がるようなアニメーションで画面遷移したかと思います。

Navigation.pushNamedで画面遷移している場合

Navigation.pushNamed自体には直接設定はできないようなので、ルーティングの設定部でアニメーションの実装をします。MaterialAppのroutesを削除し、代わりに以下のonGenerateRouteに定義していきます。

      onGenerateRoute: (settings) { // 
        switch(settings.name) {
          case '/': {
            return PageRouteBuilder(
                pageBuilder: (_, __, ___)=>FirstPage(),
                transitionsBuilder: (context, animation, secondaryAnimation, child){
                  return FadeTransition(opacity: animation, child: child,);
                }
            );
          }
          case '/next': {
            return PageRouteBuilder(
                pageBuilder: (_, __, ___)=>NextPage(),
                transitionsBuilder: (context, animation, secondaryAnimation, child){
                  return FadeUpwardsPageTransitionsBuilder()
                      .buildTransitions(
                      MaterialPageRoute(builder: (context)=>NextPage()),
                      context,
                      animation,
                      secondaryAnimation,
                      child
                  );                }
            );
          }
          default: {
            return MaterialPageRoute(builder: (context) => FirstPage());
          }
        }
      },

上記のように、コールバックでsettings値が返却されるので、settings.nameでパス名の取得ができます。それを条件判定入れて、PageRouteBuilderで遷移先、アニメーションの指定をします。

サンプルコードですが、以下の感じで定義していきます。
以下のサンプルでは、Path’/’が来たとき、トップページにフェードアニメーションで遷移、Path’/next’だったら、次ページをFadeUpwardsPageTransitionsBuilderのアニメーションで遷移するように設定しています。

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialRoute: '/',
      // routes: { // routesはコメントに
      //   '/': (context) => FirstPage(),
      //   '/next': (context) => NextPage(),
      // },
      onGenerateRoute: (settings) { // ここを追加
        switch(settings.name) {
          case '/': {
            return PageRouteBuilder(
                pageBuilder: (_, __, ___)=>FirstPage(),
                transitionsBuilder: (context, animation, secondaryAnimation, child){
                  return FadeTransition(opacity: animation, child: child,);
                }
            );
          }
          case '/next': {
            return PageRouteBuilder(
                pageBuilder: (_, __, ___)=>NextPage(),
                transitionsBuilder: (context, animation, secondaryAnimation, child){
                  return FadeUpwardsPageTransitionsBuilder()
                      .buildTransitions(
                      MaterialPageRoute(builder: (context)=>NextPage()),
                      context,
                      animation,
                      secondaryAnimation,
                      child
                  );                }
            );
          }
          default: {
            return MaterialPageRoute(builder: (context) => FirstPage());
          }
        }
      },
    );
  }

上記を実装して、アプリを実行してみます。

いかがでしょうか。
画面遷移をするための実装には、いくつかの実装手段があることをご理解いただけたのかと思います。

ここまでは遷移時のアニメーションを定義する実装方法を説明してきました。
続けては、デフォルトでのアニメーションの説明と、カスタムアニメーションについて説明していきたいと思います。


標準アニメーションの種類

上記に登場してきたFadeUpwardsPageTransitionsBuilderCupertinoPageTransitionsBuilderですが、他にもTransitionsBuilderシリーズは用意されていますので、以下に紹介したいと思います。

なお、以下のアニメーションの実装については、Flutterのpage_transitions_theme.dartにありますので、実際のコードがどうなっているのか、一度見てもいいかと思います。

FadeUpwardsPageTransitionsBuilder

iOS以外のOSでデフォルト設定されているアニメーション。

CupertinoPageTransitionsBuilder

iOSでのデフォルト設定されているアニメーション。

OpenUpwardsPageTransitionsBuilder

ページめくりっぽい感じのアニメーション。

ZoomPageTransitionsBuilder

これはAndroid端末でも時々見かけるかな・・・。


カスタム遷移アニメーションを作ってみる

上記以外のアニメーションパターンを作ることは可能です。
ただ、本当に凝ったアニメーションを作るのであれば、ここでは語りきれないところもあるので、本件では簡単に実装できるような内容にします。

まず、iOSデフォルトアニメーションのCupertinoPageTransitionsBuilderのコードを見てみましょう。

class CupertinoPageTransitionsBuilder extends PageTransitionsBuilder {
  /// Construct a [CupertinoPageTransitionsBuilder].
  const CupertinoPageTransitionsBuilder();

  @override
  Widget buildTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return CupertinoPageRoute.buildPageTransitions<T>(route, context, animation, secondaryAnimation, child); // ここでAnimatedWidgetを返している
  }
}

上記を見てもらうとわかりますが、buildTransitionsメソッドでAnimatedWidgetクラスを継承したクラスインスタンスを返しているので、同じようなクラスを作ればいけそうです。

AnimatedWidgetを継承しているクラスですが、例えばFadeTransitionとか、SlideTransition等がそれに当たります。

公式を眺めていると、AnimatedWidgetで実装するための記事がありましたので、それを参考に作ってみようと思います。

https://flutter.dev/docs/cookbook/animation/page-route-animation#3-use-an-animatedwidget

CupertinoPageTransitionsBuilderを元に作った内容は以下になります。

class CustomTransitionsBuilder extends PageTransitionsBuilder {
  const CustomTransitionsBuilder();

  @override
  Widget buildTransitions<T>(
      PageRoute<T> route,
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
      ) {

    var begin = Offset(0.0, 1.0);
    var end = Offset.zero;
    var tween = Tween(begin: begin, end: end);
    var offsetAnimation = animation.drive(tween);

    return SlideTransition(
      position: offsetAnimation,
      child: child,
    );
  }
}

上記は右側から次画面が出てくるようなイメージのアニメーションになります。
(ようするにCupertinoPageTransitionsBuilderの動きと同様・・・)

上記をMaterialAppのテーマに反映させます。

class MyApp extends StatelessWidget{

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        pageTransitionsTheme: const PageTransitionsTheme(
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: CustomTransitionsBuilder(), //ここに反映
            TargetPlatform.iOS: CustomTransitionsBuilder(), //ここに反映
          },
        ),
      ),
      initialRoute: '/',  // ここ以降を追加
      routes: {
        '/': (context) => FirstPage(),
        '/next': (context) => NextPage(),
      },
    );
  }
}

これで実行してみましょう。

どうでしょうか。
Transitionを利用したアニメーションの知識があれば、もっと複雑なアニメーションも可能かと思います。興味ある方はやってみてください。

まとめ

本記事では、画面遷移時のアニメーションの実装について以下の内容で説明してきました。

  • アプリ全体の画面遷移アニメーション定義
  • 画面遷移ごとにアニメーション定義
  • 標準アニメーションについて
  • カスタムアニメーションについて

アニメーションについては、アプリ全体のテーマに基本となるアニメーションを定義し、必要な箇所で随時定義していくのが普通かと思います。

iOSの画面の場合、画面遷移のアニメーションは、右横から次の画面が出てくるアニメーションになるかと思いますが、モーダル画面は下からせり上がるようなアニメーションにすると、iOSっぽくなるのでは?とも思います。

本件で、画面遷移におけるアニメーションの話はおしまいにしますが、画面遷移のネタはまだありますので、引き続き見ていただければと思います。