この記事は Ian Lake による Android Developers - Medium の記事 " Animations in Navigation Compose" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Jetpack Composeは、「時間があれば調整する」というアニメーションの考え方を「とても簡単なのでやらない手はない」に変えます。その際、大きな役割を果たすのが、画面レベルの遷移です。そのために次の 3 つの具体的な問題を解決する一連のソリューションを目指して Navigation Compose の開発が進められています。
上記のソリューションごとに微妙に異なるアプローチが必要になります。ここでは、その詳細について詳しく説明します。
Jetpack Compose は、最初の 0.1.0-dev01 リリースから最新の Compose 1.0.1 リリースまで、とても長い道のりを歩んできました。ビューの世界と比べて大きく改善された領域の 1 つが、アニメーションと遷移です。完璧なアニメーション API を追求し、Compose が 1.0.0 に進む中、多くの変更が行われました。
信じられないほど強力な animateTo() や animate*AsState() など、たくさんの低レベルのアニメーション API は安定版になりましたが、現時点では、Compose の土台を構成する多くの API が、@ExperimentalAnimationApi とマークされた構成要素に基づいています。
animateTo()
animate*AsState()
@ExperimentalAnimationApi
試験運用版 API(Kotlin の世界では @RequiresOptIn API を使っている API)は、今後変更される可能性がある API であることを表します。つまり、これらの API は、今後のリリースで変更、改善、置換される可能性があります。たとえば、Compose 1.1.0-alpha04 や 1.2.0-alpha08 などで変更されるかもしれません。そのため、試験運用版 API を使うライブラリでは、使っていた Compose のバージョンをアップデートする場合、そのライブラリ も 同時にアップデートしない限り、クラッシュしたり動作しなくなったりします(初期の Compose リリースを使っていた方なら、その苦しみがわかるはずです)。
@RequiresOptIn API
AndroidX リリース ページに記載されているように、Navigation や Compose を含むすべての AndroidX ライブラリは、セマンティック バージョニングに厳密 (英語)に従います。逆に、試験運用版でない API は、ライブラリがリリース候補(RC)フェーズに入ったタイミングで確定します。こういった安定版 API が互換性のない形で変更されるのは、メジャー バージョンが変更されるタイミング(「2.0」など)になります。
これは、上位互換性や下位互換性の観点で見れば素晴らしいことです。たとえば、新しいアルファ版を試すために Fragment のバージョンをアップグレードしつつ、他の依存関係は安定版リリースの状態を維持すれば、すべて問題なく動作します。
しかしながら、試験運用版 API は根底から覆る可能性もあるので、異なるアーティファクト グループにわたって試験運用版 API を使うことは厳しく禁じられています。つまり、androidx.fragment のバージョンをアップグレードしても、androidx.appcompat が動作しなくなることはありません。androidx.navigation や androidx.compose.animation も同様です。
androidx.fragment
androidx.appcompat
androidx.navigation
androidx.compose.animation
Navigation 2.4 は大きなリリースです。Navigation Compose の最初のリリースであるとともに、Navigation Compose と Fragment 対応の Navigation 、両方で複数バックスタックがサポートされた最初のリリースでもあるからです。つまり私たちは、ベータ版、RC 版を経て安定版に向かう準備として、残りの関連 API リクエストに対応する作業を行っています。
Navigation Compose では、Compose 1.0.1 を土台としつつ、Compose 1.1.0-alpha01 以降を利用するために移行したい人(または既に移行した人)のために上位互換性を持たせることが目的となります。
この上位互換性要件が意味するのは、Navigation Compose 2.4.0 のコードが安定版の Compose Animation API しか利用できないということです。私たちはこのようにして、Navigation 2.4.0-alpha05 にクロスフェードのサポートを追加できました。Compose の世界では、ジャンプカットは真っ先に排除すべきです。
安定版の Compose Animation API のみを使えるという制限により、Navigation 2.4 から AnimatedContent (英語)などの API を直接使うことはできないため、皆さんが Navigation 2.4 の一部として直接利用したい高度なアニメーション制御などが提供されない場合があります。ただし、Navigation は拡張可能なので、ベースとなるフレームワークは既に構築されており、それを利用できます。
AnimatedContent
今回リリースされた Navigation 2.4.0-alpha06 で Accompanist Navigation Animation を実現できたのは、このような宛先間のアニメーションがベースとしてサポートされているからです。Navigation Animation アーティファクトは、これまで利用されてきたバージョンの Navigation Compose API で実現できる一連のアニメーションを提供します。
rememberNavController()
rememberAnimatedNavController()
NavHost
AnimatedNavHost
import androidx.navigation.compose.navigation
import com.google.accompanist.navigation.animation.navigation
import androidx.navigation.compose.composable
import com.google.accompanist.navigation.animation.composable
一見、アプリの見た目は変わっていないように見えます。デフォルトのアニメーションは同じ種類の fadeIn と fadeOut のままで、Navigation 2.4 のクロスフェードがこれを行ってくれます。しかし、1 つの重要な新機能が利用できるようになります。それは、アニメーションを構成し、独自の画面遷移に置き換える機能です。
fadeIn
fadeOut
この制御は、すべての Composable の宛先に存在する 4 つの新しいパラメータを通して行います。
enterTransition
navigate()
exitTransition
popEnterTransition
popBackStack()
popExitTransition
パラメータはすべて同じ形式です。
enterTransition: ( ( initial: NavBackStackEntry, target: NavBackStackEntry ) -> EnterTransition?)? = null,
(
initial: NavBackStackEntry,
target: NavBackStackEntry
) -> EnterTransition?
)? = null,
受け取るのはラムダです。このラムダは、移動元(initial)と移動先(target)を表す NavBackStackEntry を受け取ります。たとえば enterTransition の場合、次に入る宛先が target で、それが enterTransition の適用対象となります。exitTransition はその逆で、initial 画面が exitTransition の適用対象です。
initial
target
NavBackStackEntry
つまり、宛先は次のように書くことができます。
composable( "profile/{id}", enterTransition = { _, _ -> // Let's make for a really long fade in fadeIn(animationSpec = tween(2000) }) { // Add content like normal}
"profile/{id}",
enterTransition = { _, _ ->
// Let's make for a really long fade in
fadeIn(animationSpec = tween(2000)
}
) {
// Add content like normal
または、移動元や移動先に基づいてアニメーションを制御することもできます。
composable( "friendList" exitTransition = { _, target -> when (target.destination.route) { "profile/{id}" -> ExitTransition.fadeOut( animationSpec = tween(2000) ) // slowly fade it out else -> null // use the defaults } }) { // Add content like normal}composable( "profile/{id}", enterTransition = { initial, _ -> when (initial.destination.route) { "friendList" -> slideInVertically( initialOffsetY = { 1800 } ) // slide in the profile screen else -> null // use the defaults }) { // Add content like normal
"friendList"
exitTransition = { _, target ->
when (target.destination.route) {
"profile/{id}" -> ExitTransition.fadeOut(
animationSpec = tween(2000)
) // slowly fade it out
else -> null // use the defaults
composable(
enterTransition = { initial, _ ->
when (initial.destination.route) {
"friendList" -> slideInVertically(
initialOffsetY = { 1800 }
) // slide in the profile screen
この例では、友達リスト画面は、プロフィール画面に戻る際の遷移を制御しています。また、プロフィール画面は、友達リスト画面から入ってくる際の遷移を制御しています。これにより、2 つの宛先間でカスタムのスライド オーバー アニメーションを実現しています。この例からは、null を指定すると「デフォルトを使用」という意味になることもわかります。デフォルトは、親のナビゲーション グラフ、そして親の親のナビゲーション グラフというように、ルートとなる AnimatedNavHost までの階層によって決まります。つまり、AnimatedNavHost でグローバルな enterTransition と exitTransition を変更するだけで、デフォルトのアニメーション(ここでは、クロスフェードのタイミング)を設定できます。
null
1 つのサブグラフだけのデフォルトを変更したい場合は(たとえば、ログインフローでは常に水平スライド アニメーションを使う)、ネストしたグラフのレベルでアニメーションを設定することもできます。
navigation( startDestination = "ask_username" route = "login" enterTransition = { initial, _ -> // Check to see if the previous screen is in the login graph if (initial.destination.hierarchy.any { it.route == "login" }) { slideInHorizontally(initialOffsetX = { 1000 } } else null // use the defaults } exitTransition = { _, target -> // Check to see if the new screen is in the login graph if (target.destination.hierarchy.any { it.route == "login" }) { slideOutHorizontally(targetOffsetX = { -1000 } } else null // use the defaults } popEnterTransition = { initial, _ -> // Check to see if the previous screen is in the login graph if (initial.destination.hierarchy.any { it.route == "login" }) { // Note how we animate from the opposite direction on a pop slideInHorizontally(initialOffsetX = { -1000 } } else null // use the defaults } popExitTransition = { _, target -> // Check to see if the new screen is in the login graph if (target.destination.hierarchy.any { it.route == "login" }) { // Note how we animate from the opposite direction on a pop slideOutHorizontally(targetOffsetX = { 1000 } } else null // use the defaults }) { composable("ask_username") { // Add content } composable("ask_password") { // Add content } composable("register") {
startDestination = "ask_username"
route = "login"
// Check to see if the previous screen is in the login graph
if (initial.destination.hierarchy.any { it.route == "login" }) {
slideInHorizontally(initialOffsetX = { 1000 }
} else
null // use the defaults
// Check to see if the new screen is in the login graph
if (target.destination.hierarchy.any { it.route == "login" }) {
slideOutHorizontally(targetOffsetX = { -1000 }
popEnterTransition = { initial, _ ->
// Note how we animate from the opposite direction on a pop
slideInHorizontally(initialOffsetX = { -1000 }
popExitTransition = { _, target ->
slideOutHorizontally(targetOffsetX = { 1000 }
composable("ask_username") {
// Add content
composable("ask_password") {
composable("register") {
階層拡張メソッド (英語)を使って宛先が実際にログイングラフの一部であるかどうかを判断する方法に注目してください。このようにすると、ログイングラフ への 遷移とログイングラフ からの 遷移に、デフォルトの遷移(または上位レベルで設定した遷移)を使うことができます。
水平スライドのような方向性のある遷移では、この仕組みのおかげで、enterTransition と popEnterTransition との違いが非常に役立ちます。ある画面は右にスライドし、他の画面は左にスライドするような事態を避けることができます。
Accompanist は Jetpack ライブラリのブースター ロケットとしてはたらき、Compose 1.1 の開発が進行中でも、試験運用版機能をすぐに 利用できるようにします。
Accompanist Navigation Animation は、次のようにして追加します。
implementation "com.google.accompanist:accompanist-navigation-animation:0.16.1"
implementation
"com.google.accompanist:accompanist-navigation-animation:0.16.1"
Compose 1.0.1 をベースとした Navigation 2.4 と、試験運用版 API によって Compose 1.0 の限界を押し広げる Accompanist Navigation Animation の先には、別の景色、すなわち Compose 1.1 が見えています。Compose ロードマップ (英語)を見てみると、実に重要な 1 つの機能が予定されています。
共通要素遷移のサポート
Navigation 2.5 で目指しているのは、Compose 1.1 のすべての利点を Navigation Compose に導入することです。つまり、アニメーション API が試験運用版でなくなれば、直接 Navigation Compose に組み込めるようになります。さらに、共通要素遷移が利用できるようになり、それをサポートする API も構築できます。
さらに言えば、Accompanist Navigation Animation は一時的な対応と考えるべきです。Navigation Compose 自体が同じレベルのアニメーション API を提供すれば(皆さんのフィードバックに合わせて調整します)、直接それを使うことができ、Accompanist Navigation Animation は完全に削除できます。
短時間で機能を提供できる Jetpack ライブラリとして、安定性と、私たちが自らに課している上位互換性要件、下位互換性要件のバランスを取るのは、思うほど簡単なことではありません。Jetpack Compose は、Accompanist という大きな助けを借りて勢いを増し、このブースター ロケットが必要なくなるところまで加速していきます。Accompanist に時間を費やしてくれた Chris Banes (英語)を始めとするすべてのデベロッパー、Compose を支えるすべてのチーム、そして Android 開発の未来を形作ることに貢献してくださっているすべての皆さんに感謝します。
追伸: Navigation + Accompanist のメリットをさらに知りたい方は、新しい Accompanist Navigation Material (英語)をチェックしてみてください!
Navigation-Material の紹介 🧭🎨️ (英語)
Reviewed by Tamao Imura - Developer Marketing Manager, Google Play