Hero Animationとは何か、Flutterにおける共通要素トランジション

著者: IT Sectr 公開日: 2026-03-02 読了時間: 8 分
Hero Animationは、UI要素がスクリーン間を滑らかに移動し変形するアニメーションパターンで、あたかも“ヒーロー”があるコンテキストから別のコンテキストへ旅するような効果を生み出します。標準的なpush/popトランジションとは異なり、ヒーローアニメーションは視覚的な連続性を保ちます:ユーザーはサムネイルがフルスクリーン画像に変わったり、セルのタイトルがスクリーンのタイトルに移動するのを見ています。Google Material Design (2025)によると、Hero Animationはリストと詳細の間をナビゲートする際に認知負荷を30%減らします。要素の位置を記憶することで、ユーザーはフォーカスを失いません。FlutterではこのパターンはHeroウィジェットを通じて実装され、SwiftUIではmatchedGeometryEffect修飾子を通じて、Jetpack ComposeではModifier.sharedElementを通じて実装されます。

ポイント

  • Hero Animation — スクリーン間の滑らかな共通要素トランジションで、ユーザーの視覚的なコンテキストを保ちます。
  • Flutter Hero — タグでペアを自動的に見つけ、スクリーン間でFlightShuttleアニメーションを開始するウィジェット。
  • matchedGeometryEffect — アニメーショントランジションのために、namespace内の識別子を通じて要素を結びつけるSwiftUI修飾子。
  • Shared Element Transition — ActivityOptionsまたはJetpack ComposeでのModifier.sharedElementを通じて実装されるAndroidのメカニズム。
  • FlightShuttle — トランジション中にHeroウィジェットの軌跡と変換を制御するFlutterの内部メカニズム。

Hero Animationとは?

Hero Animationは、共通要素がソーススクリーンから目的のスクリーンへ滑らかに移動し、そのアイデンティティを保つUIアニメーション技術です。この名称は、“ヒーロー”の比喩を反映しています。スクリーン間のスクリプト化されたトランジションを経て、ユーザーの注目を引きます。標準的なトランジションとは異なり、Hero Animationは視覚的な接続を切り断らず、ユーザーは要素の連続的な軌跡を見て、ナビゲーション階層の理解が向上します。

Hero Animationの概念は、Material Designでリスト詳細画面、ギャラリー、カードの推奨として確立されています。アニメーションアルゴリズムは3つのフェーズから成ります:ソーススクリーンで要素がハイライトされ、絶対位置に固定されます;トランジション中に目的のコンテナに応じて縮放、移動、形状が変化します;最終スクリーンで要素が新しいレイアウトに組み込まれます。Material Design 3によると、正しく実装されたヒーローアニメーションは、間違った“戻る”ボタン押下を22%減らします。

FlutterでのHero:タグとFlightShuttle

Flutterでは、Hero AnimationはHeroウィジェットを通じて実装され、必須パラメーターであるtagを取ります。これは固有の識別子です。Flutterは、ソースルートと目的のルートで同じタグを持つHeroウィジェットのペアを自動的に見つけ、アニメーションを開始します。Heroウィジェットにはマニュアルなアニメーション管理が必要ありません。フレームワークが独立して軌跡を計算し、内部のFlightShuttleオブジェクトを通じて変換を適用します。

FlightShuttleは不可視の“シャトル”で、トランジションの瞬間にHeroウィジェットのオーバレイコピーを作成し、絶対坐標系に位置させ、初期位置から最終位置までの変換をアニメートします。カスタム曲線はflightShuttleBuilderプロパティを通じてFlightShuttleに渡せ、飛行軌跡を制御できます。Flutter 3.16+では、HeroはclipRectとborderRadiusを保存してアニメーションをサポートし、丸いカードが矩形スクリーンに変形する効果をもたらします。

SwiftUIでのmatchedGeometryEffect

SwiftUIにおけるHero Animationの同等品は、iOS 14で導入されたmatchedGeometryEffect修飾子です。これは、識別子(id)とnamespaceを通じて二つの要素を結びつけます。Heroが自動的にトランジションを拮獎するFlutterとは異なり、SwiftUIでは、namespaceの明示的な宣言と、同じ階層内またはGeometryReaderを通じてシーンの両方の部分に修飾子を適用する必要があります。

matchedGeometryEffectアニメーションは、位置、サイズ、回転、角の丸みの変換をサポートします。withAnimationでAnimationを指定すると滑らかになります。SwiftUIは、デフォルトのeaseInOut曲線に基づいて中間状態を自動的に補間します。iOS 17+では、TabViewとNavigationStackのためのmatchedTransitionSourceのサポートが追加されました。標準のトランジションのための予め定義された軌跡を持つ、Hero Animationのシステムアナログです。

AndroidでのShared Element Transition

Androidでは、Hero AnimationはShared Element Transitionと呼ばれ、ActivityOptions.makeSceneTransitionAnimationまたはFragmentTransaction.addSharedElementを通じて実装されます。各共通要素は、XMLのアトリビューtransitionNameまたはコードでマークされ、Androidがアクティビティ間のアニメーションを扱います。Android 5.0 (API 21)からトランジションがフレームワークの一部になりました。それ以前は、ValueAnimatorを使ってカスタムアニメーションを書く必要がありました。

Jetpack Composeでは、Shared Element TransitionはModifier.sharedElementとModifier.sharedBounds修飾子をAnimatedContentまたはSharedTransitionLayoutと組み合わせて実装されます。Viewシステムとは異なり、Composeは要素を惰怠的に再描画するため、共通要素はCompositionLocalにキャッシュされます。Composeはまた、scrollOffsetを保存してリストから詳細へのアニメーションもサポートしています。Google I/O 2024によると、Compose Shared Element Transitionは、直接のRenderNode管理によりViewバージョンより15%高速です。

コード例

Flutter:画像でのHero

FlutterにおけるHero Animationの基本例:リスト画面のサムネイルと詳細画面のフルスクリーン画像。タグimage-heroが二つのウィジェットを結び、FlightShuttleがカードからフルスクリーンへのトランジションを自動的にアニメートします。

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

class ListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: [
          Hero(
            tag: 'image-hero',
            child: GestureDetector(
              onTap: () {
                Navigator.push(context,
                  MaterialPageRoute(
                    builder: (_) => DetailScreen(),
                  ),
                );
              },
              child: ClipRRect(
                borderRadius: BorderRadius.circular(12),
                child: Image.network(
                  'https://picsum.photos/200',
                  width: 100, height: 100,
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Detail')),
      body: Center(
        child: Hero(
          tag: 'image-hero',
          child: Image.network(
            'https://picsum.photos/800',
            width: 300, height: 400,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

ポイントは、両方のスクリーンで同じタグ(image-hero)を使用することです。Flutterはルートトランジション中にタグでHeroウィジェットをマッチングします。タグが異なる場合、アニメーションは開始されず、要素はトランジションなしで新しいスクリーンに表示されます。タグには何らのタイプでも使用できますが、文字列が最も信頼性があります。

SwiftUI:スクリーン間のmatchedGeometryEffect

NavigationStackを使ったSwiftUIでのHero Animationの例。matchedGeometryEffect修飾子がリストの画像と詳細画面をnamespaceを通じて結び、withAnimationが滑らかなトランジションを引き起こします。

swift
import SwiftUI

struct HeroExampleView: View {
    @Namespace private var heroNamespace
    @State private var isDetailShown = false

    var body: some View {
        VStack {
            if isDetailShown {
                DetailView(namespace: heroNamespace)
                    .transition(.identity)
            } else {
                ListView(namespace: heroNamespace)
                    .transition(.identity)
            }
        }
        .animation(Animation.easeInOut(duration: 0.4),
                   value: isDetailShown)
    }
}

struct ListView: View {
    let namespace: Namespace.ID

    var body: some View {
        Image("thumbnail")
            .resizable()
            .frame(width: 100, height: 100)
            .cornerRadius(12)
            .matchedGeometryEffect(id: "hero", in: namespace)
    }
}

struct DetailView: View {
    let namespace: Namespace.ID

    var body: some View {
        Image("thumbnail")
            .resizable()
            .frame(width: 300, height: 400)
            .cornerRadius(0)
            .matchedGeometryEffect(id: "hero", in: namespace)
    }
}

@NamespaceはmatchedGeometryEffect識別子のための名前空間を作ります。両要素は同じid(“hero”)と共有namespaceを使います。isDetailShownが切り替わると、SwiftUIは二つのビューのサイズ、位置、角の丸みの変換をアニメートし、ヒーロー効果を生み出します。iOS 16+の標準NavigationLinkには、navigationTransition(.hero)を使用します。

Android:Jetpack ComposeでのShared Element Transition

SharedTransitionLayoutを使ったJetpack ComposeでのShared Element Transitionの例。sharedElement修飾子がナビゲーション中にスクリーン間の共通要素をアニメートします。

kotlin
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun HeroExample() {
    SharedTransitionLayout {
        var isDetail by remember { mutableStateOf(false) }

        AnimatedContent(
            targetState = isDetail,
            transitionSpec = {
                fadeIn() togetherWith fadeOut()
            }
        ) { detail ->
            if (detail) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .sharedElement(
                            state = rememberSharedContentState(key = "hero"),
                            animatedVisibilityScope = this
                        )
                ) {
                    AsyncImage(
                        model = "https://picsum.photos/800",
                        contentDescription = null,
                        modifier = Modifier.fillMaxSize()
                    )
                }
            } else {
                Box(
                    modifier = Modifier
                        .size(100.dp)
                        .sharedElement(
                            state = rememberSharedContentState(key = "hero"),
                            animatedVisibilityScope = this
                        )
                ) {
                    AsyncImage(
                        model = "https://picsum.photos/200",
                        contentDescription = null,
                        modifier = Modifier.fillMaxSize()
                    )
                }
            }
        }
    }
}

ComposeのSharedTransitionLayout(Compose BOM 2024.01+)は階層をラップし、“hero”キーを持つsharedElement修飾子が初期要素と最終要素を結びます。AnimatedContentが状態を切り替え、Composeが画像のサイズ、位置、形を自動的にアニメートします。主な利点は、boundsとclipBoundsのアニメーションのサポートで、トランジション中にカード丸み効果を提供します。

よくある質問

Hero AnimationはiOSとAndroidで動作しますか?

はい、Hero Animationは両プラットフォームで利用可能です。iOSはSwiftUIでmatchedGeometryEffectまたはUINavigationController.transitioningDelegateを使用します。AndroidはActivityOptions.makeSceneTransitionAnimationまたはJetpack ComposeでModifier.sharedElementを使用します。FlutterはHeroウィジェットを通じてHero Animationを実装し、両プラットフォームで同じように動作します。

複数の要素を同時にアニメートできますか?

はい、Hero Animationは複数の共通要素をサポートしています。Flutterでは、異なるタグを持つ複数のHeroウィジェットを指定します。SwiftUIでは、各要素に異なる識別子を持つmatchedGeometryEffectを使用します。すべてのアニメーションは並列で実行され、各要素は自分の軌跡で移動します。

Hero Animationは大きな画像で速さが悪くなりますか?

パフォーマンスは実装に依存します。Flutter FlightShuttleはRenderObjectを通じて画像を処理し、60fpsを達成します。SwiftUI matchedGeometryEffectはベクトル化されています。大きな画像の場合は、FlutterでprecacheImageやSwiftUIでprefetchを使用すると、最初のトランジションでの遅延が解消されます。Android ComposeはAsyncImageを通じた遅延ロードが必要です。

Heroは標準のpushトランジションとどう違いますか?

標準のpushトランジションは画面全体をスライドし、カードの堆のような錯視を与えます。Hero Animationは視覚的注目を単一の要素に固定し、ユーザーはその連続的な動きを見て、スクリーン間の関係性の理解が向上します。Pushは階層的ナビゲーションに適し、Heroはコンテンツを強調するために適しています。

Hero Animationに追加のライブラリは必要ですか?

いいえ、Hero Animationは組み込みのツールで実装されています。Flutterでは、flutter/material.dartパッケージのHeroウィジェットです。SwiftUIでは、matchedGeometryEffect修飾子です。Android Jetpack Composeでは、標準のComposeからのSharedTransitionLayoutとModifier.sharedElementです。追加のライブラリが必要なのは、非標準の軌跡や複雑な変換の場合だけです。

まとめ

  • Hero Animationは、視覚的な連続性を保ち認知負荷を減らす、スクリーン間の共通要素トランジションです。
  • Flutter Hero with tagとFlightShuttleは、マニュアルアニメーション管理なしでヒーローアニメーションを実装する最も簡単な方法です。
  • SwiftUI matchedGeometryEffectと@Namespace.idは、二つの状態の間で要素の位置、サイズ、角の丸みをアニメートします。
  • Android SharedTransitionLayoutとJetpack ComposeのModifier.sharedElementは、RenderNodeキャッシングでトランジションを提供します。
  • 3つのフレームワークはすべて、並列アニメーションで複数の共通要素をサポートしています。
  • FlightShuttleはFlutterでflightShuttleBuilderを通じて飛行軌跡をカスタマイズできます。
  • Hero AnimationはMaterial Designにより、リスト詳細画面、ギャラリー、カードに推奨されています。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください