Fix scroll-back placeholders: in-memory bitmap cache in DensePhotoGrid
bitmapCache (MutableMap keyed on devicePath) lives at DensePhotoGrid level and survives Compose cell recycling. DenseThumb checks memory cache first — scroll-back is now instant for already-loaded cells. Disk cache (ThumbnailCache) is only hit when bitmap not in memory.
This commit is contained in:
@@ -53,6 +53,11 @@ fun DensePhotoGrid(
|
|||||||
onDeselectAllFolder: (List<PhonePhoto>) -> Unit,
|
onDeselectAllFolder: (List<PhonePhoto>) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
// In-memory bitmap cache — survives cell recycling during scroll
|
||||||
|
// keyed on cacheKey so it resets when device changes
|
||||||
|
val bitmapCache = remember(cacheKey) {
|
||||||
|
mutableMapOf<String, ImageBitmap>()
|
||||||
|
}
|
||||||
if (folderSections.isEmpty() || folderSections.all { it.allPhotos.isEmpty() }) {
|
if (folderSections.isEmpty() || folderSections.all { it.allPhotos.isEmpty() }) {
|
||||||
Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Column(
|
Column(
|
||||||
@@ -144,6 +149,7 @@ fun DensePhotoGrid(
|
|||||||
cacheKey = cacheKey,
|
cacheKey = cacheKey,
|
||||||
pullFn = pullFn,
|
pullFn = pullFn,
|
||||||
thumbnailVersion = thumbnailVersion,
|
thumbnailVersion = thumbnailVersion,
|
||||||
|
bitmapCache = bitmapCache,
|
||||||
onToggle = { onTogglePhoto(labeled.photo) },
|
onToggle = { onTogglePhoto(labeled.photo) },
|
||||||
dateLabel = labeled.dateLabel,
|
dateLabel = labeled.dateLabel,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
@@ -167,13 +173,16 @@ private fun DenseThumb(
|
|||||||
cacheKey: String,
|
cacheKey: String,
|
||||||
pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean,
|
pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean,
|
||||||
thumbnailVersion: Int,
|
thumbnailVersion: Int,
|
||||||
|
bitmapCache: MutableMap<String, ImageBitmap>,
|
||||||
onToggle: () -> Unit,
|
onToggle: () -> Unit,
|
||||||
dateLabel: String?,
|
dateLabel: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var thumbnail by remember(photo.devicePath) { mutableStateOf<ImageBitmap?>(null) }
|
// Check in-memory cache first (survives Compose cell recycling during scroll)
|
||||||
|
var thumbnail by remember(photo.devicePath) {
|
||||||
|
mutableStateOf(bitmapCache[photo.devicePath])
|
||||||
|
}
|
||||||
|
|
||||||
// Re-run when thumbnailVersion increments (new batch cached) or on first composition
|
|
||||||
LaunchedEffect(photo.devicePath, thumbnailVersion) {
|
LaunchedEffect(photo.devicePath, thumbnailVersion) {
|
||||||
if (thumbnail == null) {
|
if (thumbnail == null) {
|
||||||
val bytes = withContext(Dispatchers.IO) {
|
val bytes = withContext(Dispatchers.IO) {
|
||||||
@@ -186,7 +195,9 @@ private fun DenseThumb(
|
|||||||
}
|
}
|
||||||
if (bytes != null && bytes.isNotEmpty()) {
|
if (bytes != null && bytes.isNotEmpty()) {
|
||||||
runCatching {
|
runCatching {
|
||||||
thumbnail = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap()
|
val bitmap = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap()
|
||||||
|
bitmapCache[photo.devicePath] = bitmap // store in memory cache
|
||||||
|
thumbnail = bitmap
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
thumbLog.warn("Decode failed for ${photo.filename} (${bytes.size} bytes): ${e.message}")
|
thumbLog.warn("Decode failed for ${photo.filename} (${bytes.size} bytes): ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user