fix: fix image flashing
This commit is contained in:
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.util.LruCache
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
@@ -55,6 +56,40 @@ import kotlin.math.min
|
|||||||
|
|
||||||
private const val TAG = "AttachmentComponents"
|
private const val TAG = "AttachmentComponents"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🖼️ Глобальный LRU кэш для bitmap'ов изображений
|
||||||
|
* Это предотвращает мигание при перезаходе в диалог,
|
||||||
|
* так как изображения остаются в памяти между recomposition
|
||||||
|
*/
|
||||||
|
object ImageBitmapCache {
|
||||||
|
// Размер кэша = 1/8 доступной памяти (стандартная практика Android)
|
||||||
|
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
|
||||||
|
private val cacheSize = maxMemory / 8
|
||||||
|
|
||||||
|
private val cache = object : LruCache<String, Bitmap>(cacheSize) {
|
||||||
|
override fun sizeOf(key: String, bitmap: Bitmap): Int {
|
||||||
|
// Размер в килобайтах
|
||||||
|
return bitmap.byteCount / 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: String): Bitmap? = cache.get(key)
|
||||||
|
|
||||||
|
fun put(key: String, bitmap: Bitmap) {
|
||||||
|
if (cache.get(key) == null) {
|
||||||
|
cache.put(key, bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(key: String) {
|
||||||
|
cache.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
cache.evictAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 📐 Telegram Bubble Specification
|
* 📐 Telegram Bubble Specification
|
||||||
* Все константы взяты из ChatMessageCell.java и Theme.java
|
* Все константы взяты из ChatMessageCell.java и Theme.java
|
||||||
@@ -523,10 +558,21 @@ fun ImageAttachment(
|
|||||||
// Bounds для shared element transition
|
// Bounds для shared element transition
|
||||||
var imageBounds by remember { mutableStateOf<Rect?>(null) }
|
var imageBounds by remember { mutableStateOf<Rect?>(null) }
|
||||||
|
|
||||||
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
|
// 🔥 Ключ для глобального кэша
|
||||||
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
val cacheKey = "img_${attachment.id}"
|
||||||
var blurhashBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
|
||||||
var downloadProgress by remember { mutableStateOf(0f) }
|
// 🔥 Сначала проверяем глобальный кэш
|
||||||
|
var downloadStatus by remember(attachment.id) {
|
||||||
|
mutableStateOf(
|
||||||
|
if (ImageBitmapCache.get(cacheKey) != null) DownloadStatus.DOWNLOADED
|
||||||
|
else DownloadStatus.PENDING
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var imageBitmap by remember(attachment.id) {
|
||||||
|
mutableStateOf(ImageBitmapCache.get(cacheKey))
|
||||||
|
}
|
||||||
|
var blurhashBitmap by remember(attachment.id) { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var downloadProgress by remember(attachment.id) { mutableStateOf(0f) }
|
||||||
|
|
||||||
val preview = getPreview(attachment.preview)
|
val preview = getPreview(attachment.preview)
|
||||||
val downloadTag = getDownloadTag(attachment.preview)
|
val downloadTag = getDownloadTag(attachment.preview)
|
||||||
@@ -541,6 +587,19 @@ fun ImageAttachment(
|
|||||||
|
|
||||||
// Определяем начальный статус и декодируем blurhash (как в Desktop calcDownloadStatus)
|
// Определяем начальный статус и декодируем blurhash (как в Desktop calcDownloadStatus)
|
||||||
LaunchedEffect(attachment.id, attachment.localUri) {
|
LaunchedEffect(attachment.id, attachment.localUri) {
|
||||||
|
// 🔥 Если изображение уже в глобальном кэше - используем его!
|
||||||
|
val cachedBitmap = ImageBitmapCache.get(cacheKey)
|
||||||
|
if (cachedBitmap != null) {
|
||||||
|
imageBitmap = cachedBitmap
|
||||||
|
downloadStatus = DownloadStatus.DOWNLOADED
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 Если изображение уже загружено локально - не перезагружаем!
|
||||||
|
if (imageBitmap != null && downloadStatus == DownloadStatus.DOWNLOADED) {
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
// Определяем статус (логика из Desktop useAttachment.ts)
|
// Определяем статус (логика из Desktop useAttachment.ts)
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
@@ -594,7 +653,7 @@ fun ImageAttachment(
|
|||||||
if (downloadStatus == DownloadStatus.DOWNLOADED) {
|
if (downloadStatus == DownloadStatus.DOWNLOADED) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// 🚀 0. Если есть localUri - загружаем напрямую из URI (optimistic UI)
|
// 🚀 0. Если есть localUri - загружаем напрямую из URI (optimistic UI)
|
||||||
if (attachment.localUri.isNotEmpty()) {
|
if (imageBitmap == null && attachment.localUri.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
val uri = android.net.Uri.parse(attachment.localUri)
|
val uri = android.net.Uri.parse(attachment.localUri)
|
||||||
|
|
||||||
@@ -641,7 +700,11 @@ fun ImageAttachment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bitmap != null) {
|
||||||
imageBitmap = bitmap
|
imageBitmap = bitmap
|
||||||
|
// 🔥 Сохраняем в глобальный кэш
|
||||||
|
ImageBitmapCache.put(cacheKey, bitmap)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Fallback to other methods
|
// Fallback to other methods
|
||||||
}
|
}
|
||||||
@@ -649,7 +712,12 @@ fun ImageAttachment(
|
|||||||
|
|
||||||
// 1. Если нет bitmap, пробуем blob из памяти
|
// 1. Если нет bitmap, пробуем blob из памяти
|
||||||
if (imageBitmap == null && attachment.blob.isNotEmpty()) {
|
if (imageBitmap == null && attachment.blob.isNotEmpty()) {
|
||||||
imageBitmap = base64ToBitmap(attachment.blob)
|
val bitmap = base64ToBitmap(attachment.blob)
|
||||||
|
if (bitmap != null) {
|
||||||
|
imageBitmap = bitmap
|
||||||
|
// 🔥 Сохраняем в глобальный кэш
|
||||||
|
ImageBitmapCache.put(cacheKey, bitmap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Если всё ещё нет, читаем из файловой системы
|
// 2. Если всё ещё нет, читаем из файловой системы
|
||||||
@@ -662,7 +730,12 @@ fun ImageAttachment(
|
|||||||
privateKey
|
privateKey
|
||||||
)
|
)
|
||||||
if (localBlob != null) {
|
if (localBlob != null) {
|
||||||
imageBitmap = base64ToBitmap(localBlob)
|
val bitmap = base64ToBitmap(localBlob)
|
||||||
|
if (bitmap != null) {
|
||||||
|
imageBitmap = bitmap
|
||||||
|
// 🔥 Сохраняем в глобальный кэш
|
||||||
|
ImageBitmapCache.put(cacheKey, bitmap)
|
||||||
|
}
|
||||||
} else if (attachment.localUri.isEmpty()) {
|
} else if (attachment.localUri.isEmpty()) {
|
||||||
// Только если нет localUri - помечаем как NOT_DOWNLOADED
|
// Только если нет localUri - помечаем как NOT_DOWNLOADED
|
||||||
downloadStatus = DownloadStatus.NOT_DOWNLOADED
|
downloadStatus = DownloadStatus.NOT_DOWNLOADED
|
||||||
|
|||||||
Reference in New Issue
Block a user