こんにちは。Android アプリ開発担当の nagayama(@nagayan_dev)です。
今回は Swipeable API が deprecated になったため、代替となる AnchoredDraggable API に移行していきたいと思います。
背景
以前、下記の「Android Jetpack Compose で途中で止まる Swipeable レイアウトを作ってみる」で Modifier.swipeable
を用いた Compose UI 実装をご紹介しました。
tech.gunosy.io
しかし Modifier.swipeable
は androidx.compose.foundation:foundation-*
ライブラリの 1.6.0 から deprecated となり、代わりに Modifier.anchoredDraggable
を使用するようにと公式からアナウンスがされています。
developer.android.com
今回は「Android Jetpack Compose で途中で止まる Swipeable レイアウトを作ってみる」で作成したコードを Modifier.anchoredDraggable
に移行する対応をしたいと思います。
実装
完成したコードは下記のようになります。
private enum class OpenedSwipeableState { INITIAL, OPENED, OVER_SWIPED } @SuppressLint("RememberReturnType") @OptIn(ExperimentalFoundationApi::class) @Composable private fun SwipeableRow( onSwipe: () -> Unit, content: @Composable () -> Unit ) { BoxWithConstraints { val constraintsScope = this // 画面の横幅 val maxWidthPx = with(LocalDensity.current) { constraintsScope.maxWidth.toPx() } // 削除ボタンの横幅 val deleteButtonWidth = 64.dp val deleteButtonWidthPx = with(LocalDensity.current) { deleteButtonWidth.toPx() } val anchors = DraggableAnchors { OpenedSwipeableState.INITIAL at 0f OpenedSwipeableState.OPENED at deleteButtonWidthPx OpenedSwipeableState.OVER_SWIPED at maxWidthPx } val decayAnimationSpec = rememberSplineBasedDecay<Float>() val anchorDraggableState = remember { AnchoredDraggableState( initialValue = OpenedSwipeableState.INITIAL, confirmValueChange = { when (it) { OpenedSwipeableState.INITIAL -> { // do nothing } OpenedSwipeableState.OPENED -> { // Opened Event } OpenedSwipeableState.OVER_SWIPED -> { // Over Swipe Event // todo delete onSwipe.invoke() } } true }, anchors = anchors, positionalThreshold = { distance: Float -> distance * 0.5f }, velocityThreshold = { 5000f }, // 横スワイプですぐに消えてしまうため、大きい数値を設定 snapAnimationSpec = SpringSpec( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMediumLow ), decayAnimationSpec = decayAnimationSpec, ) } Box( Modifier.anchoredDraggable( state = anchorDraggableState, reverseDirection = true, orientation = Orientation.Horizontal, ) ) { DeleteButtonLayout() // 後から出てくる削除ボタン Box( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .offset { IntOffset(-anchorDraggableState.offset.roundToInt(), 0) } ) { content() } } } }
こちらを元に解説をしていきます。
DraggableAnchors
val anchors = DraggableAnchors { OpenedSwipeableState.INITIAL at 0f OpenedSwipeableState.OPENED at deleteButtonWidthPx OpenedSwipeableState.OVER_SWIPED at maxWidthPx }
DraggableAnchors
は「状態」と「停止する位置(px)」をセットにした Map を定義します。状態の定義は前回使用した OpenedSwipeableState をそのまま使用します。停止位置も同じ値にするため、INITIAL 状態は 0 、OPENED は削除ボタン分サイズ 、OVER_SWIPED は画面の横幅サイズを指定します。
AnchoredDraggableState
val decayAnimationSpec = rememberSplineBasedDecay<Float>() val anchorDraggableState = remember { AnchoredDraggableState( initialValue = OpenedSwipeableState.INITIAL, confirmValueChange = { when (it) { OpenedSwipeableState.INITIAL -> { // do nothing } OpenedSwipeableState.OPENED -> { // Opened Event } OpenedSwipeableState.OVER_SWIPED -> { // Over Swipe Event // todo delete onSwipe.invoke() } } true }, anchors = anchors, positionalThreshold = { distance: Float -> distance * 0.5f }, velocityThreshold = { 5000f }, // 横スワイプですぐに消えてしまうため、大きい数値を設定 snapAnimationSpec = SpringSpec( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMediumLow ), decayAnimationSpec = decayAnimationSpec, ) }
AnchoredDraggableState
は新しい API である Modifier.anchoredDraggable
に指定する State です。引数は下記のようになっています。
initialValue
:DraggableAnchors
で定義した状態の初期状態を設定します。confirmValueChange
:状態の変化した時に処理を実行する場合、ここに実装します。OVER_SWIPED のイベント時に削除を実行するため、onSwipe
の通知を出しています。anchors
:先ほど作成したDraggableAnchors
を設定します。positionalThreshold
:状態の変化位置のしきい値です。特にこだわりは無いため 50% の位置に設定しています。velocityThreshold
:スワイプ速度のしきい値です。positionalThreshold
に位置に達していなくてもこの速度以上のスワイプがされると終了するもので、横スワイプの使いやすさに直結します。元のModifier.swipeable
にもvelocityThreshold
があり、デフォルト値は 125.dp となっていました。今回も同じ値を指定してみましたが横スワイプするとすぐに消えてしまう印象があったため、 何度か試してみた結果 5000f が使いやすかったのでこの値にしました。snapAnimationSpec
:跳ねるアニメーションの設定値であるSpringSpec
を設定します。後述を参照してください。デフォルトの値よりも跳ねるように設定しています。decayAnimationSpec
:別の状態に到達または通過する速度でスワイプした時のアニメーション設定です。こちらも特にこだわりはないためrememberSplineBasedDecay<Float>()
をそのまま設定しています。
以前の SwipeableState
/ Modifier.swipeable
と違い、細かいスワイプ動作やアニメーション設定ができるようになっています。
SpringSpec
SpringSpec は跳ねるアニメーションの設定値です。
@Immutable class SpringSpec<T>( val dampingRatio: Float = Spring.DampingRatioNoBouncy, val stiffness: Float = Spring.StiffnessMedium, val visibilityThreshold: T? = null )
「バネ」のような動き方を再現するため、主に以下 2 つを設定します。
dampingRatio
:バネの振動がどれくらい早く収束するかの値(減衰比)です。値が大きいほど速く振動が停止し、小さいほど長い時間振動します。値は 0.0 〜 1.0 の間で指定します。デフォルトは 1.0 となっているため、まったく振動しない設定になっています。stiffness
:バネの固さ(剛性)です。値が大きいほど「固いバネ」で速いアニメーションとなり、値が小さいほど「柔らかいバネ」でゆっくりとアニメーションをします。デフォルトは 1500f です。
Modifier.anchoredDraggable
Box( Modifier.anchoredDraggable( state = anchorDraggableState, reverseDirection = true, orientation = Orientation.Horizontal, ) ) { DeleteButtonLayout() // 後から出てくる削除ボタン }
Modifier.anchoredDraggable
は deprecated になった Modifier.swipeable
に代わる API です。設定する引数は基本的に swipeable
のものとは変わりありません。
state
:スワイプの状態です。先ほど作成したAnchoredDraggableState
を設定します。reverseDirection
:デフォルトは右方向にスワイプでき、ここを true にすると左方向にスワイプできます。orientation
:スワイプできる方向。今回は横方向のスワイプなのでOrientation.Horizontal
を指定します。
offset
Box( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .offset { IntOffset(-anchorDraggableState.offset.roundToInt(), 0) } ) { content() }
最後にスワイプ対象の offset 値を設定します。元の Modifier.swipeable
の時と同様に、state から offset 値を取得して Int 型に変換します。offset は右方向が正の値になるため、「マイナス値」で指定をします。
以上、Modifier.anchoredDraggable
に移行する対応が完了しました。
まとめ
Modifier.swipeable
から Modifier.anchoredDraggable
に移行する対応をしました。設定できるパラメータが増えたためちょっと手間ではあるものの、表現の幅が広がって使いやすくなったのではないかと感じました。ただ、Modifier.anchoredDraggable
等が ExperimentalFoundationApi
となっているため、今後変更が入る可能性があります。これからも Jetpack Compose ライフを楽しみましょう。
最後に
Gunosy では Android エンジニアを募集しております。ご興味をお持ちの方はぜひご連絡ください。