JetpackComposeで横スワイプで消えるカード型Layoutを作ってみる

こんなのです。スワイプというよりはドラッグ。
![Something went wrong]()

Qiitaの制限で、今月は画像があげられないようなので以下Readmeにもgifを貼ってます

コード

https://github.com/kkhouse/Playground/tree/main/AndroidProject/animation/GestureAndRotation

### ざっくり解説

ドラッグ操作を状態として追跡して、animate*asStateでアニメーションする。

```kotlin
CardLayout(
    modifier = Modifier
        .offset {
            IntOffset(
                animatedOffsetX.roundToInt(),
                animatedOffsetY.roundToInt()
            )
        }
        .pointerInput(Unit) {
            detectDragGestures(
                onDrag = { change, dragAmount ->
                    change.consume()
                    draggableState = draggableState.copy(
                        offset = Offset(
                            x = draggableState.offset.x + dragAmount.x,
                            y = draggableState.offset.y + dragAmount.y
                        ),
                        rotateDegree = draggableState.createAngleDelta(cardX),
                        swipingState = createSwipingState(
                            cardX,
                            draggableState.offset.x + dragAmount.x
                        )
                    )
                },
                onDragEnd = {
                    draggableState = draggableState.copy(
                        isDragEnded = true,
                        offset = when (abs(draggableState.offset.x) < 1000f) {
                            true -> Offset(x = cardX, y = cardY)
                            else -> when (draggableState.swipingState.isRightSwiping()) {
                                true -> Offset(
                                    x = draggableState.offset.x + 500f,
                                    y = draggableState.offset.y + 500f
                                )
                                else -> Offset(
                                    x = draggableState.offset.x - 500f,
                                    y = draggableState.offset.y + 500f
                                )
                            }
                        },
                        rotateDegree = 0f,
                        swipingState = SwipingState.CENTER,
                    )
                }
            )
        }
        .graphicsLayer {
            rotationZ = animateRotateDegree
        }
        .size(
            width = DraggableCardConst.cardWidth,
            height = DraggableCardConst.cardHeight
        ),
    textState = draggableState.createCardLayoutState(cardX)
)
```

#### 状態管理

制御すべき値が多いので、Stateクラスで管理。
offsetが現在のスワイプ量
rotateDegreeがCard Layoutの角度

```kotlin
private data class DraggableCardState(
    val offset: Offset = Offset(x = 0f, y = 0f),
    val isDragEnded: Boolean = false,
    val rotateDegree: Float = 0f,
    val swipingCardText: SwipingCardTextState = SwipingCardTextState(),
    val swipingState: SwipingState = SwipingState.CENTER
) {

    fun createSwipeDistance(cardX: Float): Float {
        return when (swipingState.isRightSwiping()) {
            true -> offset.x - cardX
            else -> cardX - offset.x
        }
    }
    fun createAngleRatio(cardX: Float) : Float {
        return when(swipingState.isRightSwiping()) {
            true -> createSwipeDistance(cardX).coerceIn(0f, maxAngleSwipeDistance).unaryMinus() / maxAngleSwipeDistance
            else -> createSwipeDistance(cardX).coerceIn(0f, maxAngleSwipeDistance) / maxAngleSwipeDistance
        }
    }
    fun createAngleDelta(cardX: Float): Float {
        return createAngleRatio(cardX) * maxAngle.div(2)
    }

    fun createCardLayoutState(cardX: Float): SwipingCardTextState {
        val alpha = abs(createAngleDelta(cardX) / maxAngle).plus(0.4f) // default alpha value
        return when(swipingState) {
            SwipingState.RIGHT -> SwipingCardTextState(
                swipingState = SwipingState.RIGHT, alpha = alpha, alphaText = "RIGHT"
            )
            SwipingState.LEFT -> SwipingCardTextState(
                swipingState = SwipingState.LEFT, alpha = alpha, alphaText = "LEFT"
            )
            SwipingState.CENTER -> SwipingCardTextState(
                swipingState = SwipingState.CENTER, alpha = 0f, alphaText = ""
            )
        }
    }
}
```


##### ドラック時のOffset値の取得
以下にて行える
https://developer.android.com/jetpack/compose/gestures?hl=ja#dragging

```kotlin
.pointerInput(Unit) {
            detectDragGestures(
                onDrag = { change, dragAmount ->
                    change.consume()
                    draggableState = draggableState.copy(
                        offset = Offset(
                            x = draggableState.offset.x + dragAmount.x,
                            y = draggableState.offset.y + dragAmount.y
                        ),
                        rotateDegree = draggableState.createAngleDelta(cardX),
                        swipingState = createSwipingState(
                            cardX,
                            draggableState.offset.x + dragAmount.x
                        )
                    )
                }

```

実際にViewを動かすには、Viewの`Modifier.offset`に取得したoffset値を渡す

```kotlin
.offset {
            IntOffset(
                animatedOffsetX.roundToInt(),
                animatedOffsetY.roundToInt()
            )
        }
```


offset値のアニメーションはanimateFloatAsStateで

https://developer.android.com/jetpack/compose/animation?hl=ja#animate-as-state


```kotlin
val animatedOffsetX by animateFloatAsState(
    targetValue = draggableState.offset.x,
    animationSpec = if(draggableState.isDragEnded) tween(durationMillis = 500) else spring(),//default
    finishedListener = { draggableState = draggableState.copy(isDragEnded = false) }
)
val animatedOffsetY by animateFloatAsState(
    targetValue = draggableState.offset.y,
    animationSpec = if(draggableState.isDragEnded) tween(durationMillis = 500) else spring()//default
)
```



##### 角度計算

スワイプによって傾くように制御するので、スワイプ幅と角度をマッピングする必要がある。
以下Stateクラスにて、スワイプ量/最大スワイプ量 の比率を計算して、角度を求めている。

```kotlin
private data class DraggableCardState(
    val offset: Offset = Offset(x = 0f, y = 0f),
    val isDragEnded: Boolean = false,
    val rotateDegree: Float = 0f,
    val swipingCardText: SwipingCardTextState = SwipingCardTextState(),
    val swipingState: SwipingState = SwipingState.CENTER
) {

    fun createSwipeDistance(cardX: Float): Float {
        return when (swipingState.isRightSwiping()) {
            true -> offset.x - cardX
            else -> cardX - offset.x
        }
    }
    fun createAngleRatio(cardX: Float) : Float {
        return when(swipingState.isRightSwiping()) {
            true -> createSwipeDistance(cardX).coerceIn(0f, maxAngleSwipeDistance).unaryMinus() / maxAngleSwipeDistance
            else -> createSwipeDistance(cardX).coerceIn(0f, maxAngleSwipeDistance) / maxAngleSwipeDistance
        }
    }
    fun createAngleDelta(cardX: Float): Float {
        return createAngleRatio(cardX) * maxAngle.div(2)
    }

```

上記角度をアニメーションでラップして、graphicsLayerのrotationZに適応
graphicsLayerは以下参考

https://medium.com/mobile-app-development-publication/have-fun-with-jetpack-compose-graphicslayer-modifier-e39c12a4791f

```kotlin
.graphicsLayer { rotationZ = animateRotateDegree }
```


#### Drag終了後に、元に戻すか、画面から消すか

onDragEndでドラッグの終了を拾える。
1000f を超えたら消す、それ以外は元に戻す。
`draggableState.offset.x + 500f` この辺りで画面外に消している。

```kotlin
onDragEnd = {
    draggableState = draggableState.copy(
        isDragEnded = true,
        offset = when (abs(draggableState.offset.x) < 1000f) {
            true -> Offset(x = cardX, y = cardY)
            else -> when (draggableState.swipingState.isRightSwiping()) {
                true -> Offset(
                    x = draggableState.offset.x + 500f,
                    y = draggableState.offset.y + 500f
                )
                else -> Offset(
                    x = draggableState.offset.x - 500f,
                    y = draggableState.offset.y + 500f
                )
            }
        },
        rotateDegree = 0f,
        swipingState = SwipingState.CENTER,
    )
}

```