LoginSignup
0

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

Posted at

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

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

コード

ざっくり解説

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

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の角度

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

.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値を渡す

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

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

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クラスにて、スワイプ量/最大スワイプ量 の比率を計算して、角度を求めている。

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は以下参考

.graphicsLayer { rotationZ = animateRotateDegree }

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

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

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,
    )
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0