Skip to content

Commit 94cd7fb

Browse files
committed
feat(gallery): 引入共享元素过渡效果并优化图片预览逻辑
- 在 `GalleryTabPager` 和 `ImagePreviewDialog` 中添加共享元素过渡功能 - 优化 `ZoomableImage` 的参数传递,增强动画支持 - 新增 `sharedBoundsBy` 和 `animatedVisibilityScope` 配置,提高用户交互体验
1 parent d19ee1f commit 94cd7fb

File tree

2 files changed

+62
-46
lines changed

2 files changed

+62
-46
lines changed

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

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.github.vrcmteam.vrcm.presentation.screens.gallery
22

3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.ExperimentalSharedTransitionApi
35
import androidx.compose.foundation.clickable
46
import androidx.compose.foundation.layout.*
57
import androidx.compose.foundation.lazy.grid.GridCells
@@ -26,9 +28,7 @@ import com.skydoves.landscapist.coil3.CoilImage
2628
import io.github.vrcmteam.vrcm.network.api.files.FileApi
2729
import io.github.vrcmteam.vrcm.network.api.files.data.FileData
2830
import io.github.vrcmteam.vrcm.network.api.files.data.FileTagType
29-
import io.github.vrcmteam.vrcm.presentation.compoments.EmptyContent
30-
import io.github.vrcmteam.vrcm.presentation.compoments.LocationDialogContent
31-
import io.github.vrcmteam.vrcm.presentation.compoments.RefreshBox
31+
import io.github.vrcmteam.vrcm.presentation.compoments.*
3232
import io.github.vrcmteam.vrcm.presentation.settings.locale.strings
3333
import io.github.vrcmteam.vrcm.presentation.supports.Pager
3434
import org.koin.compose.koinInject
@@ -154,14 +154,15 @@ sealed class GalleryTabPager(private val tagType: FileTagType) : Pager {
154154
}
155155
}
156156

157+
@OptIn(ExperimentalSharedTransitionApi::class)
157158
@Composable
158159
private fun GalleryItem(
159160
file: FileData,
160161
tagType: FileTagType,
161162
aspectRatio: Float = 1f,
162163
shape: Shape = MaterialTheme.shapes.medium
163164
) {
164-
val (_, setDialogContent) = LocationDialogContent.current
165+
val (dialogContent, setDialogContent) = LocationDialogContent.current
165166

166167

167168

@@ -177,42 +178,50 @@ sealed class GalleryTabPager(private val tagType: FileTagType) : Pager {
177178
} else {
178179
""
179180
}
180-
CoilImage(
181-
imageModel = { imageUrl },
182-
imageOptions = ImageOptions(
183-
contentScale = ContentScale.Crop,
184-
alignment = Alignment.Center
185-
),
186-
imageLoader = { koinInject() },
187-
modifier = Modifier
188-
.fillMaxSize()
189-
.clip(shape)
190-
.clickable {
191-
// 点击图片时,打开全屏预览
192-
setDialogContent(ImagePreviewDialog(file.id, file.name, file.extension))
193-
},
194-
loading = {
195-
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
196-
CircularProgressIndicator(
197-
modifier = Modifier.size(24.dp),
198-
strokeWidth = 2.dp
199-
)
200-
}
201-
},
202-
failure = {
203-
Column(
204-
modifier = Modifier.fillMaxSize(),
205-
horizontalAlignment = Alignment.CenterHorizontally,
206-
verticalArrangement = Arrangement.Center
207-
) {
208-
Text(
209-
text = strings.galleryTabLoadFailed,
210-
style = MaterialTheme.typography.bodySmall,
211-
color = MaterialTheme.colorScheme.error
181+
AnimatedVisibility(dialogContent == null || (dialogContent as ImagePreviewDialog).fileId != file.id) {
182+
CoilImage(
183+
imageModel = { imageUrl },
184+
imageOptions = ImageOptions(
185+
contentScale = ContentScale.Crop,
186+
alignment = Alignment.Center
187+
),
188+
imageLoader = { koinInject() },
189+
modifier = Modifier
190+
.fillMaxSize()
191+
.clip(shape)
192+
.sharedBoundsBy(
193+
file.id,
194+
sharedTransitionScope = LocalSharedTransitionDialogScope.current,
195+
animatedVisibilityScope = this@AnimatedVisibility
212196
)
197+
.clickable {
198+
// 点击图片时,打开全屏预览
199+
setDialogContent(ImagePreviewDialog(file.id, file.name, file.extension))
200+
},
201+
loading = {
202+
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
203+
CircularProgressIndicator(
204+
modifier = Modifier.size(24.dp),
205+
strokeWidth = 2.dp
206+
)
207+
}
208+
},
209+
failure = {
210+
Column(
211+
modifier = Modifier.fillMaxSize(),
212+
horizontalAlignment = Alignment.CenterHorizontally,
213+
verticalArrangement = Arrangement.Center
214+
) {
215+
Text(
216+
text = strings.galleryTabLoadFailed,
217+
style = MaterialTheme.typography.bodySmall,
218+
color = MaterialTheme.colorScheme.error
219+
)
220+
}
213221
}
214-
}
215-
)
222+
)
223+
}
224+
216225
}
217226

218227
}

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.vrcmteam.vrcm.presentation.screens.gallery
22

33
import androidx.compose.animation.AnimatedVisibilityScope
4+
import androidx.compose.animation.ExperimentalSharedTransitionApi
45
import androidx.compose.foundation.gestures.detectTapGestures
56
import androidx.compose.foundation.gestures.detectTransformGestures
67
import androidx.compose.foundation.layout.*
@@ -22,9 +23,7 @@ import io.github.vrcmteam.vrcm.core.extensions.saveImageToGallery
2223
import io.github.vrcmteam.vrcm.core.shared.SharedFlowCentre
2324
import io.github.vrcmteam.vrcm.getAppPlatform
2425
import io.github.vrcmteam.vrcm.network.api.files.FileApi
25-
import io.github.vrcmteam.vrcm.presentation.compoments.LocationDialogContent
26-
import io.github.vrcmteam.vrcm.presentation.compoments.SharedDialog
27-
import io.github.vrcmteam.vrcm.presentation.compoments.ToastText
26+
import io.github.vrcmteam.vrcm.presentation.compoments.*
2827
import io.github.vrcmteam.vrcm.presentation.extensions.getInsetPadding
2928
import io.github.vrcmteam.vrcm.presentation.settings.locale.strings
3029
import io.github.vrcmteam.vrcm.presentation.supports.AppIcons
@@ -39,7 +38,7 @@ import kotlin.math.min
3938
* 图片预览对话框
4039
*/
4140
class ImagePreviewDialog(
42-
private val fileId: String,
41+
val fileId: String,
4342
private val fileName: String,
4443
private val fileExtension: String,
4544
) : SharedDialog {
@@ -60,17 +59,18 @@ class ImagePreviewDialog(
6059
) {
6160
// 显示原始大小的图片
6261
val imageUrl = FileApi.convertFileUrl(fileId, 2048)
63-
6462
// 为了防止ZoomableImage拦截背景点击事件,单独放在一个Box中
6563
Box(
6664
modifier = Modifier
6765
.fillMaxSize()
6866
) {
6967
ZoomableImage(
68+
id = fileId,
7069
imageUrl = imageUrl,
7170
contentDescription = fileName,
7271
maxScale = 5f,
73-
minScale = 0.5f
72+
minScale = 0.5f,
73+
animatedVisibilityScope = animatedVisibilityScope
7474
)
7575
}
7676

@@ -143,12 +143,15 @@ class ImagePreviewDialog(
143143
/**
144144
* 支持手势缩放和平移的图片组件
145145
*/
146+
@OptIn(ExperimentalSharedTransitionApi::class)
146147
@Composable
147148
fun BoxScope.ZoomableImage(
149+
id: String,
148150
imageUrl: String,
149151
contentDescription: String? = null,
150152
maxScale: Float = 3f,
151-
minScale: Float = 0.8f
153+
minScale: Float = 0.8f,
154+
animatedVisibilityScope: AnimatedVisibilityScope
152155
) {
153156
var scale by remember { mutableStateOf(1f) }
154157
var offset by remember { mutableStateOf(Offset.Zero) }
@@ -248,7 +251,11 @@ fun BoxScope.ZoomableImage(
248251
}
249252
}
250253
)
251-
},
254+
}.sharedBoundsBy(
255+
id,
256+
sharedTransitionScope = LocalSharedTransitionDialogScope.current,
257+
animatedVisibilityScope = animatedVisibilityScope
258+
),
252259
loading = {
253260
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
254261
CircularProgressIndicator()

0 commit comments

Comments
 (0)