Skip to content

Commit 0ace9d9

Browse files
committed
Images & Videos
1 parent bd33a3c commit 0ace9d9

10 files changed

Lines changed: 306 additions & 327 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# MediaPreviewer dismiss hang bug — 已修复
2+
3+
## 现象(已修)
4+
- Images tab → 点首图 → 拖下 → 图片能缩回列表格,scrim 灰罩 + 底部 action button 不消失,容器卡住不 dismiss
5+
6+
## Root cause
7+
- LazyGrid item + `Modifier.animateItem` + `combinedClickable` 在 Compose 1.11.x 下触发同一 item 多次 compose
8+
- 每次 compose 生成新的 `TransformItemState` 实例,通过 `DisposableEffect { addItem() }` 把已测量的老实例从 `transformItemStateMap` 里挤掉
9+
- 老实例的 Box 仍持有 onGloballyPositioned 测量值(265x265),但已不在 map 里;新实例 blockSize = IntSize.Zero
10+
- `findTransformItem(currentKey)` 命中未测量的新实例 → `transformState.itemState` 持有一个 0-sized instance
11+
- 拖下触发 `dragDownClose()``exitTransform()` 内部 `srcSize = blockSize ?: Zero``fitScale = 0/0 = Infinity` → 6 个 `animateTo(NaN/Infinity, …)` 不收敛 → `awaitAll` 永远 hang → scrim 永不消失
12+
13+
## Fix(F1 + caller size)
14+
**核心 fix**:`TransformItemState.addItem` 保留已测量的 instance,不让未测量的新实例覆盖
15+
```kotlin
16+
synchronized(imageTransformMutex) {
17+
val prev = transformItemStateMap[currentKey]
18+
if (prev != null && prev !== this && prev.blockSize.width > 0 && prev.blockSize.height > 0) {
19+
return // 已测量的赢,未测量的不要抢位置
20+
}
21+
transformItemStateMap[currentKey] = this
22+
}
23+
```
24+
**辅助 fix**:ImageGridItem/VideoGridItem 的 TransformImageView modifier 显式 `Modifier.size(widthPx.toDp())`,对齐 ChatImageItem。这样新 compose 阶段新实例本身也能拿到正确的 size,F1 是保险。
25+
26+
## 为什么 ChatImages 没这问题
27+
- ChatImageItem `val itemState = rememberTransformItemState()`,写法完全一样
28+
- ChatImages 用 FlowRow(非 lazy)→ parent 不会触发同一 item 的重复 compose → 老实例不会被覆盖
29+
- 这是 LazyGrid + animateItem 在 Compose 1.11.x 的回归,F1 是真正的根治
30+
31+
## F2 / F3 备选(已弃用)
32+
- **F2** `remember(m.id) { rememberTransformItemState() }`:实测无效,Compose 1.11.x LazyGrid 下 slot 仍会重生成
33+
- **F3** `dragDownClose` 防御性 guard `blockSize == Zero → viewerContainerShrinkDown()`:违反 user 禁止防御性判断原则,且只掩盖症状
34+
35+
## 验证(4 个场景实机通过)
36+
- ✅ drag down > 阈值(`scale < 0.9`):图片缩回 grid,scrim 消失,action bar 消失
37+
- ✅ tap → back:正常 dismiss
38+
- ✅ swipe 切下一张 → drag down:正常 dismiss
39+
- ✅ drag < 阈值(回弹):scale 1F 路径,无副作用
40+
41+
## 修改文件
42+
- `app/.../mediaviewer/previewer/TransformItemState.kt`:F1 保护(5 行)
43+
- `app/.../mediaviewer/previewer/MediaPreviewerState.kt`:删 `scaleToCloseMinValue` 私有 wrapper,`animating` setter 改 private
44+
- `app/.../components/ImageGridItem.kt`:TransformImageView modifier 改显式 size
45+
- `app/.../components/VideoGridItem.kt`:同上
46+
- 其他 TransformImageView caller(ChatFileThumbnail/AppFileListItem/FileListItem)原本就用 `Modifier.size(40.dp)`,无需改
47+
48+
## 关键代码位置(后续维护参考)
49+
- `MediaPreviewerState.kt:326` `dragDownClose()` — 触发器
50+
- `MediaPreviewerState.kt:117` `findTransformItem(key)``transformItemStateMap[key]`
51+
- `MediaTransform.kt:46-47` `srcSize = itemState?.blockSize ?: IntSize.Zero`
52+
- `MediaTransform.kt:106-107` `fitScale = fitSize.width / displayRatioSize.width` — width=0 时 Infinity
53+
- `MediaTransform.kt:145-162` `exitTransform` — 6 个 animateTo(NaN→finite 不收敛)
54+
- `TransformItemState.kt:40-52` `addItem/removeItem` — F1 在此
55+
- `MediaTransformViews.kt:82-100` `TransformItemView``DisposableEffect { addItem() }` + `Box.onGloballyPositioned`
56+
57+
## 设备
58+
- adb id: `47260DLAQ003RB` (Pixel 9 USB)
59+
- Compose = "1.11.3"

app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
44
import androidx.compose.foundation.combinedClickable
55
import androidx.compose.foundation.interaction.MutableInteractionSource
66
import androidx.compose.foundation.layout.Box
7-
import androidx.compose.foundation.layout.aspectRatio
8-
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.size
98
import androidx.compose.foundation.selection.toggleable
109
import androidx.compose.runtime.Composable
1110
import androidx.compose.runtime.derivedStateOf
@@ -14,6 +13,7 @@ import androidx.compose.runtime.remember
1413
import androidx.compose.runtime.rememberCoroutineScope
1514
import androidx.compose.ui.Alignment
1615
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.platform.LocalDensity
1717
import com.ismartcoding.plain.lib.extensions.formatBytes
1818
import com.ismartcoding.plain.lib.extensions.getFilenameFromPath
1919
import com.ismartcoding.plain.lib.helpers.CoroutinesHelper.coMain
@@ -93,7 +93,7 @@ fun ImageGridItem(
9393
),
9494
) {
9595
TransformImageView(
96-
modifier = Modifier.fillMaxSize().align(Alignment.Center).aspectRatio(1f),
96+
modifier = Modifier.size(with(LocalDensity.current) { widthPx.toDp() }),
9797
uri = ImageMediaStoreHelper.getItemUri(m.id),
9898
path = m.path,
9999
fileName = m.path.getFilenameFromPath(),

app/src/main/java/com/ismartcoding/plain/ui/components/VideoGridItem.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import androidx.compose.foundation.ExperimentalFoundationApi
44
import androidx.compose.foundation.combinedClickable
55
import androidx.compose.foundation.interaction.MutableInteractionSource
66
import androidx.compose.foundation.layout.Box
7-
import androidx.compose.foundation.layout.aspectRatio
8-
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.size
98
import androidx.compose.foundation.selection.toggleable
109
import androidx.compose.runtime.Composable
1110
import androidx.compose.runtime.derivedStateOf
1211
import androidx.compose.runtime.getValue
1312
import androidx.compose.runtime.remember
1413
import androidx.compose.ui.Alignment
1514
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.platform.LocalDensity
1616
import com.ismartcoding.plain.lib.extensions.formatBytes
1717
import com.ismartcoding.plain.lib.extensions.formatDuration
1818
import com.ismartcoding.plain.lib.extensions.getFilenameFromPath
@@ -91,7 +91,7 @@ fun VideoGridItem(
9191
),
9292
) {
9393
TransformImageView(
94-
modifier = Modifier.fillMaxSize().align(Alignment.Center).aspectRatio(1f),
94+
modifier = Modifier.size(with(LocalDensity.current) { widthPx.toDp() }),
9595
path = m.path,
9696
fileName = m.path.getFilenameFromPath(),
9797
key = m.id,

app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaPreviewerState.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ class MediaPreviewerState(
4949
val pagerState: PagerState,
5050
) {
5151
var verticalDragType by mutableStateOf(VerticalDragType.None)
52-
private var scaleToCloseMinValue by mutableFloatStateOf(DEFAULT_SCALE_TO_CLOSE_MIN_VALUE)
5352
val videoState = VideoState()
5453
private var mutex = Mutex()
5554
internal var openCallback: (() -> Unit)? = null
@@ -102,7 +101,7 @@ class MediaPreviewerState(
102101
var pagerUserScrollEnabled by mutableStateOf(true)
103102

104103
var animating by mutableStateOf(false)
105-
internal set
104+
private set
106105
var visible by mutableStateOf(false)
107106
internal set
108107
private var visibleTarget by mutableStateOf<Boolean?>(null)
@@ -286,7 +285,7 @@ class MediaPreviewerState(
286285
dragActivated && event.type == PointerEventType.Release -> {
287286
pagerUserScrollEnabled = true; mediaViewerState?.allowGestureInput = true
288287
vStartOffset = null; vOrientationDown = null; dragActivated = false
289-
if ((viewerContainerState?.scale?.value ?: 1F) < scaleToCloseMinValue) {
288+
if ((viewerContainerState?.scale?.value ?: 1F) < DEFAULT_SCALE_TO_CLOSE_MIN_VALUE) {
290289
scope.launch {
291290
if (canTransformOut) {
292291
val key = getKey(pagerState.currentPage)

app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/TransformItemState.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ class TransformItemState(
4040
fun addItem(key: Any? = null) {
4141
val currentKey = key ?: this.key
4242
if (checkInBound != null) return
43-
synchronized(imageTransformMutex) { transformItemStateMap[currentKey] = this }
43+
synchronized(imageTransformMutex) {
44+
val prev = transformItemStateMap[currentKey]
45+
if (prev != null && prev !== this && prev.blockSize.width > 0 && prev.blockSize.height > 0) {
46+
return
47+
}
48+
transformItemStateMap[currentKey] = this
49+
}
4450
}
4551

4652
fun removeItem(key: Any? = null) {

app/src/main/java/com/ismartcoding/plain/ui/page/videos/VideoFilesSelectModeBottomActions.kt

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)