fix(photo-editor): 修复裁剪框拖动的闭包快照陷阱

pointerInput(Unit) 的手势 lambda 只在首次组合捕获一次 state,
后续 onDragStart 读的是过期快照,导致再次拖动从中心开始。

改用本地累积值绕开 StateFlow 往返:onDragStart 把 state 快照到
mutableFloatStateOf,onDrag 每帧在累积值上叠加增量,onDragEnd
一次性提交。onDragStart 通过 rememberUpdatedState 读最新 state。

enterCrop 改为幂等(已启用则保留当前框),避免 LaunchedEffect
因 editorState 变化重触时冲掉用户拖动结果。
This commit is contained in:
xfy 2026-07-22 15:18:19 +08:00
parent b8509381d9
commit d3ebd6829b
2 changed files with 43 additions and 25 deletions

View File

@ -80,10 +80,11 @@ class PhotoEditorViewModel(
update { it.copy(rotationDegrees = it.rotationDegrees + delta) }
}
/** 进入裁剪模式:启用裁剪框并重置为默认居中区域。 */
/** 进入裁剪模式:仅在尚未启用时初始化为默认居中区域,已启用则保留当前框。 */
fun enterCrop() {
update {
it.copy(cropLeft = 0.1f, cropTop = 0.1f, cropRight = 0.9f, cropBottom = 0.9f)
if (it.cropEnabled) it
else it.copy(cropLeft = 0.1f, cropTop = 0.1f, cropRight = 0.9f, cropBottom = 0.9f)
}
}

View File

@ -40,8 +40,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import kotlin.math.absoluteValue
@ -436,12 +438,22 @@ private fun CropOverlay(
onCropChange: (Float, Float, Float, Float) -> Unit,
modifier: Modifier = Modifier
) {
// 拖动手柄内聚在此组件,避免跨重组延迟
var dragHandle by remember { mutableStateOf(CropHandle.NONE) }
val left = state.cropLeft ?: 0.1f
val top = state.cropTop
val right = state.cropRight ?: 0.9f
val bottom = state.cropBottom
// 拖动期间的本地累积值;非拖动时为 null绘制回退到 state
var dragLeft by remember { mutableFloatStateOf(0f) }
var dragTop by remember { mutableFloatStateOf(0f) }
var dragRight by remember { mutableFloatStateOf(0f) }
var dragBottom by remember { mutableFloatStateOf(0f) }
// pointerInput(Unit) 只捕获一次 state用 rememberUpdatedState 让
// 手势 lambda 内始终读到最新的 state含上次拖动提交后的值
val currentState by rememberUpdatedState(state)
// 绘制用值:拖动中取本地累积,否则取 state
val left = if (dragHandle != CropHandle.NONE) dragLeft else (state.cropLeft ?: 0.1f)
val top = if (dragHandle != CropHandle.NONE) dragTop else state.cropTop
val right = if (dragHandle != CropHandle.NONE) dragRight else (state.cropRight ?: 0.9f)
val bottom = if (dragHandle != CropHandle.NONE) dragBottom else state.cropBottom
Canvas(
modifier = modifier.pointerInput(Unit) {
detectDragGestures(
@ -449,13 +461,16 @@ private fun CropOverlay(
val size = this.size
val ox = offset.x / size.width
val oy = offset.y / size.height
dragHandle = hitHandle(
ox, oy,
state.cropLeft ?: 0.1f,
state.cropTop,
state.cropRight ?: 0.9f,
state.cropBottom
)
val baseL = currentState.cropLeft ?: 0.1f
val baseT = currentState.cropTop
val baseR = currentState.cropRight ?: 0.9f
val baseB = currentState.cropBottom
// 进入拖动:把 state 当前值快照到本地累积
dragLeft = baseL
dragTop = baseT
dragRight = baseR
dragBottom = baseB
dragHandle = hitHandle(ox, oy, baseL, baseT, baseR, baseB)
},
onDrag = { change, drag ->
if (dragHandle == CropHandle.NONE) return@detectDragGestures
@ -463,18 +478,20 @@ private fun CropOverlay(
val size = this.size
val dx = drag.x / size.width
val dy = drag.y / size.height
// 每帧从 state 读最新值,叠加增量
val cur = moveCrop(
dx, dy,
state.cropLeft ?: 0.1f,
state.cropTop,
state.cropRight ?: 0.9f,
state.cropBottom,
dragHandle
)
onCropChange(cur.left, cur.top, cur.right, cur.bottom)
// 在本地累积值上叠加增量,不触发 StateFlow 往返
val cur = moveCrop(dx, dy, dragLeft, dragTop, dragRight, dragBottom, dragHandle)
dragLeft = cur.left
dragTop = cur.top
dragRight = cur.right
dragBottom = cur.bottom
},
onDragEnd = {
// 拖动结束:一次性提交回 ViewModel
if (dragHandle != CropHandle.NONE) {
onCropChange(dragLeft, dragTop, dragRight, dragBottom)
}
dragHandle = CropHandle.NONE
},
onDragEnd = { dragHandle = CropHandle.NONE },
onDragCancel = { dragHandle = CropHandle.NONE }
)
}