feat(date-recorder): 详情页图片灯箱,支持缩放/旋转手势

点击详情页大图弹出全屏深色遮罩,集成 panpf/zoomimage 的
SketchZoomAsyncImage,支持双指缩放、平移、捻合旋转(松手吸附
0/90/180/270°)与双击复位,点空白或返回键关闭。

新增 zoomimage-compose-sketch4 1.6.0 依赖(与项目 sketch 同作者,
SketchZoomAsyncImage 是 AsyncImage 超集,额外提供缩放/旋转/大图分片)。
This commit is contained in:
xfy 2026-07-22 14:26:27 +08:00
parent d47a38bc19
commit cec8debff0
4 changed files with 95 additions and 0 deletions

View File

@ -105,6 +105,9 @@ dependencies {
implementation(libs.camera.view) implementation(libs.camera.view)
implementation(libs.androidx.exifinterface) implementation(libs.androidx.exifinterface)
// 日期记录器:详情页灯箱图片缩放/旋转(与 sketch 同作者 panpfSketchZoomAsyncImage 是 AsyncImage 超集)
implementation(libs.zoomimage.compose.sketch4)
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${libs.versions.kotlin.get()}") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${libs.versions.kotlin.get()}")
testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.room.testing) testImplementation(libs.room.testing)

View File

@ -0,0 +1,72 @@
package plus.rua.project.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.github.panpf.sketch.rememberAsyncImageState
import com.github.panpf.zoomimage.SketchZoomAsyncImage
import com.github.panpf.zoomimage.rememberSketchZoomState
/**
* 全屏图片灯箱支持双指缩放平移与旋转查看
*
* 在全屏深色遮罩中居中显示图片底层用 zoomimage [SketchZoomAsyncImage]
* 提供手势双指缩放单指平移双指捻合自由旋转松手后吸附到最近的
* 0/90/180/270°双击复位点击图片外的空白区域或按返回键触发 [onDismiss]
*
* sketch [rememberAsyncImageState] 负责加载zoomimage
* [rememberSketchZoomState] 负责缩放/平移/旋转后者是 sketch `AsyncImage`
* 之外新增的必需参数
*
* @param photoUri 图片 URI null 时直接返回不渲染
* @param contentDescription 无障碍描述 null 表示纯装饰
* @param onDismiss 关闭回调点击空白区域或按返回键时触发
* @param modifier 布局修饰符
*/
@Composable
fun ImageLightbox(
photoUri: String?,
contentDescription: String?,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
if (photoUri == null) return
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
decorFitsSystemWindows = false,
usePlatformDefaultWidth = false
)
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color.Black)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismiss
),
contentAlignment = Alignment.Center
) {
val asyncImageState = rememberAsyncImageState()
val zoomState = rememberSketchZoomState()
SketchZoomAsyncImage(
uri = photoUri,
contentDescription = contentDescription,
state = asyncImageState,
zoomState = zoomState,
modifier = Modifier.fillMaxSize()
)
}
}
}

View File

@ -1,5 +1,7 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -81,6 +83,7 @@ fun RecordDetailScreen(
) )
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var showLightbox by remember { mutableStateOf(false) }
// 删除完成后自动返回 // 删除完成后自动返回
if (state.deleted) { if (state.deleted) {
@ -142,6 +145,7 @@ fun RecordDetailScreen(
else -> DetailContent( else -> DetailContent(
state = state, state = state,
onPhotoClick = { showLightbox = true },
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(innerPadding)
@ -165,11 +169,20 @@ fun RecordDetailScreen(
} }
) )
} }
if (showLightbox) {
ImageLightbox(
photoUri = state.photoUri,
contentDescription = state.record?.title,
onDismiss = { showLightbox = false }
)
}
} }
@Composable @Composable
private fun DetailContent( private fun DetailContent(
state: DateRecordDetailUiState, state: DateRecordDetailUiState,
onPhotoClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val record = state.record ?: return val record = state.record ?: return
@ -183,6 +196,11 @@ private fun DetailContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onPhotoClick
)
) )
Column(modifier = Modifier.padding(horizontal = 16.dp)) { Column(modifier = Modifier.padding(horizontal = 16.dp)) {

View File

@ -22,6 +22,7 @@ sketch = "4.4.0"
spotless = "8.8.0" spotless = "8.8.0"
tyme4kt = "1.5.0" tyme4kt = "1.5.0"
versions = "0.54.0" versions = "0.54.0"
zoomimage = "1.6.0"
[libraries] [libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
@ -57,6 +58,7 @@ room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
sketch-animated-webp = { module = "io.github.panpf.sketch4:sketch-animated-webp", version.ref = "sketch" } sketch-animated-webp = { module = "io.github.panpf.sketch4:sketch-animated-webp", version.ref = "sketch" }
sketch-compose = { module = "io.github.panpf.sketch4:sketch-compose", version.ref = "sketch" } sketch-compose = { module = "io.github.panpf.sketch4:sketch-compose", version.ref = "sketch" }
tyme4kt = { module = "cn.6tail:tyme4kt", version.ref = "tyme4kt" } tyme4kt = { module = "cn.6tail:tyme4kt", version.ref = "tyme4kt" }
zoomimage-compose-sketch4 = { module = "io.github.panpf.zoomimage:zoomimage-compose-sketch4", version.ref = "zoomimage" }
[plugins] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }