こんにちは。今年もこの季節がやってきました。株式会社ZOZOでAndroidエンジニアをしております@zzt-osamuhanzawaです。ZOZO Advent Calendar 2023の13日目シリーズ5の担当します。最近のAndroidのUI実装する際、Composeで実装するのが増えてきたかと思います。今回はComposeでポップアップのような画面上部にユーザーに通知し、左スワイプで消えるようなUIの実装について紹介したいと思います。
はじめに
スワイプ操作はCompose Material API であるswipeable
を利用します。ただし、いきなりですがswipeable
は今後、AnchoredDraggable
に置き換わるそうです。ただ、そちらは試験運用ということで今回は swipeable
での方法で話していきたいと思います。画面上部にポップアップ表示し、左スワイプで消えるようなユースケースとしてはユーザーへの何らかの通知やワーニングを知らせるようなUIをイメージです。
SwapeableなComposeの実装について
早速ですが実装を見ていきましょう。以下のコードが実現したいComposeのベースとなる部分です。
@Composable
fun SwipeablePopup(
widthDp: Dp = 340.dp,
onDismiss: () -> Unit,
backgroundColor: Color = Color.White,
content: @Composable BoxScope.() -> Unit,
) {
val swipeableState = rememberSwipeableState(1)
val screenWidth = LocalConfiguration.current.screenWidthDp
val screenWidthPx = with(LocalDensity.current) { screenWidth.dp.toPx() }
val sizePx = with(LocalDensity.current) { widthDp.toPx() }
val centerOffset = screenWidthPx * 0.5f - sizePx * 0.5f
val anchors = mapOf(-screenWidthPx to 0, centerOffset to 1)
val rawOffsetX = swipeableState.offset.value.roundToInt()
val offsetX = if (rawOffsetX > 0) 0 else rawOffsetX
if (swipeableState.currentValue == 0) {
onDismiss() // dismissを通知
}
Box(
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.offset { IntOffset(offsetX, 0) }
.width(widthDp)
.background(
color = backgroundColor,
shape = RoundedCornerShape(16.dp)
)
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.3f) },
orientation = Orientation.Horizontal,
)
) {
content() // 実際に表示したいCompose
}
}
}
重要なのは.swipeable
の箇所ですが、パラメータに関しては
-
state
: スワイプの状態(今回は 1 or 0) -
thresholds
: スワイプ状態を維持するかしないかの閾値 -
orientation
: 縦横どちらにスワイプするか、今回は横スワイプ
となります。anchors
に関してはstateに対するpx値をMapで指定します。今回は左スワイプでPopupをdismissしたいため画面の横幅サイズのマイナス値を指定してます。右スワイプで消したい場合は横幅サイズを指定すれば良いと思います。
また、指の動きとPopUpを連動させたいためswipeableState
からoffsetの値を受け取り、Box
の.offset
を以下のように指定しています。
.offset { IntOffset(offsetX, 0) }
以上が今回、実現したいUIのベースになるCompose実装になります。
あとはSwipeablePopup
のパラメータであるcontent
に表示したいComposeを渡してあげるだけです。
fun PopupWarning(
widthDp: Dp = 340.dp,
onDismiss: () -> Unit,
) {
SwipeablePopup(
widthDp = widthDp,
onDismiss = onDismiss,
) {
// 表示したいレイアウトを実装
}
}
自動でDismissするようなPopup UI
ネタとして少し寂しいので、このSwipeablePopupを利用して勝手にdismissする実装についても少し書きたいと思います。画面上部にPopupが表示され続けるのもUX的には良くないこともあるので、例えば数秒後に消えるような実装についてです。これを実現する場合は、 AnimatedVisibility
とdelay()
を使い表示状態を変化させます。以下が全体のコードとなります。
@Composable
fun SomeErrorPopup(
title: String,
paragraph: String,
autoDismissMs: Long = 3000L, // 指定がなかったら3秒表示
) {
val state = remember {
MutableTransitionState(false).apply { targetState = true }
}
AnimatedVisibility(
visibleState = state,
enter = slideInVertically() + fadeIn(),
exit = slideOutHorizontally() + fadeOut()
) {
if (autoDismissMs > 0) {
LaunchedEffect(Unit) {
delay(autoDismissMs)
state.targetState = false
}
}
PopupWarning(
onDismiss = {
state.targetState = false
},
title = title,
paragraph = paragraph,
)
}
}
特にこれといった難しく点もなく、AnimatedVisibility
を利用し、指定した時間だけdelayさせ、時間が経過した場合、非表示、もしくはスワイプアウトした時点で表示状態を非表示にするといった処理を行います。
まとめ
簡単ですが今回はswipeable
なPopup UIをComposeでどう実装するかを説明しました。何かの参考になれば嬉しいです。
参考資料