From 28e08cb72c7392a2717e8be302600da8d15f2a05 Mon Sep 17 00:00:00 2001 From: amos wong Date: Thu, 12 Mar 2026 20:52:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=95=E7=94=A8=E7=8E=B0=E6=88=90=E7=9A=84?= =?UTF-8?q?=E5=BA=93=E5=AE=9E=E7=8E=B0=E4=B9=9D=E5=AE=AB=E6=A0=BC=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle.kts | 9 +- .../app/ui/components/ReorderableMediaGrid.kt | 221 ++++-------------- .../memory/app/ui/screen/EditPostScreen.kt | 205 ++++------------ 3 files changed, 102 insertions(+), 333 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 3695ddc..032721a 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -13,11 +13,11 @@ android { applicationId = "com.memory.app" minSdk = 26 targetSdk = 35 - versionCode = 76 - versionName = "1.9.2" + versionCode = 87 + versionName = "2.0.3" buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"") - buildConfigField("int", "VERSION_CODE", "76") + buildConfigField("int", "VERSION_CODE", "87") } signingConfigs { @@ -91,6 +91,9 @@ dependencies { // DataStore implementation("androidx.datastore:datastore-preferences:1.1.1") + // Reorderable - 拖拽排序 + implementation("sh.calvin.reorderable:reorderable:2.4.0") + // Debug debugImplementation("androidx.compose.ui:ui-tooling") } diff --git a/android/app/src/main/java/com/memory/app/ui/components/ReorderableMediaGrid.kt b/android/app/src/main/java/com/memory/app/ui/components/ReorderableMediaGrid.kt index 4b76fab..28f1657 100644 --- a/android/app/src/main/java/com/memory/app/ui/components/ReorderableMediaGrid.kt +++ b/android/app/src/main/java/com/memory/app/ui/components/ReorderableMediaGrid.kt @@ -1,16 +1,13 @@ package com.memory.app.ui.components 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.clickable -import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress 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.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -22,21 +19,15 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput 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.sp import androidx.compose.ui.zIndex import coil.compose.AsyncImage import com.memory.app.ui.theme.Brand500 -import kotlinx.coroutines.launch +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyGridState sealed class MediaItemData { abstract val id: String @@ -45,7 +36,6 @@ sealed class MediaItemData { data class Video(val uri: Uri, override val id: String = uri.toString()) : MediaItemData() } -@OptIn(ExperimentalFoundationApi::class) @Composable fun ReorderableMediaGrid( items: List, @@ -57,12 +47,6 @@ fun ReorderableMediaGrid( modifier: Modifier = Modifier ) { var mediaItems by remember(items) { mutableStateOf(items) } - var draggedIndex by remember { mutableStateOf(null) } - var draggedItemOffset by remember { mutableStateOf(Offset.Zero) } - - // 记录每个项目的目标位置偏移(用于动画) - val itemOffsets = remember { mutableStateMapOf>() } - val scope = rememberCoroutineScope() // 同步外部变化 LaunchedEffect(items) { @@ -74,177 +58,68 @@ fun ReorderableMediaGrid( if (mediaItems.isEmpty()) return 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( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(spacing) + val lazyGridState = rememberLazyGridState() + val reorderableState = rememberReorderableLazyGridState(lazyGridState) { from, to -> + 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) { - Row(horizontalArrangement = Arrangement.spacedBy(spacing)) { - for (col in 0 until columns) { - val index = row * columns + col - if (index < mediaItems.size) { - val item = mediaItems[index] - val progress = uploadProgress[index] - val isDragging = draggedIndex == index - - // 为每个项目创建动画偏移 - val animatedOffset = itemOffsets.getOrPut(item.id) { - Animatable(Offset.Zero, Offset.VectorConverter) - } - - ReorderableMediaItem( - item = item, - index = index, - isDragging = isDragging, - dragOffset = if (isDragging) draggedItemOffset else Offset.Zero, - animatedOffset = if (!isDragging) animatedOffset.value else Offset.Zero, - onRemove = if (!isLoading && onRemove != null) { - { 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 + items(mediaItems, key = { it.id }) { item -> + ReorderableItem(reorderableState, key = item.id) { isDragging -> + val index = mediaItems.indexOf(item) + val progress = uploadProgress[index] + + MediaItemCard( + item = item, + isDragging = isDragging, + onRemove = if (!isLoading && onRemove != null) { + { onRemove(item) } + } else null, + onClick = if (onClick != null) { + { onClick(item) } + } else null, + uploadProgress = progress, + modifier = Modifier + .aspectRatio(1f) + .longPressDraggableHandle( + enabled = !isLoading, + onDragStarted = {}, + onDragStopped = {} ) - } - } + ) } } } } @Composable -private fun ReorderableMediaItem( +private fun MediaItemCard( item: MediaItemData, - index: Int, isDragging: Boolean, - dragOffset: Offset, - animatedOffset: Offset, onRemove: (() -> Unit)?, onClick: (() -> Unit)?, - onDragStart: () -> Unit, - onDrag: (Offset) -> Unit, - onDragEnd: () -> Unit, - uploadProgress: Float? = null + uploadProgress: Float?, + modifier: Modifier = Modifier ) { - // 判断是否正在动画中 - val isAnimating = animatedOffset != Offset.Zero - Box( - modifier = Modifier - .size(90.dp) + modifier = modifier .graphicsLayer { - // 使用 graphicsLayer 进行硬件加速的变换 scaleX = 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 } - .zIndex( - when { - isDragging -> 2f // 拖拽项最高层级 - isAnimating -> 1f // 动画项次高层级,避免被其他项遮挡 - else -> 0f // 普通项最低层级 - } - ) - .pointerInput(Unit) { - detectDragGesturesAfterLongPress( - onDragStart = { onDragStart() }, - onDrag = { change, dragAmount -> - change.consume() - onDrag(dragAmount) - }, - onDragEnd = { onDragEnd() }, - onDragCancel = { onDragEnd() } - ) - } + .zIndex(if (isDragging) 1f else 0f) .then( if (onClick != null && uploadProgress == null) { Modifier.clickable { onClick() } diff --git a/android/app/src/main/java/com/memory/app/ui/screen/EditPostScreen.kt b/android/app/src/main/java/com/memory/app/ui/screen/EditPostScreen.kt index 774d53d..9e76f22 100644 --- a/android/app/src/main/java/com/memory/app/ui/screen/EditPostScreen.kt +++ b/android/app/src/main/java/com/memory/app/ui/screen/EditPostScreen.kt @@ -15,8 +15,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress 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.verticalScroll @@ -61,6 +64,8 @@ import com.memory.app.ui.components.MusicCard import com.memory.app.ui.components.VideoPlayer import com.memory.app.ui.theme.* import kotlinx.coroutines.launch +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyGridState @Composable fun EditPostScreen( @@ -491,7 +496,6 @@ private sealed class EditMediaItem { data class NewVideo(val uri: Uri, override val id: String = uri.toString()) : EditMediaItem() } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun EditReorderableMediaGrid( items: List, @@ -502,12 +506,6 @@ private fun EditReorderableMediaGrid( isLoading: Boolean = false ) { var mediaItems by remember(items) { mutableStateOf(items) } - var draggedIndex by remember { mutableStateOf(null) } - var draggedItemOffset by remember { mutableStateOf(Offset.Zero) } - - // 记录每个项目的目标位置偏移(用于动画) - val itemOffsets = remember { mutableStateMapOf>() } - val scope = rememberCoroutineScope() LaunchedEffect(items) { if (items != mediaItems) { @@ -518,174 +516,67 @@ private fun EditReorderableMediaGrid( if (mediaItems.isEmpty()) return 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)) { - for (row in 0 until rows) { - Row(horizontalArrangement = Arrangement.spacedBy(spacing)) { - for (col in 0 until columns) { - val index = row * columns + col - if (index < mediaItems.size) { - val item = mediaItems[index] - val progress = uploadProgress[index] - val isDragging = draggedIndex == index - - // 为每个项目创建动画偏移 - val animatedOffset = itemOffsets.getOrPut(item.id) { - Animatable(Offset.Zero, Offset.VectorConverter) - } - - EditReorderableMediaItem( - item = item, - index = index, - isDragging = isDragging, - dragOffset = if (isDragging) draggedItemOffset else Offset.Zero, - animatedOffset = if (!isDragging) animatedOffset.value else Offset.Zero, - onRemove = if (!isLoading && onRemove != null) { - { 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@EditReorderableMediaItem - 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 + val lazyGridState = rememberLazyGridState() + val reorderableState = rememberReorderableLazyGridState(lazyGridState) { from, to -> + 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) + ) { + items(mediaItems, key = { it.id }) { item -> + ReorderableItem(reorderableState, key = item.id) { isDragging -> + val index = mediaItems.indexOf(item) + val progress = uploadProgress[index] + + EditMediaItemCard( + item = item, + isDragging = isDragging, + onRemove = if (!isLoading && onRemove != null) { + { onRemove(item) } + } else null, + onClick = if (onClick != null) { + { onClick(item) } + } else null, + uploadProgress = progress, + modifier = Modifier + .aspectRatio(1f) + .longPressDraggableHandle( + enabled = !isLoading, + onDragStarted = {}, + onDragStopped = {} ) - } - } + ) } } } } @Composable -private fun EditReorderableMediaItem( +private fun EditMediaItemCard( item: EditMediaItem, - index: Int, isDragging: Boolean, - dragOffset: Offset, - animatedOffset: Offset, onRemove: (() -> Unit)?, onClick: (() -> Unit)?, - onDragStart: () -> Unit, - onDrag: (Offset) -> Unit, - onDragEnd: () -> Unit, - uploadProgress: Float? = null + uploadProgress: Float?, + modifier: Modifier = Modifier ) { - // 判断是否正在动画中 - val isAnimating = animatedOffset != Offset.Zero - Box( - modifier = Modifier - .size(90.dp) + modifier = modifier .graphicsLayer { - // 使用 graphicsLayer 进行硬件加速的变换 scaleX = 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 } - .zIndex( - when { - isDragging -> 2f // 拖拽项最高层级 - isAnimating -> 1f // 动画项次高层级,避免被其他项遮挡 - else -> 0f // 普通项最低层级 - } - ) - .pointerInput(Unit) { - detectDragGesturesAfterLongPress( - onDragStart = { onDragStart() }, - onDrag = { change, dragAmount -> - change.consume() - onDrag(dragAmount) - }, - onDragEnd = { onDragEnd() }, - onDragCancel = { onDragEnd() } - ) - } + .zIndex(if (isDragging) 1f else 0f) .then( if (onClick != null && uploadProgress == null) { Modifier.clickable { onClick() }