Navigation3のNavDisplayでは画面遷移時の動きを指定できます。
3つの引数で指定でき、それぞれの適用箇所は以下になります。
| 引数 | 適用場所 |
|---|---|
transitionSpec |
backStackに積む(プッシュ) |
popTransitionSpec |
backStackから取り出す(ポップ) |
predictivePopTransitionSpec |
予測型「戻る」 |
予測型戻る動作時のアニメーションも個別に指定できます。この引数はAnimatedContentTransitionScope<Scene<T>>.(@NavigationEvent.SwipeEdge Int) -> ContentTransformであり、引数として左右どちら側の操作に応じて遷移を変更できます。
本稿では、どのように実装すればよいか、どのような表現が可能なのかを紹介しようと思います。
デフォルト動作
まずはデフォルト動作です。以下のような実装になっています。
ContentTransformの第1引数はtargetContentEnterで、入ってくる画面の遷移、第2引数initialContentExitで、出ていく画面の遷移を指定します。
internal const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700
public actual fun <T : Any> defaultTransitionSpec():
AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform = {
ContentTransform(
fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)),
fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)),
)
}
public actual fun <T : Any> defaultPopTransitionSpec():
AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform = {
ContentTransform(
fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)),
fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)),
)
}
public actual fun <T : Any> defaultPredictivePopTransitionSpec():
AnimatedContentTransitionScope<Scene<T>>.(@NavigationEvent.SwipeEdge Int) -> ContentTransform =
{
ContentTransform(
fadeIn(
spring(
dampingRatio = 1.0f, // reflects material3 motionScheme.defaultEffectsSpec()
stiffness = 1600.0f, // reflects material3 motionScheme.defaultEffectsSpec()
)
),
scaleOut(targetScale = 0.7f),
)
}
プッシュ・ポップの動作はフェードになっており、現在の画面が薄くなって、新しい画面が濃くなる遷移です。
予測型戻るの動作は別の動きになっており、現在の画面が縮小して、下から前の画面が現れるという動きです。
slideInHorizontally / slideOutHorizontally
次の画面が横からスライドイン、現在の画面がスライドアウトする動作を実装するなら以下のようにします。
移動量が同一だと全体が横スクロールしているように見えるので、スタック下の画面の動きを小さくして、上の画面が覆いかぶさるような指定としています。
予測型戻るでは左右どちら側にスワイプしているかでスライドの向きを変更することで、上の画面をスワイプで動かしているような動きにしています。
private fun <T : Any> transition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { it },
),
initialContentExit = slideOutHorizontally(
targetOffsetX = { -it / 5 },
),
)
}
private fun <T : Any> popTransition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { -it / 5 },
),
initialContentExit = slideOutHorizontally(
targetOffsetX = { it },
),
)
}
private fun <T : Any> predictivePopTransition(): AnimatedContentTransitionScope<Scene<T>>.(
@SwipeEdge Int,
) -> ContentTransform =
{ edge ->
if (edge == NavigationEvent.EDGE_RIGHT) {
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { it / 5 },
),
initialContentExit = slideOutHorizontally(
targetOffsetX = { -it },
),
)
} else {
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { -it / 5 },
),
initialContentExit = slideOutHorizontally(
targetOffsetX = { it },
),
)
}
}
slideInHorizontally + unveilIn / slideOutHorizontally + veilOut
slideIn/slideOutだけだと、前後の画面はそのまま表示されるため、上下関係がわかりにくい場合があります。
遷移に伴って色を被せる遷移としてunveilIn/veilOutがあります。これを組み合わせるとスタック下側にある画面を暗くすることができます。逆の指定をして明るくすることもできます。
本稿執筆時点では@ExperimentalAnimationApiをつける必要があり、将来的に変更される可能性があります。
private fun <T : Any> transition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { it },
),
initialContentExit = slideOutHorizontally(
targetOffsetX = { -it / 5 },
) + veilOut(),
)
}
private fun <T : Any> popTransition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { -it / 5 },
) + unveilIn(),
initialContentExit = slideOutHorizontally(
targetOffsetX = { it },
),
)
}
private fun <T : Any> predictivePopTransition(): AnimatedContentTransitionScope<Scene<T>>.(
@SwipeEdge Int,
) -> ContentTransform =
{ edge ->
if (edge == NavigationEvent.EDGE_RIGHT) {
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { it / 5 },
) + unveilIn(),
initialContentExit = slideOutHorizontally(
targetOffsetX = { -it },
),
)
} else {
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { -it / 5 },
) + unveilIn(),
initialContentExit = slideOutHorizontally(
targetOffsetX = { it },
),
)
}
}
unveilInのinitialColor、veilOutのtargetColorを引数で指定でき、initialColorから完全な透明、及び、完全な透明からtargetColorに変更する遷移となります。デフォルトの値はColor.Black.copy(alpha = 0.5f)となっています。
slideInHorizontally + fadeIn / slideOutHorizontally + fadeOut
スライドとフェードを組み合わせるパターン、この場合ベースとなる画面は動かさない方が遷移がきれいかと思います。
private fun <T : Any> transition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = slideInHorizontally(
initialOffsetX = { it },
) + fadeIn(),
initialContentExit = ExitTransition.None,
)
}
private fun <T : Any> popTransition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = slideOutHorizontally(
targetOffsetX = { it },
) + fadeOut(),
)
}
private fun <T : Any> predictivePopTransition(): AnimatedContentTransitionScope<Scene<T>>.(
@SwipeEdge Int,
) -> ContentTransform =
{ edge ->
if (edge == NavigationEvent.EDGE_RIGHT) {
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = slideOutHorizontally(
targetOffsetX = { -it },
) + fadeOut(),
)
} else {
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = slideOutHorizontally(
targetOffsetX = { it },
) + fadeOut(),
)
}
}
scaleIn + fadeIn / scaleOut + fadeOut
上からスタック上の画面が覆いかぶさり、縮小しながら消えるような遷移の実装です。
private fun <T : Any> transition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = scaleIn(
initialScale = 1.3f,
) + fadeIn(),
initialContentExit = ExitTransition.None,
)
}
private fun <T : Any> popTransition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = scaleOut(
targetScale = 0.7f,
) + fadeOut(),
)
}
private fun <T : Any> predictivePopTransition(): AnimatedContentTransitionScope<Scene<T>>.(
@SwipeEdge Int,
) -> ContentTransform =
{
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = scaleOut(
targetScale = 0.7f,
) + fadeOut(),
)
}
視線の動きを小さくしつつも、変化を感じられる遷移だと思います。
slideInVertically / slideOutVertically
次の画面が下から上にせり上がり、戻るときは下に抜けていくという動作です。
private fun <T : Any> transition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = slideInVertically(
initialOffsetY = { it },
),
initialContentExit = ExitTransition.None,
)
}
private fun <T : Any> popTransition(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =
{
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = slideOutVertically(
targetOffsetY = { it },
),
)
}
private fun <T : Any> predictivePopTransition(): AnimatedContentTransitionScope<Scene<T>>.(
@SwipeEdge Int,
) -> ContentTransform =
{
ContentTransform(
targetContentEnter = EnterTransition.None,
initialContentExit = slideOutVertically(
targetOffsetY = { it },
),
)
}





