@@ -2,6 +2,8 @@ package io.github.vrcmteam.vrcm.presentation.screens.gallery
22
33import androidx.compose.animation.AnimatedVisibilityScope
44import androidx.compose.animation.ExperimentalSharedTransitionApi
5+ import androidx.compose.animation.core.animateFloatAsState
6+ import androidx.compose.foundation.gestures.detectDragGestures
57import androidx.compose.foundation.gestures.detectTapGestures
68import androidx.compose.foundation.gestures.detectTransformGestures
79import androidx.compose.foundation.layout.*
@@ -24,6 +26,7 @@ import io.github.vrcmteam.vrcm.core.shared.SharedFlowCentre
2426import io.github.vrcmteam.vrcm.getAppPlatform
2527import io.github.vrcmteam.vrcm.network.api.files.FileApi
2628import io.github.vrcmteam.vrcm.presentation.compoments.*
29+ import io.github.vrcmteam.vrcm.presentation.extensions.enableIf
2730import io.github.vrcmteam.vrcm.presentation.extensions.getInsetPadding
2831import io.github.vrcmteam.vrcm.presentation.settings.locale.strings
2932import 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