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) } update { it.copy(rotationDegrees = it.rotationDegrees + delta) }
} }
/** 进入裁剪模式:启用裁剪框并重置为默认居中区域。 */ /** 进入裁剪模式:仅在尚未启用时初始化为默认居中区域,已启用则保留当前框。 */
fun enterCrop() { fun enterCrop() {
update { 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -436,12 +438,22 @@ private fun CropOverlay(
onCropChange: (Float, Float, Float, Float) -> Unit, onCropChange: (Float, Float, Float, Float) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// 拖动手柄内聚在此组件,避免跨重组延迟
var dragHandle by remember { mutableStateOf(CropHandle.NONE) } var dragHandle by remember { mutableStateOf(CropHandle.NONE) }
val left = state.cropLeft ?: 0.1f // 拖动期间的本地累积值;非拖动时为 null绘制回退到 state
val top = state.cropTop var dragLeft by remember { mutableFloatStateOf(0f) }
val right = state.cropRight ?: 0.9f var dragTop by remember { mutableFloatStateOf(0f) }
val bottom = state.cropBottom 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( Canvas(
modifier = modifier.pointerInput(Unit) { modifier = modifier.pointerInput(Unit) {
detectDragGestures( detectDragGestures(
@ -449,13 +461,16 @@ private fun CropOverlay(
val size = this.size val size = this.size
val ox = offset.x / size.width val ox = offset.x / size.width
val oy = offset.y / size.height val oy = offset.y / size.height
dragHandle = hitHandle( val baseL = currentState.cropLeft ?: 0.1f
ox, oy, val baseT = currentState.cropTop
state.cropLeft ?: 0.1f, val baseR = currentState.cropRight ?: 0.9f
state.cropTop, val baseB = currentState.cropBottom
state.cropRight ?: 0.9f, // 进入拖动:把 state 当前值快照到本地累积
state.cropBottom dragLeft = baseL
) dragTop = baseT
dragRight = baseR
dragBottom = baseB
dragHandle = hitHandle(ox, oy, baseL, baseT, baseR, baseB)
}, },
onDrag = { change, drag -> onDrag = { change, drag ->
if (dragHandle == CropHandle.NONE) return@detectDragGestures if (dragHandle == CropHandle.NONE) return@detectDragGestures
@ -463,18 +478,20 @@ private fun CropOverlay(
val size = this.size val size = this.size
val dx = drag.x / size.width val dx = drag.x / size.width
val dy = drag.y / size.height val dy = drag.y / size.height
// 每帧从 state 读最新值,叠加增量 // 在本地累积值上叠加增量,不触发 StateFlow 往返
val cur = moveCrop( val cur = moveCrop(dx, dy, dragLeft, dragTop, dragRight, dragBottom, dragHandle)
dx, dy, dragLeft = cur.left
state.cropLeft ?: 0.1f, dragTop = cur.top
state.cropTop, dragRight = cur.right
state.cropRight ?: 0.9f, dragBottom = cur.bottom
state.cropBottom, },
dragHandle onDragEnd = {
) // 拖动结束:一次性提交回 ViewModel
onCropChange(cur.left, cur.top, cur.right, cur.bottom) if (dragHandle != CropHandle.NONE) {
onCropChange(dragLeft, dragTop, dragRight, dragBottom)
}
dragHandle = CropHandle.NONE
}, },
onDragEnd = { dragHandle = CropHandle.NONE },
onDragCancel = { dragHandle = CropHandle.NONE } onDragCancel = { dragHandle = CropHandle.NONE }
) )
} }