画面遷移アニメーション

ここでは、Flutterで画面遷移アニメーションをカスタマイズする方法を紹介していきます。
例えば、スライドインやフェードアウトなど画面遷移する時のアニメーションを指定したい場面があるでしょう。
そのような、UI・UXを意識したアプリ開発に役立ててください。

PageRouteBuilder

画面遷移時は以下のように、MaterialPageRoute / CupertinoPageRoute を使う事が多いでしょう。
しかし、これらは Android / iOS の作法に沿ったデザインが適用されているため、アニメーション等はカスタマイズできません。

Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) { return Page2(); },
  ),
);

Navigator.of(context).push(
  CupertinoPageRoute(
    builder: (context) { return Page2(); },
  ),
);

そこで、画面遷移のアニメーション等をカスタマイズしたい場合は PageRouteBuilder を使うことで対応できます。
基本的な使い方は簡単で、表示する画面のWidgetと遷移時のアニメーションを指定すればOKです。

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      // 表示する画面のWidget
      return Page2();
    },
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // 遷移時のアニメーションを指定
      final Offset begin = Offset(0.0, 1.0);
      final Offset end = Offset.zero;
      final Tween<Offset> tween = Tween(begin: begin, end: end);
      final Animation<Offset> offsetAnimation = animation.drive(tween);
      return SlideTransition(
        position: offsetAnimation,
        child: child,
      );
    },
  ),
);

アニメーションの基本的な使い方は「アニメーション基礎」で紹介しているます。
基礎知識を身に着けたい場合はこちらをご覧ください。

スライドイン(左右)

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return Page2();
    },
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      final Offset begin = Offset(1.0, 0.0); // 右から左
      // final Offset begin = Offset(-1.0, 0.0); // 左から右
      final Offset end = Offset.zero;
      final Animatable<Offset> tween = Tween(begin: begin, end: end)
          .chain(CurveTween(curve: Curves.easeInOut));
      final Animation<Offset> offsetAnimation = animation.drive(tween);
      return SlideTransition(
        position: offsetAnimation,
        child: child,
      );
    },
  ),
);

スライドイン(上下)

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return Page2();
    },
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      final Offset begin = Offset(0.0, 1.0); // 下から上
      // final Offset begin = Offset(0.0, -1.0); // 上から下
      final Offset end = Offset.zero;
      final Animatable<Offset> tween = Tween(begin: begin, end: end)
          .chain(CurveTween(curve: Curves.easeInOut));
      final Animation<Offset> offsetAnimation = animation.drive(tween);
      return SlideTransition(
        position: offsetAnimation,
        child: child,
      );
    },
  ),
);

フェードイン

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return Page2();
    },
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      final double begin = 0.0;
      final double end = 1.0;
      final Animatable<double> tween = Tween(begin: begin, end: end)
          .chain(CurveTween(curve: Curves.easeInOut));
      final Animation<double> doubleAnimation = animation.drive(tween);
      return FadeTransition(
        opacity: doubleAnimation,
        child: child,
      );
    },
  ),
);

ブラックアウト・ホワイトアウト

ブラックアウト・ホワイトアウトの様に連続したアニメーションの場合は、Intervalで指定した区間のみアニメーションを適用することで再現できます。

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return Page2();
    },
    transitionDuration: Duration(seconds: 3),
    reverseTransitionDuration: Duration(seconds: 3),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      final color = ColorTween(
        begin: Colors.transparent,
        end: Colors.black, // ブラックアウト
        // end: Colors.white, // ホワイトアウト
      ).animate(CurvedAnimation(
        parent: animation,
        // 前半
        curve: Interval(
          0.0,
          0.5,
          curve: Curves.easeInOut,
        ),
      ));
      final opacity = Tween<double>(
        begin: 0.0,
        end: 1.0,
      ).animate(CurvedAnimation(
        parent: animation,
        // 後半
        curve: Interval(
          0.5,
          1.0,
          curve: Curves.easeInOut,
        ),
      ));
      return AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return Container(
            color: color.value,
            child: Opacity(
              opacity: opacity.value,
              child: child,
            ),
          );
        },
        child: child,
      );
    },
  ),
);

ワイプ

ClipPathを使うことで任意の形でWidgetをくり抜くことができます。
これを使いWidgetを四角形でくり抜きワイプを表現できます。

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return Page2();
    },
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      final opacity = Tween<double>(
        begin: 0.0,
        end: 1.0,
      ).animate(CurvedAnimation(
        parent: animation,
        curve: Curves.easeInOut,
      ));
      return AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return ClipPath(
            clipper: WipeClippter(opacity.value),
            child: child,
          );
        },
        child: child,
      );
    },
  ),
);

class WipeClippter extends CustomClipper<Path> {
  const WipeClippter(this.progress) : super();

  final double progress;

  
  Path getClip(Size size) {
    return Path()
      ..addRect(Rect.fromCenter(
        center: Offset(size.width / 2, size.height / 2),
        width: size.width * progress,
        height: size.height,
      ))
      ..close();
  }

  
  bool shouldReclip(covariant CustomClipper oldClipper) {
    return true;
  }
}