Skip to content

Commit 9dacb9c

Browse files
committed
feat(gallery): 增强图片预览逻辑,支持缩放动画与手势拖动关闭功能
- 为图片预览添加缩放动画,实现更平滑的交互体验 - 新增手势拖动关闭逻辑,增强用户操作的灵活性 - 优化 `ZoomableImage` 组件的缩放和平移处理,提升代码可维护性和性能
1 parent 94cd7fb commit 9dacb9c

File tree

1 file changed

+107
-44
lines changed

1 file changed

+107
-44
lines changed

composeApp/src/commonMain/kotlin/presentation/screens/gallery/ImagePreviewDialog.kt

Lines changed: 107 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package io.github.vrcmteam.vrcm.presentation.screens.gallery
22

33
import androidx.compose.animation.AnimatedVisibilityScope
44
import androidx.compose.animation.ExperimentalSharedTransitionApi
5+
import androidx.compose.animation.core.animateFloatAsState
6+
import androidx.compose.foundation.gestures.detectDragGestures
57
import androidx.compose.foundation.gestures.detectTapGestures
68
import androidx.compose.foundation.gestures.detectTransformGestures
79
import androidx.compose.foundation.layout.*
@@ -24,6 +26,7 @@ import io.github.vrcmteam.vrcm.core.shared.SharedFlowCentre
2426
import io.github.vrcmteam.vrcm.getAppPlatform
2527
import io.github.vrcmteam.vrcm.network.api.files.FileApi
2628
import io.github.vrcmteam.vrcm.presentation.compoments.*
29+
import io.github.vrcmteam.vrcm.presentation.extensions.enableIf
2730
import io.github.vrcmteam.vrcm.presentation.extensions.getInsetPadding
2831
import io.github.vrcmteam.vrcm.presentation.settings.locale.strings
2932
import io.github.vrcmteam.vrcm.presentation.supports.AppIcons
@@ -70,7 +73,11 @@ class ImagePreviewDialog(
7073
contentDescription = fileName,
7174
maxScale = 5f,
7275
minScale = 0.5f,
73-
animatedVisibilityScope = animatedVisibilityScope
76+
animatedVisibilityScope = animatedVisibilityScope,
77+
onDismiss = {
78+
// 移除对话框
79+
setDialogContent(null)
80+
},
7481
)
7582
}
7683

@@ -87,7 +94,14 @@ class ImagePreviewDialog(
8794
fileName = "${fileName}${fileExtension}"
8895
)
8996
}.onFailure {
90-
SharedFlowCentre.toastText.emit(ToastText.Error(strings.imageSaveError.replace("%s",it.message.orEmpty())))
97+
SharedFlowCentre.toastText.emit(
98+
ToastText.Error(
99+
strings.imageSaveError.replace(
100+
"%s",
101+
it.message.orEmpty()
102+
)
103+
)
104+
)
91105
}.onSuccess { isSuccess ->
92106
if (isSuccess) {
93107
SharedFlowCentre.toastText.emit(ToastText.Success(strings.imageSaveSuccess))
@@ -151,23 +165,47 @@ fun BoxScope.ZoomableImage(
151165
contentDescription: String? = null,
152166
maxScale: Float = 3f,
153167
minScale: Float = 0.8f,
154-
animatedVisibilityScope: AnimatedVisibilityScope
168+
animatedVisibilityScope: AnimatedVisibilityScope,
169+
onDismiss: () -> Unit
155170
) {
156-
var scale by remember { mutableStateOf(1f) }
157-
var offset by remember { mutableStateOf(Offset.Zero) }
158-
var doubleTapState by remember { mutableStateOf(0) } // 0: 正常, 1: 放大, 2: 缩小
171+
var targetScale by remember { mutableStateOf(1f) }
172+
var targetOffset by remember { mutableStateOf(Offset.Zero) }
173+
var doubleTapState by remember { mutableStateOf(0) }
174+
175+
// 用于长按拖动的偏移量
176+
177+
// 设置拖动阈值 (像素)
178+
val dismissThreshold = 800f
159179

160-
// 监视scale变化,当缩放回到1.0f时,重置offset到中心
161-
LaunchedEffect(scale) {
162-
if (scale <= 1.0f) {
163-
offset = Offset.Zero
180+
// 使用 animateFloatAsState 为 scale 添加动画
181+
val animatedScale by animateFloatAsState(
182+
targetValue = targetScale,
183+
animationSpec = androidx.compose.animation.core.spring(
184+
dampingRatio = 0.8f,
185+
stiffness = 300f
186+
),
187+
label = "scale_animation"
188+
)
189+
190+
// 使用 animateOffsetAsState 为 offset 添加动画
191+
val animatedOffset by androidx.compose.animation.core.animateOffsetAsState(
192+
targetValue = targetOffset,
193+
animationSpec = androidx.compose.animation.core.spring(
194+
dampingRatio = 0.8f,
195+
stiffness = 300f
196+
),
197+
label = "offset_animation"
198+
)
199+
200+
// 监视animatedScale变化,当缩放回到1.0f时,重置offset到中心
201+
LaunchedEffect(animatedScale) {
202+
if (animatedScale <= 1.0f) {
203+
targetOffset = Offset.Zero
164204
}
165205
}
166206

167207
Box(
168-
modifier = Modifier
169-
.align(Alignment.Center),
170-
208+
modifier = Modifier.align(Alignment.Center),
171209
contentAlignment = Alignment.Center
172210
) {
173211
CoilImage(
@@ -181,32 +219,56 @@ fun BoxScope.ZoomableImage(
181219
imageLoader = { koinInject() },
182220
modifier = Modifier
183221
.graphicsLayer(
184-
scaleX = scale,
185-
scaleY = scale,
186-
translationX = offset.x,
187-
translationY = offset.y
222+
scaleX = animatedScale,
223+
scaleY = animatedScale,
224+
translationX = animatedOffset.x,
225+
translationY = animatedOffset.y
188226
).pointerInput(Unit) {
189-
detectTransformGestures { _, pan, zoom, _ ->
190-
// 处理缩放
191-
val prevScale = scale
192-
scale = (scale * zoom).coerceIn(minScale, maxScale)
227+
detectTransformGestures(true){ _, pan, zoom, _ ->
228+
// 处理缩放 - 手势缩放时直接更新targetScale
229+
val prevScale = targetScale
230+
targetScale = (targetScale * zoom).coerceIn(minScale, maxScale)
193231

194232
// 处理平移 (只有在放大状态下才能平移)
195-
if (scale > 1f) {
233+
if (animatedScale > 1f) {
196234
// 计算新的偏移量
197-
val newOffset = offset + (pan * scale)
235+
val newOffset = targetOffset + (pan * animatedScale)
198236

199237
// 限制平移范围,防止图片移出屏幕太多
200-
val maxX = (scale - 1) * size.width / 2
201-
val maxY = (scale - 1) * size.height / 2
238+
val maxX = (animatedScale - 1) * size.width / 2
239+
val maxY = (animatedScale - 1) * size.height / 2
202240

203-
offset = Offset(
241+
targetOffset = Offset(
204242
newOffset.x.coerceIn(-maxX, maxX),
205243
newOffset.y.coerceIn(-maxY, maxY)
206244
)
207-
} else if (prevScale > 1f && scale <= 1f) {
208-
// 如果从放大状态缩小到正常或更小,重置位置
209-
offset = Offset.Zero
245+
} else if (prevScale > 1f && targetScale <= 1f) {
246+
// 如果从放大状态缩小到正常或更小,重置位置(带动画)
247+
targetOffset = Offset.Zero
248+
}
249+
}
250+
}
251+
// 只有在正常大小时才启用长按拖动
252+
.enableIf(doubleTapState == 0) {
253+
pointerInput(Unit) {
254+
detectDragGestures(
255+
onDragEnd = {
256+
// 拖动结束时检查是否达到阈值
257+
val distance = kotlin.math.sqrt(
258+
targetOffset.x * targetOffset.x + targetOffset.y * targetOffset.y
259+
)
260+
261+
if (distance >= dismissThreshold) {
262+
// 达到阈值,调用onDismiss
263+
onDismiss()
264+
} else {
265+
// 未达到阈值,重置拖动偏移量(带动画)
266+
targetOffset = Offset.Zero
267+
}
268+
}
269+
) { change, dragAmount ->
270+
// 更新拖动偏移量
271+
targetOffset += dragAmount
210272
}
211273
}
212274
}
@@ -217,18 +279,18 @@ fun BoxScope.ZoomableImage(
217279
when (doubleTapState) {
218280
0 -> {
219281
// 正常 -> 放大
220-
scale = min(maxScale, 2.5f)
282+
targetScale = min(maxScale, 2.5f)
221283

222284
// 可以选择让双击的位置作为放大的中心点
223285
val centerX = size.width / 2
224286
val centerY = size.height / 2
225-
val targetOffsetX = (centerX - tapOffset.x) * (scale - 1)
226-
val targetOffsetY = (centerY - tapOffset.y) * (scale - 1)
287+
val targetOffsetX = (centerX - tapOffset.x) * (targetScale - 1)
288+
val targetOffsetY = (centerY - tapOffset.y) * (targetScale - 1)
227289

228290
// 限制偏移量
229-
val maxX = (scale - 1) * size.width / 2
230-
val maxY = (scale - 1) * size.height / 2
231-
offset = Offset(
291+
val maxX = (targetScale - 1) * size.width / 2
292+
val maxY = (targetScale - 1) * size.height / 2
293+
targetOffset = Offset(
232294
targetOffsetX.coerceIn(-maxX, maxX),
233295
targetOffsetY.coerceIn(-maxY, maxY)
234296
)
@@ -238,24 +300,25 @@ fun BoxScope.ZoomableImage(
238300

239301
1 -> {
240302
// 放大 -> 更放大
241-
scale = maxScale
303+
targetScale = maxScale
242304
doubleTapState = 2
243305
}
244306

245307
else -> {
246308
// 缩小回正常
247-
scale = 1f
248-
offset = Offset.Zero // 确保位置回到中心
309+
targetScale = 1f
310+
targetOffset = Offset.Zero // 确保位置回到中心(带动画)
249311
doubleTapState = 0
250312
}
251313
}
252314
}
253315
)
254-
}.sharedBoundsBy(
255-
id,
256-
sharedTransitionScope = LocalSharedTransitionDialogScope.current,
257-
animatedVisibilityScope = animatedVisibilityScope
258-
),
316+
}
317+
.sharedBoundsBy(
318+
id,
319+
sharedTransitionScope = LocalSharedTransitionDialogScope.current,
320+
animatedVisibilityScope = animatedVisibilityScope
321+
),
259322
loading = {
260323
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
261324
CircularProgressIndicator()
@@ -272,4 +335,4 @@ fun BoxScope.ZoomableImage(
272335
}
273336
)
274337
}
275-
}
338+
}

0 commit comments

Comments
 (0)