外部使用
setContent {
Box(modifier = Modifier.fillMaxSize()){
ThreePointParent(modifier = Modifier.align(Alignment.Center))
}
}
package com.example.composetest.threePoint
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import android.util.Log // 導(dǎo)入 Log 庫
private const val TAG = "RealTimeDrag"
/**
* 存儲(chǔ)可拖拽子項(xiàng)的狀態(tài)
*/
data class DraggableItem(
val id: Int,
val originalIndex: Int,
var currentOrder: Int,
var offsetPx: Float = 0f, // 視覺偏移量,用于讓位/歸位動(dòng)畫目標(biāo)
var zIndex: Float = 0f // Z軸深度,用于拖拽時(shí)懸浮
)
@Composable
fun ThreePointParent(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.width(1500.dp)
.height(800.dp)
.background(Color.Gray.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
ThreeFingerDraggableRow(itemCount = 3) { index ->
ThreePointView(index)
}
}
}
@Composable
fun ThreeFingerDraggableRow(
itemCount: Int,
content: @Composable (index: Int) -> Unit
) {
val itemList = remember {
mutableStateListOf<DraggableItem>().apply {
repeat(itemCount) { index ->
add(DraggableItem(id = index, originalIndex = index, currentOrder = index))
}
}
}
var draggingItemRef by remember { mutableStateOf<DraggableItem?>(null) }
var rowWidth by remember { mutableStateOf(0) }
var currentDragOffsetX by remember { mutableFloatStateOf(0f) }
var logicalOrderOffset by remember { mutableIntStateOf(0) }
val itemWidthPx = if (rowWidth > 0 && itemCount > 0) (rowWidth.toFloat() / itemCount) else 0f
// --- 輔助 Lambda 函數(shù) (添加 Log) ---
val startDrag: (DraggableItem) -> Unit = { item ->
draggingItemRef = item
currentDragOffsetX = 0f
logicalOrderOffset = 0
item.zIndex = 1f
Log.d(TAG, "DragStart: Item ${item.id} (${item.currentOrder}) - Three fingers recognized.")
}
val endDrag: (DraggableItem?) -> Unit = { item ->
val isCancelled = (item == null) // 通過傳入 null 判斷是否是 Cancel
if (isCancelled) {
Log.d(TAG, "DragCancel: Item reset.")
} else {
item?.let { dragItem ->
val finalRelativeOffset = currentDragOffsetX - (logicalOrderOffset * itemWidthPx)
dragItem.offsetPx = finalRelativeOffset
dragItem.offsetPx = 0f
dragItem.zIndex = 0f
Log.d(TAG, "DragEnd: Item ${dragItem.id} settled. FinalOffset=${finalRelativeOffset}")
}
}
currentDragOffsetX = 0f
logicalOrderOffset = 0
draggingItemRef = null
}
// --- Row 布局 ---
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(Color.DarkGray)
.onGloballyPositioned { coordinates ->
rowWidth = coordinates.size.width
}
) {
itemList.sortedBy { it.currentOrder }.forEach { item ->
val isDragging = item == draggingItemRef
val targetOffset = item.offsetPx
val animatedOffsetX by animateFloatAsState(
targetValue = targetOffset,
animationSpec = tween(durationMillis = 3000),
label = "ItemOffsetAnimation"
)
val dragModifier = Modifier.pointerInput(item) {
awaitPointerEventScope {
while (true) {
// 1. 等待首次按下事件
val down = awaitFirstDown()
Log.v(TAG, "Pointer Down Detected. ID: ${down.id}")
// 2. *** 關(guān)鍵修改:循環(huán)等待三指按下 ***
var event = currentEvent
var touchCount = event.changes.filter { it.pressed }.size
// 循環(huán)等待,直到觸摸點(diǎn)達(dá)到 3 個(gè),或者所有手指抬起
while (touchCount < 3) {
val nextEvent = awaitPointerEvent()
event = nextEvent // 更新事件
touchCount = nextEvent.changes.filter { it.pressed }.size
Log.v(TAG, "Waiting for 3 fingers. Current Count: ${touchCount}")
// 如果所有手指都抬起了,跳出等待,重新開始
if (touchCount == 0) break
}
Log.v(TAG, "Final Check Touch Count: ${touchCount}")
if (touchCount >= 3) {
// 3. 三指識(shí)別成功,啟動(dòng)拖拽
startDrag(item)
// 4. 進(jìn)入拖拽循環(huán)
var dragCanceled = false
try {
do {
// 獲取下一個(gè)指針事件
event = awaitPointerEvent()
// 找到第一個(gè)按下并移動(dòng)的指針 (ID 必須與 down.id 匹配)
val dragPointer = event.changes.firstOrNull { it.id == down.id && it.pressed }
if (dragPointer != null) {
val movement = dragPointer.positionChange()
dragPointer.consume()
if (movement != Offset.Zero) {
val dragAmountX = movement.x
// --- 拖拽邏輯 ---
draggingItemRef?.let { dragItem ->
currentDragOffsetX += dragAmountX
val relativeOffset = currentDragOffsetX - (logicalOrderOffset * itemWidthPx)
val correctionDeltaOrder = checkAndSwapRealTime(
dragItem,
itemList,
itemWidthPx,
relativeOffset
)
if (correctionDeltaOrder != 0) {
logicalOrderOffset += correctionDeltaOrder
Log.d(TAG, "SWAP: Item ${dragItem.id} swapped. NewOrder=${dragItem.currentOrder}, NewLogicalOffset=${logicalOrderOffset}")
}
Log.v(TAG, "Dragging: currentCx=$currentDragOffsetX, relativeOffset=$relativeOffset")
}
// --- 拖拽邏輯結(jié)束 ---
}
}
// 消耗所有未被處理的事件
event.changes.forEach { if (!it.isConsumed) it.consume() }
// 拖拽持續(xù)條件:至少有一個(gè)指針仍在按下
} while (event.changes.any { it.pressed })
} catch (e: Exception) {
Log.e(TAG, "Drag loop interrupted.", e)
dragCanceled = true
} finally {
if (dragCanceled) {
endDrag(null) // onDragCancel
} else {
endDrag(item) // onDragEnd
}
}
} else {
// 觸摸點(diǎn)數(shù)不足 3 個(gè),或已抬起,消耗事件并等待下一個(gè) down 事件
event.changes.forEach { if (!it.isConsumed) it.consume() }
}
}
}
}
// ... (Item 渲染 Box 保持不變)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.zIndex(item.zIndex)
.then(dragModifier)
.graphicsLayer {
translationX = when {
isDragging -> currentDragOffsetX
else -> animatedOffsetX
}
},
contentAlignment = Alignment.Center
) {
content(item.originalIndex)
}
}
}
}
// ... (checkAndSwapRealTime 和 ThreePointView 保持不變)
fun checkAndSwapRealTime(
draggingItem: DraggableItem,
itemList: SnapshotStateList<DraggableItem>,
itemWidthPx: Float,
relativeOffset: Float
): Int {
if (itemWidthPx == 0f) return 0
val currentOrder = draggingItem.currentOrder
val halfWidth = itemWidthPx / 2f
var correctionDeltaOrder = 0
// --- 向右拖拽 ---
if (relativeOffset > halfWidth && currentOrder < itemList.size - 1) {
val neighbor = itemList.find { it.currentOrder == currentOrder + 1 }
if (neighbor != null) {
neighbor.offsetPx = -itemWidthPx
neighbor.currentOrder = currentOrder
draggingItem.currentOrder = currentOrder + 1
neighbor.offsetPx = 0f
correctionDeltaOrder = 1
}
}
// --- 向左拖拽 ---
else if (relativeOffset < -halfWidth && currentOrder > 0) {
val neighbor = itemList.find { it.currentOrder == currentOrder - 1 }
if (neighbor != null) {
neighbor.offsetPx = itemWidthPx
neighbor.currentOrder = currentOrder
draggingItem.currentOrder = currentOrder - 1
neighbor.offsetPx = 0f
correctionDeltaOrder = -1
}
}
return correctionDeltaOrder
}
@Composable
fun ThreePointView(index: Int){
val colors = listOf(Color.Red, Color.Green, Color.Blue)
Box(
modifier = Modifier
.fillMaxSize()
.background(colors.getOrElse(index) { Color.Gray })
,
contentAlignment = Alignment.Center
){
Text(
text = "第${index}個(gè)",
fontSize = 30.sp,
color = Color.White
)
}
}
@Preview(showBackground = true)
@Composable
fun PreviewDraggableRow() {
ThreePointParent()
}