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