ポイント
Hero Animationは、共通要素がソーススクリーンから目的のスクリーンへ滑らかに移動し、そのアイデンティティを保つUIアニメーション技術です。この名称は、“ヒーロー”の比喩を反映しています。スクリーン間のスクリプト化されたトランジションを経て、ユーザーの注目を引きます。標準的なトランジションとは異なり、Hero Animationは視覚的な接続を切り断らず、ユーザーは要素の連続的な軌跡を見て、ナビゲーション階層の理解が向上します。
Hero Animationの概念は、Material Designでリスト詳細画面、ギャラリー、カードの推奨として確立されています。アニメーションアルゴリズムは3つのフェーズから成ります:ソーススクリーンで要素がハイライトされ、絶対位置に固定されます;トランジション中に目的のコンテナに応じて縮放、移動、形状が変化します;最終スクリーンで要素が新しいレイアウトに組み込まれます。Material Design 3によると、正しく実装されたヒーローアニメーションは、間違った“戻る”ボタン押下を22%減らします。
Flutterでは、Hero AnimationはHeroウィジェットを通じて実装され、必須パラメーターであるtagを取ります。これは固有の識別子です。Flutterは、ソースルートと目的のルートで同じタグを持つHeroウィジェットのペアを自動的に見つけ、アニメーションを開始します。Heroウィジェットにはマニュアルなアニメーション管理が必要ありません。フレームワークが独立して軌跡を計算し、内部のFlightShuttleオブジェクトを通じて変換を適用します。
FlightShuttleは不可視の“シャトル”で、トランジションの瞬間にHeroウィジェットのオーバレイコピーを作成し、絶対坐標系に位置させ、初期位置から最終位置までの変換をアニメートします。カスタム曲線はflightShuttleBuilderプロパティを通じてFlightShuttleに渡せ、飛行軌跡を制御できます。Flutter 3.16+では、HeroはclipRectとborderRadiusを保存してアニメーションをサポートし、丸いカードが矩形スクリーンに変形する効果をもたらします。
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では、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 Animationの基本例:リスト画面のサムネイルと詳細画面のフルスクリーン画像。タグimage-heroが二つのウィジェットを結び、FlightShuttleがカードからフルスクリーンへのトランジションを自動的にアニメートします。
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ウィジェットをマッチングします。タグが異なる場合、アニメーションは開始されず、要素はトランジションなしで新しいスクリーンに表示されます。タグには何らのタイプでも使用できますが、文字列が最も信頼性があります。
NavigationStackを使ったSwiftUIでのHero Animationの例。matchedGeometryEffect修飾子がリストの画像と詳細画面をnamespaceを通じて結び、withAnimationが滑らかなトランジションを引き起こします。
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)を使用します。
SharedTransitionLayoutを使ったJetpack ComposeでのShared Element Transitionの例。sharedElement修飾子がナビゲーション中にスクリーン間の共通要素をアニメートします。
@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はSwiftUIでmatchedGeometryEffectまたはUINavigationController.transitioningDelegateを使用します。AndroidはActivityOptions.makeSceneTransitionAnimationまたはJetpack ComposeでModifier.sharedElementを使用します。FlutterはHeroウィジェットを通じてHero Animationを実装し、両プラットフォームで同じように動作します。
はい、Hero Animationは複数の共通要素をサポートしています。Flutterでは、異なるタグを持つ複数のHeroウィジェットを指定します。SwiftUIでは、各要素に異なる識別子を持つmatchedGeometryEffectを使用します。すべてのアニメーションは並列で実行され、各要素は自分の軌跡で移動します。
パフォーマンスは実装に依存します。Flutter FlightShuttleはRenderObjectを通じて画像を処理し、60fpsを達成します。SwiftUI matchedGeometryEffectはベクトル化されています。大きな画像の場合は、FlutterでprecacheImageやSwiftUIでprefetchを使用すると、最初のトランジションでの遅延が解消されます。Android ComposeはAsyncImageを通じた遅延ロードが必要です。
標準のpushトランジションは画面全体をスライドし、カードの堆のような錯視を与えます。Hero Animationは視覚的注目を単一の要素に固定し、ユーザーはその連続的な動きを見て、スクリーン間の関係性の理解が向上します。Pushは階層的ナビゲーションに適し、Heroはコンテンツを強調するために適しています。
いいえ、Hero Animationは組み込みのツールで実装されています。Flutterでは、flutter/material.dartパッケージのHeroウィジェットです。SwiftUIでは、matchedGeometryEffect修飾子です。Android Jetpack Composeでは、標準のComposeからのSharedTransitionLayoutとModifier.sharedElementです。追加のライブラリが必要なのは、非標準の軌跡や複雑な変換の場合だけです。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。