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

対象者

  • Flutterを使ったアプリ開発に携わっているが、Heroウィジェットについての知識が不十分な人
  • UI/UXの向上を目指し、画面遷移のアニメーションを実装したいアプリ開発者
  • 自身のアプリ開発スキルを磨いて、より多くのプロジェクトを獲得したいフリーランス開発者

はじめに

Flutterを使ってアプリ開発を行っているあなた、画面遷移のアニメーションに悩んでいませんか?UI/UXを向上させるために、スムーズで美しい画面遷移が欠かせません。そんなあなたの悩みを解決するのが、FlutterのHeroウィジェットです。しかし、Heroウィジェットの使い方や実装方法が十分に理解できていないと、効果的なアニメーションを実現することは難しいでしょう。

この記事では、Heroウィジェットの目的や役割、実装方法から具体的な例、そして実装時の注意点について解説します。初心者にもわかりやすく、あなたがアプリ開発のスキルを磨くためのサポートをします。フリーランス開発者の方にもおすすめです。より多くのプロジェクトを獲得するためのスキルアップにお役立てください。

この記事を読み終えたあなたは、Heroウィジェットを使いこなし、アプリのUI/UXを大幅に向上させることができるでしょう。アプリ開発において、画面遷移のアニメーションがいかに重要であるかを理解し、効果的なアニメーションを実現する方法を学んでいきましょう。今すぐ始めて、あなたのアプリ開発を次のレベルに引き上げましょう!

Flutter Heroウィジェットの概要

FlutterのHeroウィジェットは、アプリケーションでシームレスなアニメーションを実現するための仕組みです。具体的には、画面遷移時に共通の視覚要素をアニメーションすることで、ユーザーがアプリを使いやすく感じる効果があります。このようなアニメーションは、Androidでは「shared element transitions」、iOSでは「matchedGeometryEffect」と呼ばれています。

Heroウィジェットの目的と役割

Heroウィジェットの主な目的は、画面遷移時に共通の視覚要素をアニメーションさせることで、ユーザーがアプリの操作を直感的に理解できるようにすることです。例えば、一覧画面から詳細画面へ遷移する際に、選択したアイテムがアニメーションしながら拡大されると、ユーザーは遷移先画面が選択したアイテムの詳細情報を表示することが分かりやすくなります。

Heroウィジェットの使用例

// Heroウィジェットの使用例
Hero(
  tag: 'exampleTag',
  child: Image.network('https://placehold.jp/50x50.png'),
)

このように、FlutterのHeroウィジェットは画面遷移時のアニメーションを簡単に実現できる便利な機能です。次のセクションでは、Heroウィジェットの実装方法について詳しく解説していきます。

Heroウィジェットの実装方法

FlutterのHeroウィジェットを利用することで、画面遷移時にアニメーションを追加することができます。実装方法は以下の3つのステップに分けられます。

画面遷移のアニメーション設定

Heroウィジェットを使用するには、まず画面遷移時にアニメーションを設定する必要があります。これには、MaterialPageRouteやCupertinoPageRouteなどのPageRouteを使用します。以下の例では、MaterialPageRouteを使用しています。

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => DetailPage()),
);

タグの指定と使用方法

次に、アニメーションさせたいウィジェットにHeroウィジェットを適用し、遷移元と遷移先で一意のタグを指定します。このタグによって、遷移元と遷移先のウィジェットが関連付けられます。

Hero(
  tag: 'exampleTag',
  child: Image.network('https://placehold.jp/50x50.png'),
)

遷移先画面:

Hero(
  tag: 'exampleTag',
  child: Image.network('https://placehold.jp/200x200.png'),
)

アニメーションのカスタマイズ

Heroアニメーションのカスタマイズも可能です。例えば、flightShuttleBuilderを使って、アニメーション中のウィジェットの見た目を変更できます。

Hero(
  tag: 'exampleTag',
  child: Image.network('https://placehold.jp/50x50.png'),
  flightShuttleBuilder: (
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) {
    return Container(
      color: Colors.red,
      child: Image.network('https://placehold.jp/50x50.png'),
    );
  },
)

このように、FlutterのHeroウィジェットを使って、画面遷移時のアニメーションを簡単に実装できます。タグを指定して遷移元と遷移先のウィジェットを関連付けたり、アニメーションのカスタマイズも可能です。次のセクションでは、具体的なHeroアニメーションの例を見ていきましょう。

Heroアニメーションの例

Heroアニメーションは、画面遷移時に滑らかなアニメーションを実現するために使われます。以下に、具体的な例を2つ紹介します。

一つの画面から別の画面への遷移

この例では、リストのアイテムをタップした際に、詳細画面に遷移する際のアニメーションを実装します。

遷移元画面(リスト画面):

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index].title),
      leading: Hero(
        tag: 'item-${items[index].id}',
        child: Image.network(items[index].imageUrl),
      ),
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => DetailPage(item: items[index]),
          ),
        );
      },
    );
  },
)

遷移先画面(詳細画面):

class DetailPage extends StatelessWidget {
  final Item item;

  DetailPage({required this.item});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(item.title),
      ),
      body: Center(
        child: Hero(
          tag: 'item-${item.id}',
          child: Image.network(item.imageUrl),
        ),
      ),
    );
  }
}

飛行中の形状変化アニメーション

この例では、飛行中のHeroウィジェットの形状が変化するアニメーションを実装します。

遷移元画面:

Hero(
  tag: 'exampleTag',
  child: Container(
    width: 100,
    height: 100,
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(50),
    ),
  ),
  flightShuttleBuilder: (
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) {
    return Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(50),
      ),
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return Container(
            width: 100 + (animation.value * 100),
            height: 100,
            decoration: BoxDecoration(
              color: Colors.blue,
              borderRadius: BorderRadius.circular(50),
            ),
          );
        },
      ),
    );
  },
)

遷移先画面:

Hero(
  tag: 'exampleTag',
  child: Container(
    width: 200,
    height: 100,
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(50),
    ),
  ),
)

これらの例により、Heroアニメーションの実装方法がより具体的に理解できるでしょう。画面遷移や飛行中の形状変化アニメーションなど、さまざまなアニメーションを実装することができます。

実装時の注意点

Heroウィジェットを使う際には、いくつかの注意点があります。ここでは、それらについて説明します。

Heroウィジェットの入れ子使用に関する制約

Heroウィジェットを入れ子にする場合、以下の制約があります。

  1. 同じタグを持つHeroウィジェットが複数ある場合、アニメーションは実行されません。
  2. 入れ子になったHeroウィジェットのタグは、親のタグと同じにすることはできません。

この制約を理解して、適切に実装することが重要です

タグの重複に関する注意

Heroウィジェットには、タグを指定する必要がありますが、同じタグを持つHeroウィジェットが複数あると、アニメーションが正常に動作しません。タグは一意であることが求められますので、適切なタグを選択してください。

例えば、以下のようにリストのアイテムごとに一意のタグを付けることができます。

Hero(
  tag: 'item-${items[index].id}',
  child: Image.network(items[index].imageUrl),
)

heroTagプロパティを使った複数のHeroウィジェットの実装

複数のHeroウィジェットを持つ場合、それぞれに異なるタグを付けることで、それぞれのアニメーションを制御できます。たとえば、以下のようにheroTagプロパティを使ってタグを指定します。

FloatingActionButton(
  heroTag: 'fab1',
  onPressed: () {},
  child: Icon(Icons.add),
),
FloatingActionButton(
  heroTag: 'fab2',
  onPressed: () {},
  child: Icon(Icons.add),
),

これらの注意点を理解し、適切に実装することで、Heroウィジェットを効果的に活用することができま
す。

Q&A

Q1: Heroウィジェットの目的は何ですか?

A1: Heroウィジェットの主な目的は、画面遷移時にスムーズなアニメーションを実現し、ユーザーエクスペリエンスを向上させることです。具体的には、2つの画面間で共通の要素(画像やアイコンなど)にアニメーションを適用し、その要素が画面間で連続して表示されることで、遷移が自然に感じられるようにします。

Q2: タグの重複が起こった場合、どのような問題がありますか?

A2: タグが重複した場合、Heroアニメーションが正常に動作しません。同じタグを持つ複数のHeroウィジェットがあると、フレームワークがどのウィジェットをアニメーションさせるべきか判断できなくなります。そのため、タグは一意であることが求められます。適切なタグを選択し、重複を避けることが重要です。

Q3: Heroアニメーションをカスタマイズする方法は何ですか?

A3: Heroアニメーションのカスタマイズには、createRectTweenプロパティを使って、アニメーション中の矩形の変化を制御できます。このプロパティには、Tweenを生成する関数を指定します。また、flightShuttleBuilderプロパティを使って、飛行中のウィジェットの外観を変更することもできます。これには、HeroFlightShuttleBuilder型の関数を指定します。これらのプロパティを活用することで、Heroアニメーションの見た目や挙動をカスタマイズできます。

まとめ

この記事を通じて、FlutterのHeroウィジェットについて学び、理解を深めました。具体的には、Heroウィジェットの目的と役割、実装方法について詳しく解説されました。また、Heroアニメーションの具体的な例や実装時の注意点も紹介されました。これにより、Flutterアプリケーションで美しい画面遷移アニメーションを実現できるようになりました。

特に重要なポイントは以下の通りです。

  • Heroウィジェット:画面遷移時に要素をスムーズに移動させるアニメーションを実現するためのウィジェット。
  • 実装方法:画面遷移のアニメーション設定、タグの指定と使用方法、アニメーションのカスタマイズが重要。
  • 実装時の注意点:Heroウィジェットの入れ子使用に関する制約、タグの重複に関する注意、およびheroTagプロパティを使った複数のHeroウィジェットの実装。

この知識を活かして、自分のFlutterアプリケーションで効果的な画面遷移アニメーションを実装し、ユーザー体験を向上させましょう。

全ソース

import 'package:flutter/material.dart';

class Item {
  const Item(this.id, this.title);
  final String id;
  final String title;
}

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final item1 = Item('id1', 'title1');
  final item2 = Item('id2', 'title2');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          Hero(
            tag: 'item-${item1.id}',
            child: GestureDetector(
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailPage(item: item1),
                ),
              ),
              child: Image.network('https://placehold.jp/50x50.png'),
            ),
          ),
          Hero(
            tag: 'item-${item2.id}',
            child: GestureDetector(
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailPage(item: item2),
                ),
              ),
              child: Container(
                width: 100,
                height: 100,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(50),
                ),
              ),
            ),
            flightShuttleBuilder: (
              BuildContext flightContext,
              Animation<double> animation,
              HeroFlightDirection flightDirection,
              BuildContext fromHeroContext,
              BuildContext toHeroContext,
            ) {
              return Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(50),
                ),
                child: AnimatedBuilder(
                  animation: animation,
                  builder: (context, child) {
                    return Container(
                      width: 100 + (animation.value * 100),
                      height: 100,
                      decoration: BoxDecoration(
                        color: Colors.blue,
                        borderRadius: BorderRadius.circular(50),
                      ),
                    );
                  },
                ),
              );
            },
          )
        ],
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  final Item item;

  const DetailPage({super.key, required this.item});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(item.title),
      ),
      body: Center(
        child: Hero(
          tag: 'item-${item.id}',
          child: Image.network('https://placehold.jp/200x200.png'),
        ),
      ),
    );
  }
}