引用现成的库实现九宫格拖拽排序功能
This commit is contained in:
@@ -13,11 +13,11 @@ android {
|
|||||||
applicationId = "com.memory.app"
|
applicationId = "com.memory.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 76
|
versionCode = 87
|
||||||
versionName = "1.9.2"
|
versionName = "2.0.3"
|
||||||
|
|
||||||
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
|
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
|
||||||
buildConfigField("int", "VERSION_CODE", "76")
|
buildConfigField("int", "VERSION_CODE", "87")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -91,6 +91,9 @@ dependencies {
|
|||||||
// DataStore
|
// DataStore
|
||||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||||
|
|
||||||
|
// Reorderable - 拖拽排序
|
||||||
|
implementation("sh.calvin.reorderable:reorderable:2.4.0")
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
package com.memory.app.ui.components
|
package com.memory.app.ui.components
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.core.AnimationVector2D
|
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.VectorConverter
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -22,21 +19,15 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.layout.positionInParent
|
|
||||||
import androidx.compose.ui.unit.IntSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.memory.app.ui.theme.Brand500
|
import com.memory.app.ui.theme.Brand500
|
||||||
import kotlinx.coroutines.launch
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyGridState
|
||||||
|
|
||||||
sealed class MediaItemData {
|
sealed class MediaItemData {
|
||||||
abstract val id: String
|
abstract val id: String
|
||||||
@@ -45,7 +36,6 @@ sealed class MediaItemData {
|
|||||||
data class Video(val uri: Uri, override val id: String = uri.toString()) : MediaItemData()
|
data class Video(val uri: Uri, override val id: String = uri.toString()) : MediaItemData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReorderableMediaGrid(
|
fun ReorderableMediaGrid(
|
||||||
items: List<MediaItemData>,
|
items: List<MediaItemData>,
|
||||||
@@ -57,12 +47,6 @@ fun ReorderableMediaGrid(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var mediaItems by remember(items) { mutableStateOf(items) }
|
var mediaItems by remember(items) { mutableStateOf(items) }
|
||||||
var draggedIndex by remember { mutableStateOf<Int?>(null) }
|
|
||||||
var draggedItemOffset by remember { mutableStateOf(Offset.Zero) }
|
|
||||||
|
|
||||||
// 记录每个项目的目标位置偏移(用于动画)
|
|
||||||
val itemOffsets = remember { mutableStateMapOf<String, Animatable<Offset, AnimationVector2D>>() }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
// 同步外部变化
|
// 同步外部变化
|
||||||
LaunchedEffect(items) {
|
LaunchedEffect(items) {
|
||||||
@@ -74,177 +58,68 @@ fun ReorderableMediaGrid(
|
|||||||
if (mediaItems.isEmpty()) return
|
if (mediaItems.isEmpty()) return
|
||||||
|
|
||||||
val columns = if (mediaItems.size <= 3) mediaItems.size else 3
|
val columns = if (mediaItems.size <= 3) mediaItems.size else 3
|
||||||
val rows = (mediaItems.size + columns - 1) / columns
|
|
||||||
val itemSize = 90.dp
|
|
||||||
val spacing = 8.dp
|
|
||||||
val itemSizePx = itemSize.value + spacing.value
|
|
||||||
|
|
||||||
Column(
|
val lazyGridState = rememberLazyGridState()
|
||||||
modifier = modifier,
|
val reorderableState = rememberReorderableLazyGridState(lazyGridState) { from, to ->
|
||||||
verticalArrangement = Arrangement.spacedBy(spacing)
|
mediaItems = mediaItems.toMutableList().apply {
|
||||||
|
add(to.index, removeAt(from.index))
|
||||||
|
}
|
||||||
|
onItemsReordered(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(columns),
|
||||||
|
state = lazyGridState,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier
|
||||||
|
.heightIn(max = 300.dp)
|
||||||
) {
|
) {
|
||||||
for (row in 0 until rows) {
|
items(mediaItems, key = { it.id }) { item ->
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing)) {
|
ReorderableItem(reorderableState, key = item.id) { isDragging ->
|
||||||
for (col in 0 until columns) {
|
val index = mediaItems.indexOf(item)
|
||||||
val index = row * columns + col
|
val progress = uploadProgress[index]
|
||||||
if (index < mediaItems.size) {
|
|
||||||
val item = mediaItems[index]
|
MediaItemCard(
|
||||||
val progress = uploadProgress[index]
|
item = item,
|
||||||
val isDragging = draggedIndex == index
|
isDragging = isDragging,
|
||||||
|
onRemove = if (!isLoading && onRemove != null) {
|
||||||
// 为每个项目创建动画偏移
|
{ onRemove(item) }
|
||||||
val animatedOffset = itemOffsets.getOrPut(item.id) {
|
} else null,
|
||||||
Animatable(Offset.Zero, Offset.VectorConverter)
|
onClick = if (onClick != null) {
|
||||||
}
|
{ onClick(item) }
|
||||||
|
} else null,
|
||||||
ReorderableMediaItem(
|
uploadProgress = progress,
|
||||||
item = item,
|
modifier = Modifier
|
||||||
index = index,
|
.aspectRatio(1f)
|
||||||
isDragging = isDragging,
|
.longPressDraggableHandle(
|
||||||
dragOffset = if (isDragging) draggedItemOffset else Offset.Zero,
|
enabled = !isLoading,
|
||||||
animatedOffset = if (!isDragging) animatedOffset.value else Offset.Zero,
|
onDragStarted = {},
|
||||||
onRemove = if (!isLoading && onRemove != null) {
|
onDragStopped = {}
|
||||||
{ onRemove(item) }
|
|
||||||
} else null,
|
|
||||||
onClick = if (onClick != null) {
|
|
||||||
{ onClick(item) }
|
|
||||||
} else null,
|
|
||||||
onDragStart = {
|
|
||||||
if (!isLoading) {
|
|
||||||
draggedIndex = index
|
|
||||||
draggedItemOffset = Offset.Zero
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDrag = { offset ->
|
|
||||||
val currentIndex = draggedIndex ?: return@ReorderableMediaItem
|
|
||||||
draggedItemOffset += offset
|
|
||||||
|
|
||||||
// 计算拖拽项当前的行列位置
|
|
||||||
val currentRow = currentIndex / columns
|
|
||||||
val currentCol = currentIndex % columns
|
|
||||||
|
|
||||||
// 直接基于偏移量判断是否应该交换,而不是先计算 targetCol
|
|
||||||
// 这样可以避免刚越过边界就触发交换的问题
|
|
||||||
|
|
||||||
// 计算应该交换到哪个方向
|
|
||||||
val colOffset = when {
|
|
||||||
draggedItemOffset.x > itemSizePx * 0.7f -> 1 // 向右超过 70%
|
|
||||||
draggedItemOffset.x < -itemSizePx * 0.7f -> -1 // 向左超过 70%
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val rowOffset = when {
|
|
||||||
draggedItemOffset.y > itemSizePx * 0.7f -> 1 // 向下超过 70%
|
|
||||||
draggedItemOffset.y < -itemSizePx * 0.7f -> -1 // 向上超过 70%
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有在超过阈值时才计算目标位置
|
|
||||||
if (colOffset != 0 || rowOffset != 0) {
|
|
||||||
val targetCol = currentCol + colOffset
|
|
||||||
val targetRow = currentRow + rowOffset
|
|
||||||
val targetIndex = targetRow * columns + targetCol
|
|
||||||
|
|
||||||
// 检查目标索引是否有效
|
|
||||||
if (targetIndex in mediaItems.indices && targetIndex != currentIndex) {
|
|
||||||
// 交换位置
|
|
||||||
val newList = mediaItems.toMutableList()
|
|
||||||
val draggedItem = newList[currentIndex]
|
|
||||||
val targetItem = newList[targetIndex]
|
|
||||||
|
|
||||||
newList[currentIndex] = targetItem
|
|
||||||
newList[targetIndex] = draggedItem
|
|
||||||
|
|
||||||
// 计算被交换项目需要移动的偏移量
|
|
||||||
val swapOffset = Offset(
|
|
||||||
(currentCol - targetCol) * itemSizePx,
|
|
||||||
(currentRow - targetRow) * itemSizePx
|
|
||||||
)
|
|
||||||
|
|
||||||
// 启动动画 - 使用更慢的动画参数
|
|
||||||
scope.launch {
|
|
||||||
val targetItemAnimatable = itemOffsets.getOrPut(targetItem.id) {
|
|
||||||
Animatable(Offset.Zero, Offset.VectorConverter)
|
|
||||||
}
|
|
||||||
targetItemAnimatable.snapTo(swapOffset)
|
|
||||||
targetItemAnimatable.animateTo(
|
|
||||||
Offset.Zero,
|
|
||||||
animationSpec = spring(
|
|
||||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
|
||||||
stiffness = Spring.StiffnessLow
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaItems = newList
|
|
||||||
onItemsReordered(newList)
|
|
||||||
|
|
||||||
// 更新拖拽索引,重置偏移量为 0
|
|
||||||
// 从新位置重新开始计算,避免连续快速交换
|
|
||||||
draggedIndex = targetIndex
|
|
||||||
draggedItemOffset = Offset.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragEnd = {
|
|
||||||
draggedIndex = null
|
|
||||||
draggedItemOffset = Offset.Zero
|
|
||||||
},
|
|
||||||
uploadProgress = progress
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReorderableMediaItem(
|
private fun MediaItemCard(
|
||||||
item: MediaItemData,
|
item: MediaItemData,
|
||||||
index: Int,
|
|
||||||
isDragging: Boolean,
|
isDragging: Boolean,
|
||||||
dragOffset: Offset,
|
|
||||||
animatedOffset: Offset,
|
|
||||||
onRemove: (() -> Unit)?,
|
onRemove: (() -> Unit)?,
|
||||||
onClick: (() -> Unit)?,
|
onClick: (() -> Unit)?,
|
||||||
onDragStart: () -> Unit,
|
uploadProgress: Float?,
|
||||||
onDrag: (Offset) -> Unit,
|
modifier: Modifier = Modifier
|
||||||
onDragEnd: () -> Unit,
|
|
||||||
uploadProgress: Float? = null
|
|
||||||
) {
|
) {
|
||||||
// 判断是否正在动画中
|
|
||||||
val isAnimating = animatedOffset != Offset.Zero
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.size(90.dp)
|
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
// 使用 graphicsLayer 进行硬件加速的变换
|
|
||||||
scaleX = if (isDragging) 1.05f else 1f
|
scaleX = if (isDragging) 1.05f else 1f
|
||||||
scaleY = if (isDragging) 1.05f else 1f
|
scaleY = if (isDragging) 1.05f else 1f
|
||||||
translationX = dragOffset.x + animatedOffset.x
|
|
||||||
translationY = dragOffset.y + animatedOffset.y
|
|
||||||
shadowElevation = if (isDragging) 8.dp.toPx() else 0f
|
shadowElevation = if (isDragging) 8.dp.toPx() else 0f
|
||||||
}
|
}
|
||||||
.zIndex(
|
.zIndex(if (isDragging) 1f else 0f)
|
||||||
when {
|
|
||||||
isDragging -> 2f // 拖拽项最高层级
|
|
||||||
isAnimating -> 1f // 动画项次高层级,避免被其他项遮挡
|
|
||||||
else -> 0f // 普通项最低层级
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectDragGesturesAfterLongPress(
|
|
||||||
onDragStart = { onDragStart() },
|
|
||||||
onDrag = { change, dragAmount ->
|
|
||||||
change.consume()
|
|
||||||
onDrag(dragAmount)
|
|
||||||
},
|
|
||||||
onDragEnd = { onDragEnd() },
|
|
||||||
onDragCancel = { onDragEnd() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.then(
|
.then(
|
||||||
if (onClick != null && uploadProgress == null) {
|
if (onClick != null && uploadProgress == null) {
|
||||||
Modifier.clickable { onClick() }
|
Modifier.clickable { onClick() }
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
||||||
@@ -61,6 +64,8 @@ import com.memory.app.ui.components.MusicCard
|
|||||||
import com.memory.app.ui.components.VideoPlayer
|
import com.memory.app.ui.components.VideoPlayer
|
||||||
import com.memory.app.ui.theme.*
|
import com.memory.app.ui.theme.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyGridState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditPostScreen(
|
fun EditPostScreen(
|
||||||
@@ -491,7 +496,6 @@ private sealed class EditMediaItem {
|
|||||||
data class NewVideo(val uri: Uri, override val id: String = uri.toString()) : EditMediaItem()
|
data class NewVideo(val uri: Uri, override val id: String = uri.toString()) : EditMediaItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditReorderableMediaGrid(
|
private fun EditReorderableMediaGrid(
|
||||||
items: List<EditMediaItem>,
|
items: List<EditMediaItem>,
|
||||||
@@ -502,12 +506,6 @@ private fun EditReorderableMediaGrid(
|
|||||||
isLoading: Boolean = false
|
isLoading: Boolean = false
|
||||||
) {
|
) {
|
||||||
var mediaItems by remember(items) { mutableStateOf(items) }
|
var mediaItems by remember(items) { mutableStateOf(items) }
|
||||||
var draggedIndex by remember { mutableStateOf<Int?>(null) }
|
|
||||||
var draggedItemOffset by remember { mutableStateOf(Offset.Zero) }
|
|
||||||
|
|
||||||
// 记录每个项目的目标位置偏移(用于动画)
|
|
||||||
val itemOffsets = remember { mutableStateMapOf<String, Animatable<Offset, AnimationVector2D>>() }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(items) {
|
LaunchedEffect(items) {
|
||||||
if (items != mediaItems) {
|
if (items != mediaItems) {
|
||||||
@@ -518,174 +516,67 @@ private fun EditReorderableMediaGrid(
|
|||||||
if (mediaItems.isEmpty()) return
|
if (mediaItems.isEmpty()) return
|
||||||
|
|
||||||
val columns = if (mediaItems.size <= 3) mediaItems.size else 3
|
val columns = if (mediaItems.size <= 3) mediaItems.size else 3
|
||||||
val rows = (mediaItems.size + columns - 1) / columns
|
|
||||||
val itemSize = 90.dp
|
|
||||||
val spacing = 8.dp
|
|
||||||
val itemSizePx = itemSize.value + spacing.value
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
|
val lazyGridState = rememberLazyGridState()
|
||||||
for (row in 0 until rows) {
|
val reorderableState = rememberReorderableLazyGridState(lazyGridState) { from, to ->
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing)) {
|
mediaItems = mediaItems.toMutableList().apply {
|
||||||
for (col in 0 until columns) {
|
add(to.index, removeAt(from.index))
|
||||||
val index = row * columns + col
|
}
|
||||||
if (index < mediaItems.size) {
|
onItemsReordered(mediaItems)
|
||||||
val item = mediaItems[index]
|
}
|
||||||
val progress = uploadProgress[index]
|
|
||||||
val isDragging = draggedIndex == index
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(columns),
|
||||||
// 为每个项目创建动画偏移
|
state = lazyGridState,
|
||||||
val animatedOffset = itemOffsets.getOrPut(item.id) {
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
Animatable(Offset.Zero, Offset.VectorConverter)
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
}
|
modifier = Modifier.heightIn(max = 300.dp)
|
||||||
|
) {
|
||||||
EditReorderableMediaItem(
|
items(mediaItems, key = { it.id }) { item ->
|
||||||
item = item,
|
ReorderableItem(reorderableState, key = item.id) { isDragging ->
|
||||||
index = index,
|
val index = mediaItems.indexOf(item)
|
||||||
isDragging = isDragging,
|
val progress = uploadProgress[index]
|
||||||
dragOffset = if (isDragging) draggedItemOffset else Offset.Zero,
|
|
||||||
animatedOffset = if (!isDragging) animatedOffset.value else Offset.Zero,
|
EditMediaItemCard(
|
||||||
onRemove = if (!isLoading && onRemove != null) {
|
item = item,
|
||||||
{ onRemove(item) }
|
isDragging = isDragging,
|
||||||
} else null,
|
onRemove = if (!isLoading && onRemove != null) {
|
||||||
onClick = if (onClick != null) {
|
{ onRemove(item) }
|
||||||
{ onClick(item) }
|
} else null,
|
||||||
} else null,
|
onClick = if (onClick != null) {
|
||||||
onDragStart = {
|
{ onClick(item) }
|
||||||
if (!isLoading) {
|
} else null,
|
||||||
draggedIndex = index
|
uploadProgress = progress,
|
||||||
draggedItemOffset = Offset.Zero
|
modifier = Modifier
|
||||||
}
|
.aspectRatio(1f)
|
||||||
},
|
.longPressDraggableHandle(
|
||||||
onDrag = { offset ->
|
enabled = !isLoading,
|
||||||
val currentIndex = draggedIndex ?: return@EditReorderableMediaItem
|
onDragStarted = {},
|
||||||
draggedItemOffset += offset
|
onDragStopped = {}
|
||||||
|
|
||||||
// 计算拖拽项当前的行列位置
|
|
||||||
val currentRow = currentIndex / columns
|
|
||||||
val currentCol = currentIndex % columns
|
|
||||||
|
|
||||||
// 直接基于偏移量判断是否应该交换,而不是先计算 targetCol
|
|
||||||
// 这样可以避免刚越过边界就触发交换的问题
|
|
||||||
|
|
||||||
// 计算应该交换到哪个方向
|
|
||||||
val colOffset = when {
|
|
||||||
draggedItemOffset.x > itemSizePx * 0.7f -> 1 // 向右超过 70%
|
|
||||||
draggedItemOffset.x < -itemSizePx * 0.7f -> -1 // 向左超过 70%
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val rowOffset = when {
|
|
||||||
draggedItemOffset.y > itemSizePx * 0.7f -> 1 // 向下超过 70%
|
|
||||||
draggedItemOffset.y < -itemSizePx * 0.7f -> -1 // 向上超过 70%
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有在超过阈值时才计算目标位置
|
|
||||||
if (colOffset != 0 || rowOffset != 0) {
|
|
||||||
val targetCol = currentCol + colOffset
|
|
||||||
val targetRow = currentRow + rowOffset
|
|
||||||
val targetIndex = targetRow * columns + targetCol
|
|
||||||
|
|
||||||
// 检查目标索引是否有效
|
|
||||||
if (targetIndex in mediaItems.indices && targetIndex != currentIndex) {
|
|
||||||
// 交换位置
|
|
||||||
val newList = mediaItems.toMutableList()
|
|
||||||
val draggedItem = newList[currentIndex]
|
|
||||||
val targetItem = newList[targetIndex]
|
|
||||||
|
|
||||||
newList[currentIndex] = targetItem
|
|
||||||
newList[targetIndex] = draggedItem
|
|
||||||
|
|
||||||
// 计算被交换项目需要移动的偏移量
|
|
||||||
val swapOffset = Offset(
|
|
||||||
(currentCol - targetCol) * itemSizePx,
|
|
||||||
(currentRow - targetRow) * itemSizePx
|
|
||||||
)
|
|
||||||
|
|
||||||
// 启动动画 - 使用更慢的动画参数
|
|
||||||
scope.launch {
|
|
||||||
val targetItemAnimatable = itemOffsets.getOrPut(targetItem.id) {
|
|
||||||
Animatable(Offset.Zero, Offset.VectorConverter)
|
|
||||||
}
|
|
||||||
targetItemAnimatable.snapTo(swapOffset)
|
|
||||||
targetItemAnimatable.animateTo(
|
|
||||||
Offset.Zero,
|
|
||||||
animationSpec = spring(
|
|
||||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
|
||||||
stiffness = Spring.StiffnessLow
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaItems = newList
|
|
||||||
onItemsReordered(newList)
|
|
||||||
|
|
||||||
// 更新拖拽索引,重置偏移量为 0
|
|
||||||
// 从新位置重新开始计算,避免连续快速交换
|
|
||||||
draggedIndex = targetIndex
|
|
||||||
draggedItemOffset = Offset.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragEnd = {
|
|
||||||
draggedIndex = null
|
|
||||||
draggedItemOffset = Offset.Zero
|
|
||||||
},
|
|
||||||
uploadProgress = progress
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditReorderableMediaItem(
|
private fun EditMediaItemCard(
|
||||||
item: EditMediaItem,
|
item: EditMediaItem,
|
||||||
index: Int,
|
|
||||||
isDragging: Boolean,
|
isDragging: Boolean,
|
||||||
dragOffset: Offset,
|
|
||||||
animatedOffset: Offset,
|
|
||||||
onRemove: (() -> Unit)?,
|
onRemove: (() -> Unit)?,
|
||||||
onClick: (() -> Unit)?,
|
onClick: (() -> Unit)?,
|
||||||
onDragStart: () -> Unit,
|
uploadProgress: Float?,
|
||||||
onDrag: (Offset) -> Unit,
|
modifier: Modifier = Modifier
|
||||||
onDragEnd: () -> Unit,
|
|
||||||
uploadProgress: Float? = null
|
|
||||||
) {
|
) {
|
||||||
// 判断是否正在动画中
|
|
||||||
val isAnimating = animatedOffset != Offset.Zero
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.size(90.dp)
|
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
// 使用 graphicsLayer 进行硬件加速的变换
|
|
||||||
scaleX = if (isDragging) 1.05f else 1f
|
scaleX = if (isDragging) 1.05f else 1f
|
||||||
scaleY = if (isDragging) 1.05f else 1f
|
scaleY = if (isDragging) 1.05f else 1f
|
||||||
translationX = dragOffset.x + animatedOffset.x
|
|
||||||
translationY = dragOffset.y + animatedOffset.y
|
|
||||||
shadowElevation = if (isDragging) 8.dp.toPx() else 0f
|
shadowElevation = if (isDragging) 8.dp.toPx() else 0f
|
||||||
}
|
}
|
||||||
.zIndex(
|
.zIndex(if (isDragging) 1f else 0f)
|
||||||
when {
|
|
||||||
isDragging -> 2f // 拖拽项最高层级
|
|
||||||
isAnimating -> 1f // 动画项次高层级,避免被其他项遮挡
|
|
||||||
else -> 0f // 普通项最低层级
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectDragGesturesAfterLongPress(
|
|
||||||
onDragStart = { onDragStart() },
|
|
||||||
onDrag = { change, dragAmount ->
|
|
||||||
change.consume()
|
|
||||||
onDrag(dragAmount)
|
|
||||||
},
|
|
||||||
onDragEnd = { onDragEnd() },
|
|
||||||
onDragCancel = { onDragEnd() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.then(
|
.then(
|
||||||
if (onClick != null && uploadProgress == null) {
|
if (onClick != null && uploadProgress == null) {
|
||||||
Modifier.clickable { onClick() }
|
Modifier.clickable { onClick() }
|
||||||
|
|||||||
Reference in New Issue
Block a user