Swipeable から AnchoredDraggable に移行する

Swipeable は、ボトムシート、ドロワー、スワイプして閉じるなど、個別の状態をスワイプできるコンポーネントの作成に役立つ Compose マテリアル API です。コンポーネントのサイズに依存するアンカーなど、高度なユースケースをより適切にサポートするために、Compose-Foundation 1.6.0-alpha01 で後継となる AnchoredDraggable が公開されました。AnchoredDraggable は、アンカー状態のドラッグ可能なコンポーネント(ボトムシート、引き出し、スワイプして閉じるなど)を構築するための Foundation API です。

マテリアルの Swipeable API は非推奨になり、Foundation の AnchoredDraggable に置き換えられました。今後のリリースで削除される予定です。このガイドでは、Swipeable API から AnchoredDraggable に移行する方法について説明します。

SwipeableStateAnchoredDraggableState に移行する

まず、状態ホルダーの変更を特定します。AnchoredDraggableState は継承できません。オフセットは初期化前に Float.NaN として表されます。

状態ホルダーを更新する

AnchoredDraggableState は最終クラスです。つまり、継承できません。既存のコンポーネントが SwipeableState から継承している場合は、状態保持クラスを更新して、AnchoredDraggableState から継承するのではなく、AnchoredDraggableState への参照を保持するようにします。

スワイプ可能

class MySwitchState: SwipeableState()

AnchoredDraggable

class MySwitchState {
    private val anchoredDraggableState = AnchoredDraggableState(...)
}

状態ホルダーは SwipeableState から継承しなくなったため、API を自分で公開する必要が生じることがあります。使用できる一般的な API は、offsetprogresscurrentValuetargetValue です。

オフセットにアクセスする

Swipeable とは異なり、AnchoredDraggableStateoffset は初期化前は Float.NaN です。AnchoredDraggable では、アンカーを AnchoredDraggableState のコンストラクタに渡すか、AnchoredDraggableState#updateAnchors で更新できます。アンカーを AnchoredDraggableState のコンストラクタに渡すと、オフセットがすぐに初期化されます。

アンカーがレイアウトに依存している場合や変更される可能性がある場合は、AnchoredDraggableState#updateAnchors を使用して、アンカーが変更されたときに状態を再作成しないようにします。

updateAnchors を使用する場合、アンカーを updateAnchors に渡す前にオフセットは Float.NaN になります。Float.NaN をコンポーネントに誤って渡さないようにするには、AnchoredDraggableState#requireOffset を使用して、読み取り時にオフセットが初期化されていることを要求します。これにより、不整合や潜在的なバグを早期に発見できます。

@Composable
fun AnchoredDraggableBox() {
    val state = remember { AnchoredDraggableState(...) }
    val density = LocalDensity.current
    val anchors = remember { DraggableAnchors { ... } }
    SideEffect {
        state.updateAnchors(anchors)
    }
    Box(
        Modifier.offset { IntOffset(x = state.requireOffset(), y = 0) }
    }
}

Modifier.swipeableModifier.anchoredDraggable に移行する

Modifier.swipeable に代わり Modifier.anchoredDraggable() になりました。次のセクションで説明するように、Modifier.swipeable() のパラメータの一部は AnchoredDraggableState に直接移動されています。

アンカーを定義する

DraggableAnchors ビルダー メソッドを使用してアンカーを定義します。次に、それらを AnchoredDraggableState#updateAnchors または AnchoredDraggableState のコンストラクタに渡します。

コンストラクタ

enum class DragValue { Start, Center, End }

@Composable
fun AnchoredDraggableBox() {
    val anchors = DraggableAnchors {
        Start at -100.dp.toPx()
        Center at 0f
        End at 100.dp.toPx()
    }
    val state = remember {
        AnchoredDraggableState(anchors = anchors)
    }
    Box(
        Modifier.offset { IntOffset(x = state.requireOffset(), y = 0) }
    )
}

updateAnchors

enum class DragValue { Start, Center, End }

@Composable
fun AnchoredDraggableBox() {
    val state = remember { AnchoredDraggableState(...) }
    val density = LocalDensity.current
    val anchors = with (density) {
        DraggableAnchors {
            Start at -100.dp.toPx()
            Center at 0f
            End at 100.dp.toPx()
        }
    }
    SideEffect {
        state.updateAnchors(anchors)
    }
    Box(
        Modifier.offset { IntOffset(x = state.requireOffset(), y = 0) }
    )
}

アンカーが静的の場合は、コンストラクタに渡します。レイアウトに依存する場合や静的でない場合は、updateAnchors を使用します。

位置のしきい値を定義する

thresholds パラメータの型と名前が変更されました。AnchoredDraggableState には、個別の ThresholdConfig インターフェースではなく、しきい値の位置を返すラムダ関数を受け取る positionalThreshold パラメータがあります。たとえば、50% の位置基準は次のように表すことができます。

val anchoredDraggableState = AnchoredDraggableState(
    positionalThreshold = { distance -> distance * 0.5f },
    ...
)

位置のしきい値 56dp は次のように表すことができます。

val density = LocalDensity.current
val anchoredDraggableState = AnchoredDraggableState(
    positionalThreshold = { with(density) { 56.dp.toPx() } },
    ...
)

ベロシティのしきい値を定義する

速度しきい値も AnchoredDraggableState のコンストラクタに渡され、ラムダとして表されます。

val density = LocalDensity.current
val anchoredDraggableState = AnchoredDraggableState(
    velocityThreshold = { with(density) { 125.dp.toPx() } },
    ...
)

API サーフェスの変更

API サーフェスの変更の概要は次のとおりです。

AnchoredDraggableState

SwipeableState

AnchoredDraggableState

open class SwipeableState(initialValue: T, animationSpec: AnimationSpec = …, confirmStateChange: (T) -> Boolean = …)

class AnchoredDraggableState( initialValue: T, animationSpec: AnimationSpec = …, confirmValueChange: (T) -> Boolean = …, positionalThreshold: Density.(Float) -> Float = …, velocityThreshold: Dp = …)

offset: State

offset: Float
requireOffset()

progress: SwipeProgress

progress: Float [0f..1f]

currentValue: T

currentValue: T

targetValue: T

targetValue: T

direction: Float [-1f, 0f, 1f] を選択)

なし

suspend animateTo(
targetValue: T,
anim: AnimationSpec = …)

suspend animateTo(
targetState: T,
velocity: Float =
lastVelocity)

suspend snapTo(targetValue: T)

suspend snapTo(targetValue: T)

performDrag(delta: Float)

dispatchRawDelta(delta: Float)

suspend performFling(velocity: Float)

suspend settle(velocity: Float)

isAnimationRunning: Boolean

isAnimationRunning: Boolean

lastVelocity: Float

Modifier.anchoredDraggable

Modifier.swipeable

Modifier.anchoredDraggable

state: SwipeableState

state: AnchoredDraggableState

anchors: Map

AnchoredDraggableState#updateAnchors
or

AnchoredDraggableState#constructor

orientation: Orientation

orientation: Orientation

enabled: Boolean = true

enabled: Boolean = true

reverseDirection: Boolean = false

reverseDirection: Boolean = false

interactionSource: MutableInteractionSource? = null

interactionSource: MutableInteractionSource? = null

thresholds: (from: T, to: T) -> ThresholdConfig = FixedThreshold(56.dp)

positionalThreshold として AnchoredDraggableState コンストラクタに渡される

resistance: ResistanceConfig? = …

現時点ではサポートされていません。最新のステータスについては、b/288084801 をご覧ください。

velocityThreshold: Dp = 125.dp

AnchoredDraggable コンストラクタに渡される