JetpackComposeのDialogを下からスライドイン表示させる
気象予報のアプリからちょっと脇にそれます。。。
前回単一選択ダイアログを作成しました。これに、iOS風の下からスライドインするアニメーションをつけようと思います。
Dialogのアニメーション
DialogComposeは、表示/非表示を切り替えるだけなら「AnimatedVisibility」でもOKですが、それ以外のアニメーションは動きません。
Dialogとしての機能(戻るボタンで閉じる、コンテンツ外をタップで閉じる)はそのままで、コンテンツに表示アニメーションをつける場合は、Dialogのcontent内で表示/非表示時にアニメーションを実行するようにすれば良いです。
アニメーションの記述
アニメーション動作を定義するCompose作成します。
@Composable
fun SlideInTransition(
visibleState: MutableTransitionState<Boolean>,
content: @Composable AnimatedVisibilityScope.() -> Unit,
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Transparent),
contentAlignment = Alignment.Center,
) {
AnimatedVisibility(
visibleState = visibleState,
enter = slideInVertically(
animationSpec = tween(
durationMillis = ANIMATION_TIME.toInt(),
easing = LinearEasing,
),
initialOffsetY = { fullHeight -> fullHeight },
),
exit = slideOutVertically(
animationSpec = tween(
durationMillis = ANIMATION_TIME.toInt(),
easing = LinearEasing,
),
targetOffsetY = { fullHeight -> fullHeight },
),
content = content,
)
}
}
引数のvisibleStateを、MutableStateではなく、MutableTransitionStateとしているのは、ダイアログを閉じる際に、アニメーションの完了を待ってDialogのonDismissRequestを呼び出すためです。(アニメーションの完了待ちに関しては後述します。)
一番外側のBoxは、ダイアログが表示した際に一番下のレイヤーになるレイアウトです。(これがないとanimationでコンテンツが表示できなかった)この中で、スライドイン/スライドアウトのアニメーション処理を行っています。
ダイアログを閉じる動作
private suspend fun startDismissWithExitAnimation(
showContentState: MutableTransitionState<Boolean>,
onDismissRequest: () -> Unit,
) = withContext(Dispatchers.IO) {
showContentState.targetState = false
while (true) {
delay(10L)
if (!showContentState.targetState && showContentState.isIdle) {
onDismissRequest.invoke()
break
}
}
}
delayがあるので、suspend関数になります。
showContentStateは、前述のvisibleStateと同じインスタンスです。これを切り替えることで、まずは下にスライドアウトするアニメーションを動かします。
アニメーションの動作終了は、showContentState.isIdleがtrueになるのでそれを監視することで実現してみました。
アニメーションが終わったら、ダイアログ自体の表示終了をリクエストするためonDismissRequestを呼び出しています。
DialogのHelperクラス
class SlideInTransitionDialogHelper(
private val onStartDismissWithExitAnimation: ((() -> Unit)?) -> Unit,
) {
fun triggerAnimatedClose(onCompletedAnimation: (() -> Unit)? = null) {
onStartDismissWithExitAnimation.invoke(onCompletedAnimation)
}
}
ダイアログ内に表示するコンテンツから閉じる、キャンセルなどのボタンを通して、ダイアログを閉じたいときに使用するHelperクラスです。
SlideInTransitionDialog
これまでのクラスを使用して、DialogComposeを作っていきます。
@Composable
fun SlideInTransitionDialog(
onDismissRequest: () -> Unit,
dismissOnBackPress: Boolean = true,
content: @Composable (SlideInTransitionDialogHelper) -> Unit,
) {
val coroutineScope: CoroutineScope = rememberCoroutineScope()
val showContentState = remember { MutableTransitionState(false) }
val mutex = remember { Mutex(true) }
LaunchedEffect(key1 = Unit) {
launch {
mutex.withLock {
showContentState.targetState = true
}
}
}
Dialog(
onDismissRequest = {
coroutineScope.launch {
startDismissWithExitAnimation(showContentState, onDismissRequest)
}
},
properties = DialogProperties(dismissOnBackPress = dismissOnBackPress),
) {
LaunchedEffect(key1 = Unit) {
if (mutex.isLocked) {
mutex.unlock()
}
}
SlideInTransition(visibleState = showContentState) {
content(
SlideInTransitionDialogHelper(
onStartDismissWithExitAnimation = { onCompletedAnimation ->
coroutineScope.launch {
startDismissWithExitAnimation(showContentState, onDismissRequest)
onCompletedAnimation?.invoke()
}
},
),
)
}
}
}
処理の流れとしては、
- Dialogの表示(背景をグレーにして、空っぽのコンテンツを表示するところまで)
- 起動したら、コンテンツをアニメーション表示する
という動作になります。
Dialogの表示を待機するのに、Mutexを使用しています。
DialogComposeのLaunchedでmutexのロックを解除するまで、SlideInTransitionDialogのLaunchedにあるmutex.withLockで処理が止まります。つまり、Dialogが起動したあとに、mutexのロックが解除され、mutex.withLockでロックの取得ができ、アニメーション表示が開始されるという流れになります。
使う側
このスライドインするDialogのコンテンツとして、前回作った単一選択ダイアログを少しいじったものをつくります。
SlideInTransitionDialog(onDismissRequest = onDismissRequest) { helper ->
Surface(
/*...*/
) {
Column(
/*...*/
) {
/*...*/
Buttons(
/*...*/
onDismissRequest = {
helper.triggerAnimatedClose()
},
onConfirmRequest = {
helper.triggerAnimatedClose {
onConfirmRequest.invoke(it)
}
},
)
}
}
}
SingleSelectionDialog(アニメーションじゃない方)では、SurfaceをDialogのコンテンツにしていました。
SingleSelectionSlideInDialog(アニメーションの方)は、先ほど作ったSlideInTransitionDialogのコンテンツにしています。
また閉じる際の動作は、Screenの呼び出し側から指定されたonDismissRequest,onConfirmRequestをそのまま呼び出していましたが、HelperクラスのtriggerAnimatedClose()を呼び出してアニメーションを実行する様に修正しています。
完成!
これで冒頭の完成イメージができあがりました。
すべてのコードはこちらからどうぞ