mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-26 01:04:30 +00:00
multiplatform: fix image loading performance and layout stability (#6631)
- Replace runBlocking { imageAndFilePath(file) } with LaunchedEffect +
withContext(Dispatchers.IO) to unblock main thread on all platforms
- Set fixed container size (width + aspectRatio) from preview bitmap to
eliminate layout shifts during async image loading
- Cache base64ToBitmap() with remember() in CIImageView and FramedItemView
- Desktop: replace imageBitmap.toAwtImage().toPainter() with BitmapPainter
to eliminate unnecessary round-trip conversion
- Desktop: add LRU cache for base64ToBitmap (200 entries) and
getLoadedImage (30 entries) to survive LazyColumn item disposal
- Clear loaded image cache on app file deletion via expect/actual
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
+13
-10
@@ -26,7 +26,7 @@ import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
|
||||
import chat.simplex.common.views.chat.chatViewScrollState
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun CIImageView(
|
||||
@@ -38,6 +38,7 @@ fun CIImageView(
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
|
||||
val previewBitmap = remember(image) { base64ToBitmap(image) }
|
||||
@Composable
|
||||
fun progressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
@@ -144,7 +145,7 @@ fun CIImageView(
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
imageView(base64ToBitmap(image), onClick = {
|
||||
imageView(previewBitmap, onClick = {
|
||||
if (fileSource != null) {
|
||||
openFile(fileSource)
|
||||
}
|
||||
@@ -178,14 +179,16 @@ fun CIImageView(
|
||||
|
||||
Box(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID)
|
||||
.then(
|
||||
if (!smallView) {
|
||||
val w = if (previewBitmap.width * 0.97 <= previewBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH
|
||||
Modifier.width(w).aspectRatio(previewBitmap.width.toFloat() / previewBitmap.height.toFloat())
|
||||
} else Modifier
|
||||
)
|
||||
.desktopModifyBlurredState(!smallView, blurred, showMenu),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val res: MutableState<Triple<ImageBitmap, ByteArray, String>?> = remember {
|
||||
mutableStateOf(
|
||||
if (chatModel.connectedToRemote()) null else runBlocking { imageAndFilePath(file) }
|
||||
)
|
||||
}
|
||||
val res: MutableState<Triple<ImageBitmap, ByteArray, String>?> = remember { mutableStateOf(null) }
|
||||
if (chatModel.connectedToRemote()) {
|
||||
LaunchedEffect(file, CIFile.cachedRemoteFileRequests.toList()) {
|
||||
withBGApi {
|
||||
@@ -195,9 +198,9 @@ fun CIImageView(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
KeyChangeEffect(file) {
|
||||
LaunchedEffect(file) {
|
||||
if (res.value == null || res.value!!.third != getLoadedFilePath(file)) {
|
||||
res.value = imageAndFilePath(file)
|
||||
res.value = withContext(Dispatchers.IO) { imageAndFilePath(file) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +209,7 @@ fun CIImageView(
|
||||
val (imageBitmap, data, _) = loaded
|
||||
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
|
||||
} else {
|
||||
imageView(base64ToBitmap(image), onClick = {
|
||||
imageView(previewBitmap, onClick = {
|
||||
if (file != null) {
|
||||
when {
|
||||
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted ->
|
||||
|
||||
+2
-2
@@ -144,7 +144,7 @@ fun FramedItemView(
|
||||
Box(Modifier.fillMaxWidth().weight(1f)) {
|
||||
ciQuotedMsgView(qi)
|
||||
}
|
||||
val imageBitmap = base64ToBitmap(qi.content.image)
|
||||
val imageBitmap = remember(qi.content.image) { base64ToBitmap(qi.content.image) }
|
||||
Image(
|
||||
imageBitmap,
|
||||
contentDescription = stringResource(MR.strings.image_descr),
|
||||
@@ -156,7 +156,7 @@ fun FramedItemView(
|
||||
Box(Modifier.fillMaxWidth().weight(1f)) {
|
||||
ciQuotedMsgView(qi)
|
||||
}
|
||||
val imageBitmap = base64ToBitmap(qi.content.image)
|
||||
val imageBitmap = remember(qi.content.image) { base64ToBitmap(qi.content.image) }
|
||||
Image(
|
||||
imageBitmap,
|
||||
contentDescription = stringResource(MR.strings.video_descr),
|
||||
|
||||
+3
@@ -130,6 +130,8 @@ const val MAX_FILE_SIZE_LOCAL: Long = Long.MAX_VALUE
|
||||
|
||||
expect fun getAppFileUri(fileName: String): URI
|
||||
|
||||
expect fun clearImageCaches()
|
||||
|
||||
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap
|
||||
expect suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>?
|
||||
|
||||
@@ -423,6 +425,7 @@ fun deleteAppFiles() {
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(TAG, "Util deleteAppFiles error: ${e.stackTraceToString()}")
|
||||
}
|
||||
clearImageCaches()
|
||||
}
|
||||
|
||||
fun directoryFileCountAndSize(dir: String): Pair<Int, Long> { // count, size in bytes
|
||||
|
||||
Reference in New Issue
Block a user