Shared Element Transition とは、ナビゲーション中に共有要素(画像、テキスト、カード)が2つの画面間をスムーズに移動し変形するアニメーションです。突然の切り替わりではなく、ユーザーは連続的な動きを目にします。画面Aの要素が画面Bへと流れ込み、コンテキストと視覚的なつながりを維持します。Material Design Guidelines(2026)によると、Shared Element Transitionは知覚パフォーマンスを35%向上させ、ナビゲーションの理解度を28%改善します。他の種類のアニメーションの詳細は、アニメーションの総合ガイドをご覧ください。
重要なポイント
Shared Element Transition は、1つ以上の視覚要素が前の画面から次の画面に移動する画面遷移アニメーションです。ナビゲーションの瞬間に、システムは元の画面上の要素のスナップショットを撮り、新しい画面上のターゲット状態への変換(位置、サイズ、形状、色)をアニメーション化します。その結果、ユーザーは2つの画面を別々のページではなく、単一の空間の一部として認識します。
主なユースケース:商品カード → 詳細画面(画像が拡大して上に移動)、画像ギャラリー → ビューア(画像がスムーズに全画面に拡大)、連絡先リスト → プロフィール(アバターと名前が移動)。正しく動作させるには、両方の画面に同じtransitionName(Android)またはスナップショット識別子(iOS)を持つViewが必要です。
Google I/O 2025によると、Shared Element Transitionはモバイルアプリケーションで最も効果的なUXパターンのトップ3に入っています。共有要素のアニメーションを見たユーザーは、遷移直後に戻る可能性が22%低くなります。ただし、過剰な使用(1回の遷移で3つ以上の共有要素)は効果を低下させ、ミッドレンジデバイスでFPSが40を下回る可能性があります。
Androidでは、Shared Element TransitionはFragmentTransaction.addSharedElement(view, transitionName)またはActivityOptions.makeSceneTransitionAnimationで実装します。各Viewには一意のandroid:transitionNameが割り当てられます(両方の画面で同じ)。システムは自動的に移動アニメーション(ChangeBounds)、サイズ変更アニメーション(ChangeTransform)、画像スケーリングアニメーション(ChangeImageTransform)を作成します。
<androidx.cardview.widget.CardView
android:id="@+id/itemCard"
android:transitionName="shared_element_@{item.id}"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/itemImage"
android:transitionName="shared_image_@{item.id}" />
</androidx.cardview.widget.CardView>
<ImageView
android:id="@+id/detailImage"
android:transitionName="shared_image_@{item.id}"
android:layout_width="match_parent"
android:layout_height="300dp" />
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.fragment.app.FragmentTransaction
// Activity: ActivityOptionsによるShared Element
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
Pair(imageView, getString(R.string.transition_name_image)),
Pair(titleView, getString(R.string.transition_name_title))
)
startActivity(intent, options.toBundle())
// Fragment: FragmentTransactionによるShared Element
val detailFragment = DetailFragment().apply {
sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
sharedElementReturnTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
}
supportFragmentManager.beginTransaction()
.addSharedElement(imageView, imageView.transitionName)
.addSharedElement(titleView, titleView.transitionName)
.replace(R.id.container, detailFragment)
.addToBackStack(null)
.commit()
// トランジションのカスタマイズ: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Androidの重要な詳細:異なるscaleType(centerCrop → fitCenter)のImageViewを正しくアニメーション化するには、ChangeImageTransformを使用します。これは表示モードの変更を自動的にアニメーション化します。角丸のViews(CardView)には、ChangeBoundsを追加します。これはアウトラインを介して角の半径をアニメーション化します。テキストアニメーション(フォントサイズ変更)には、ChangeTextTransformを使用します。共有要素がいずれかの画面で非表示または表示になる場合、システムは自動的にFadeを適用します。
iOSにはAndroidのsharedElementEnterTransitionに相当する組み込み機能はありません。共有アニメーションはUIViewControllerAnimatedTransitioningを介して、UIViewPropertyAnimatorまたはCASpringAnimationを使用して手動で実装します。主な手法:animateTransition(using:)で、元の画面のViewを取得し、スナップショットを撮り、containerViewに追加し、オリジナルを非表示にして、スナップショットをターゲット位置にアニメーション化します。最後に、ターゲットViewを表示してスナップショットを削除します。
import UIKit
"> カスタムShared Elementアニメーター
class SharedElementAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let fromView: UIView
let toView: UIView
let fromFrame: CGRect
let toFrame: CGRect
let isPresenting: Bool
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
guard let toVC = ctx.viewController(forKey: .to),
let fromVC = ctx.viewController(forKey: .from)
else { return }
let container = ctx.containerView
if isPresenting {
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
toVC.view.alpha = 0
}
// 共有要素のスナップショット
let snapshot = fromView.snapshotView(afterScreenUpdates: false)!
snapshot.frame = isPresenting ? fromFrame : toFrame
snapshot.layer.cornerRadius = isPresenting ? 12 : 0
container.addSubview(snapshot)
fromView.isHidden = true
toView.isHidden = true
UIView.animate(withDuration: transitionDuration(using: ctx),
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0) {
snapshot.frame = self.isPresenting ? self.toFrame : self.fromFrame
snapshot.layer.cornerRadius = self.isPresenting ? 0 : 12
toVC.view.alpha = 1
} completion: { _ in
fromView.isHidden = false
toView.isHidden = false
snapshot.removeFromSuperview()
ctx.completeTransition(!ctx.transitionWasCancelled)
}
}
}
// Hero — 共有トランジションを簡素化するライブラリ
// heroIDは両方の画面で一致し、AndroidのtransitionNameと同様
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimatorはiOS 10+での共有アニメーションに推奨される方法です。UIView.animateとは異なり、PropertyAnimatorはインタラクティブ制御(fractionComplete)をサポートしており、スワイプバックジェスチャーと共有トランジションを統合できます。Heroライブラリ(サードパーティ)では、共有トランジションはhero.idで設定します。これはAndroidのtransitionNameに相当し、画面間の位置、サイズ、cornerRadius、zPosition、backgroundColorを自動的にアニメーション化します。
Jetpack Composeでは、Shared Element TransitionはgraphicsLayer修飾子とSharedTransitionScopeアニメーション(Compose 1.7+)で実装します。各共有要素は一意のキーを持つsharedElement()でマークされます。システムはcomposable関数間のナビゲーション時に、位置、サイズ、形状、色、クリップマスクの変更を自動的にアニメーション化します。Android Developers Blog(2026)によると、Compose Shared Element TransitionはAnimatedContent、NavHost、およびカスタムコンテナをサポートしています。
import androidx.compose.animation.core.spring
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
// SharedTransitionLayout — 共有要素のコンテナ
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// 画面A: リスト
LazyColumn {
items(items) { item ->
Card(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${item.id}"),
boundsTransform = { _, _ ->
spring(dampingRatio = Spring.DampingRatioMediumBouncy)
}
)
) {
AsyncImage(model = item.imageUrl, contentDescription = null)
}
}
}
// 画面B: 詳細(要素が選択されると表示)
AnimatedContent(
targetState = selectedItem,
transitionSpec = {
EnterTransition.None using SizeTransform(clip = false)
}
) { item ->
item?.let {
Image(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${it.id}")
),
painter = rememberAsyncImagePainter(it.imageUrl),
contentDescription = null,
contentScale = ContentScale.FillWidth
)
}
}
}
Compose 1.7+ SharedTransitionScopeは、実験的モードで利用可能な新しいAPIです。NavHostでの手動の共有アニメーション管理を置き換えます。主な機能:カスタムアニメーションカーブのboundsTransform、角丸のclipToBounds、重なり順のzIndex。異なるcontentScaleの画像をアニメーション化するには、SharedContentKeyラッパーとともにModifier.sharedElementを使用します。古いバージョンのCompose(< 1.7)では、GoogleのSharedTransitionScopeライブラリを使用します。
Shared Element Transitionは、プラットフォームの制限を考慮しないとパフォーマンスの問題を引き起こす可能性があります。Androidでの主な問題は、大きな画像(2048 px以上)でのChangeImageTransformです。iOSでは、重いレイヤー(CAGradientLayer、多数のノードを持つCAShapeLayer)でのスナップショットです。解決策:共有アニメーションには圧縮画像を使用し、afterScreenUpdates: falseでスナップショットを撮ります。
| 問題 | 原因 | 解決策 |
|---|---|---|
| アニメーションのカクつき | 遷移前後でレイアウトが異なる / 重いスナップショット | Placeholderを使用:読み込み中に小さなプレビューを表示 |
| 共有要素がアニメーションしない | 両方の画面でtransitionNameが一致しない | XML/コードでtransitionNameが正確に一致するか確認 |
| AndroidでFPS < 40 | 共有要素に複雑なView階層が含まれている | CardView全体ではなくImageViewのみを共有 |
| iOS:スナップショットのアーティファクト | マスク/アニメーションのあるレイヤーでrenderInContext | drawHierarchy(in:afterScreenUpdates: true)を使用 |
| Compose:要素が表示されない | SharedTransitionLayoutが両方の画面をラップしていない | SharedTransitionLayoutが両方の親である必要がある |
// 最適化: 共有トランジションにおける画像のPlaceholder
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> ナビゲーション前にプレビューを読み込む
Glide.with(context)
.load(imageUrl)
.override(300, 300) // 共有トランジションのサイズ制限
.placeholder(R.drawable.placeholder)
.into(imageView)
> 遷移後のfull-resolutionの遅延読み込み
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: drawHierarchyによるスナップショット最適化
// snapshotView(afterScreenUpdates: false) — より高速、保留中の更新を無視
// 複雑なViewの場合: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
ベストプラクティス:1回の遷移で3つ以上の共有要素を追加しないでください。追加の要素ごとにレンダリング負荷が増加します。リスト(RecyclerView/UICollectionView)の場合、リスト全体ではなくクリックされたアイテムに共有要素を作成します。角丸のカードには、AndroidではclipToOutline、iOSではスナップショットのcornerRadiusを使用します。2つ目の画面の共有要素がScrollView内にある場合は、アニメーションを開始する前にレイアウトの完了を待ちます(view.doOnPreDraw)。
よくある質問
1回の遷移では3つの共有要素以下にすることをお勧めします。追加の要素ごとにレンダリング負荷が増加し、FPSが低下する可能性があります。最適:1画像+1テキスト(タイトル)。Androidでは、各要素はaddSharedElement(view, transitionName)で追加します。iOSでは、各スナップショットはcontainerViewに個別に追加します。
transitionNameがonBindViewHolderで動的に設定されていることを確認してください:holder.imageView.transitionName = "image_${position}"。FragmentTransaction.addSharedElementの場合は、holderから特定のViewを渡します。ActivityOptionsの場合は、Pair.create(holder.imageView, holder.imageView.transitionName)を使用します。postponeEnterTransition()とstartPostponedEnterTransition()でレイアウトが完了するまで共有トランジションを遅延させます。
Androidの場合:TransitionSetにChangeBackgroundを追加するか、captureStartValuesとcaptureEndValuesをオーバーライドするカスタムTransitionを使用します。iOSの場合:スナップショットのbackgroundColorをアニメーション化するか、色変更とともにUIViewPropertyAnimatorを使用します。Jetpack Composeの場合:Modifier.sharedElementはColorトランジションを介して背景を自動的にアニメーション化します。カードの場合は、 elevation(リストでは2dp、詳細画面では8dp)もアニメーション化します。
はい、Navigation Componentでは、Shared ElementはNavOptionsでサポートされています:val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment)。共有要素を渡すにはFragmentNavigatorExtrasを使用します:FragmentNavigatorExtras(view to transitionName)。Navigation Composeの場合は、SharedTransitionScope(Compose 1.7+)のModifier.sharedElementを使用します。
Androidでは、Transition Debuggingを有効にします:adb shell setprop debug.transition 1 — logcatに共有要素のキャプチャとアニメーションに関するメッセージが表示されます。Developer Optionsで「Show layout bounds」を有効にして、前後の境界を確認します。iOSでは、Debug View Hierarchyを使用してcontainerView内のスナップショットを確認します。FPSについては、Profile GPU Rendering(Android)またはCore Animationインストルメント(iOS)を使用します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。