mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-22 12:52:28 +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:
1 parent
bf56ed0f56
commit
024df7099d
7 files changed
+41
-16
No files matched your search
+8
-1
@@ -21,12 +21,19 @@ import kotlin.math.sqrt
|
||||
private fun errorBitmap(): ImageBitmap =
|
||||
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="))).toComposeImageBitmap()
|
||||
|
||||
private val base64BitmapCache = Collections.synchronizedMap(object : LinkedHashMap<String, ImageBitmap>(200, 0.75f, true) {
|
||||
override fun removeEldestEntry(eldest: Map.Entry<String, ImageBitmap>): Boolean = size > 200
|
||||
})
|
||||
|
||||
actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
|
||||
base64BitmapCache[base64ImageString]?.let { return it }
|
||||
val imageString = base64ImageString
|
||||
.removePrefix("data:image/png;base64,")
|
||||
.removePrefix("data:image/jpg;base64,")
|
||||
return try {
|
||||
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap()
|
||||
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap().also {
|
||||
base64BitmapCache[base64ImageString] = it
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "base64ToBitmap error: $e")
|
||||
errorBitmap()
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import chat.simplex.common.model.CIFile
|
||||
import chat.simplex.common.platform.*
|
||||
@@ -17,7 +18,7 @@ actual fun SimpleAndAnimatedImageView(
|
||||
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
|
||||
) {
|
||||
// LALAL make it animated too
|
||||
ImageView(imageBitmap.toAwtImage().toPainter()) {
|
||||
ImageView(BitmapPainter(imageBitmap)) {
|
||||
if (getLoadedFilePath(file) != null) {
|
||||
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
|
||||
+11
-2
@@ -16,6 +16,7 @@ import kotlinx.coroutines.delay
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
@@ -128,6 +129,14 @@ actual fun getAppFileUri(fileName: String): URI {
|
||||
}
|
||||
}
|
||||
|
||||
private val loadedImageCache = Collections.synchronizedMap(object : LinkedHashMap<String, Pair<ImageBitmap, ByteArray>>(30, 0.75f, true) {
|
||||
override fun removeEldestEntry(eldest: Map.Entry<String, Pair<ImageBitmap, ByteArray>>): Boolean = size > 30
|
||||
})
|
||||
|
||||
actual fun clearImageCaches() {
|
||||
loadedImageCache.clear()
|
||||
}
|
||||
|
||||
actual suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? {
|
||||
var filePath = getLoadedFilePath(file)
|
||||
if (chatModel.connectedToRemote() && filePath == null) {
|
||||
@@ -135,10 +144,10 @@ actual suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>?
|
||||
filePath = getLoadedFilePath(file)
|
||||
}
|
||||
return if (filePath != null) {
|
||||
try {
|
||||
loadedImageCache[filePath] ?: try {
|
||||
val data = if (file?.fileSource?.cryptoArgs != null) readCryptoFile(filePath, file.fileSource.cryptoArgs) else File(filePath).readBytes()
|
||||
val bitmap = getBitmapFromByteArray(data, false)
|
||||
if (bitmap != null) bitmap to data else null
|
||||
if (bitmap != null) (bitmap to data).also { loadedImageCache[filePath] = it } else null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to read crypto file: " + e.stackTraceToString())
|
||||
null
|
||||
|
||||
Reference in new issue
Block a user