【Flutter】Heroによる画面遷移時のアニメーション

この記事は約12分で読めます。

Heroの基本的な使い方

HeroはNavigatorのpush、pop時に対応するWidgetをアニメーションでユーザに示すことのできるWidgetです。tagに設定した値で対応関係を識別するので、中身のWidgetが一致している必要はありません。Heroを使った簡単な例を示します。

このコードは下記のようになります。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: MainView());
  }
}

class MainView extends StatelessWidget {
  final heroKey = GlobalKey();
  MainView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: GridView.count(
      crossAxisCount: 3,
      children: [
        Colors.red,
        Colors.yellow,
        Colors.green,
        Colors.cyan,
        Colors.blue,
        Colors.purple
      ].map((color) {
        return Hero(
            tag: color,
            child: GestureDetector(
                onTap: () {
                  Navigator.push(context, MaterialPageRoute(builder: (context) {
                    return SubView(color);
                  }));
                },
                child: Container(color: color, child: const Icon(Icons.home))));
      }).toList(),
    ));
  }
}

class SubView extends StatelessWidget {
  final Color color;

  const SubView(this.color, {super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: SizedBox(
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            child: Hero(
                tag: color,
                child: GestureDetector(
                    onTap: () {
                      Navigator.pop(context);
                    },
                    child: Container(
                        color: color, child: const Icon(Icons.home))))));
  }
}

各HeroのtagColorのオブジェクトを割り当てています。Navigator.pushによる遷移後のHeroにもタップまたはクリック(以下、タップに統一)したHeroのtagを割り当てているので、遷移元と遷移先の対応関係が取れており、アニメーションしてくれています。

遷移中アニメーションの制御

前述の例では同じ色のままアニメーションをさせましたが、もし遷移先の色をグレーにしてみたらどうなるでしょうか?65行目のcolor: colorcolor: Colors.greyに変更してみます。

大して気にならないかもしれませんが、よく見ると急激に色が変わるようになってしまいました。アニメーション中に色を滑らかに変化させるためには遷移中の色の変化を自分で書く必要があります。ここでは、35行目と36行目の間に下記のようなコードを入れてみます。

            flightShuttleBuilder: (flightContext, animation, flightDirection,
                fromHeroContext, toHeroContext) {
              return AnimatedBuilder(
                  animation: animation,
                  builder: (context, widget) {
                    return Container(
                        color: Color.lerp(color, Colors.grey, animation.value),
                        child: const Icon(Icons.home));
                  });
            },

すると、このようにアニメーション中の色の遷移が滑らかになります。(少し分かりにくいかもしれませんが・・・)

flightShuttleBuilder内の処理を工夫すればさらに凝ったアニメーションを行うことも出来ます。ここでは遷移中に1回転させるようにコードを変更してみます。

            flightShuttleBuilder: (flightContext, animation, flightDirection,
                fromHeroContext, toHeroContext) {
              return AnimatedBuilder(
                  animation: animation,
                  builder: (context, widget) {
                    final size = MediaQuery.of(context).size;

                    final c = Offset.lerp(
                        Offset(size.width / 6.0, size.width / 6.0),
                        size.center(Offset.zero),
                        animation.value)!;
                    final transform = Matrix4.translationValues(c.dx, c.dy, 0.0)
                      ..rotateZ(animation.value * 2.0 * pi)
                      ..translate(-c.dx, -c.dy, 0.0);

                    return Transform(
                        transform: transform,
                        child: Container(
                            color:
                                Color.lerp(color, Colors.grey, animation.value),
                            child: const Icon(Icons.home)));
                  });
            },

実行すると下記のようになります。

遷移中のウィジェットのサイズも徐々に変化していくようなので、回転中心もそれに合わせて変化させるようなコードを書いています。

Heroを複数使う

Heroはtagが重複しなければ複数同時に使うことができます。6個同時にアニメーションさせてみたコード例です。

import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: MainView());
  }
}

class MainView extends StatelessWidget {
  final heroKey = GlobalKey();
  MainView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: GridView.count(
      crossAxisCount: 3,
      children: [
        Colors.red,
        Colors.yellow,
        Colors.green,
        Colors.cyan,
        Colors.blue,
        Colors.purple
      ].map((color) {
        return Hero(
            key: key,
            tag: color,
            flightShuttleBuilder: (flightContext, animation, flightDirection,
                fromHeroContext, toHeroContext) {
              return AnimatedBuilder(
                  animation: animation,
                  builder: (context, widget) {
                    final size = MediaQuery.of(context).size;

                    final c = Offset(size.width / 6.0, size.width / 6.0);
                    final transform = Matrix4.translationValues(c.dx, c.dy, 0.0)
                      ..rotateZ(animation.value * 2.0 * pi)
                      ..translate(-c.dx, -c.dy, 0.0);

                    return Transform(
                        transform: transform,
                        child: Container(
                            color: color, child: const Icon(Icons.home)));
                  });
            },
            child: GestureDetector(
                onTap: () {
                  Navigator.push(context, MaterialPageRoute(builder: (context) {
                    return SubView(color);
                  }));
                },
                child: Container(color: color, child: const Icon(Icons.home))));
      }).toList(),
    ));
  }
}

class SubView extends StatelessWidget {
  final Color color;

  const SubView(this.color, {super.key});

  @override
  Widget build(BuildContext context) {
    final list = <Color>[
      Colors.red,
      Colors.yellow,
      Colors.green,
      Colors.cyan,
      Colors.blue,
      Colors.purple
    ];
    list.shuffle();

    return Scaffold(
      body: GridView.count(
          crossAxisCount: 3,
          children: list.map((color) {
            return Hero(
                key: key,
                tag: color,
                child: GestureDetector(
                    onTap: () {
                      Navigator.pop(context);
                    },
                    child: Container(
                        color: color, child: const Icon(Icons.home))));
          }).toList()),
    );
  }
}

実行すると、このように6個のHeroがそれぞれ遷移してくれます。

同じtagが複数存在するとエラーになってしまうので、tagは重複しないようにする必要があります。

まとめ

本記事では、Heroの基本的な使い方と遷移時の挙動を変える方法について確認しました。また、複数のHeroを同時にアニメーションさせることができるのを確認しました。

コメント

タイトルとURLをコピーしました