feat: Enhance background color animation on theme change in SwipeableDialogItem

This commit is contained in:
2026-02-20 05:39:27 +05:00
parent f1252bf328
commit b6f266faa4
6 changed files with 272 additions and 34 deletions

View File

@@ -3,10 +3,13 @@ package com.rosetta.messenger.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.graphics.Matrix
import android.net.Uri
import android.os.Build
import android.util.Base64
import androidx.exifinterface.media.ExifInterface
import com.rosetta.messenger.network.ProtocolManager
import com.vanniktech.blurhash.BlurHash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -28,6 +31,10 @@ object MediaUtils {
// Android ограничение: файл + base64 + шифрование = ~3x памяти
// 20 МБ файл = ~60 МБ RAM, безопасно для большинства устройств
const val MAX_FILE_SIZE_MB = 20
private fun logImage(message: String) {
ProtocolManager.addLog("🧪 IMG-UTIL | $message")
}
/**
* Конвертировать изображение из Uri в Base64 PNG
@@ -35,8 +42,12 @@ object MediaUtils {
*/
suspend fun uriToBase64Image(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
try {
val uriInfo = "${uri.scheme ?: "unknown"}:${uri.lastPathSegment ?: "unknown"}"
logImage("encode start: $uriInfo")
// Читаем EXIF ориентацию
val orientation = getExifOrientation(context, uri)
logImage("orientation=$orientation")
val boundsOptions =
BitmapFactory.Options().apply { inJustDecodeBounds = true }
@@ -45,47 +56,117 @@ object MediaUtils {
} ?: return@withContext null
if (boundsOptions.outWidth <= 0 || boundsOptions.outHeight <= 0) {
logImage("bounds decode failed, trying direct decode fallback")
}
val sourceWidth = boundsOptions.outWidth.coerceAtLeast(1)
val sourceHeight = boundsOptions.outHeight.coerceAtLeast(1)
val initialSample =
calculateInSampleSize(
sourceWidth,
sourceHeight,
MAX_IMAGE_SIZE * 2
)
var bitmap: Bitmap? = null
var sample = initialSample.coerceAtLeast(1)
repeat(2) { attempt ->
if (bitmap != null) return@repeat
val decodeOptions =
BitmapFactory.Options().apply {
inSampleSize = sample
inPreferredConfig = Bitmap.Config.ARGB_8888
}
bitmap =
context.contentResolver.openInputStream(uri)?.use { inputStream ->
BitmapFactory.decodeStream(inputStream, null, decodeOptions)
}
if (bitmap == null) {
bitmap = context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
BitmapFactory.decodeFileDescriptor(pfd.fileDescriptor, null, decodeOptions)
}
}
if (bitmap == null) {
logImage("decode attempt ${attempt + 1} failed (sample=$sample)")
sample *= 2
}
}
if (bitmap == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
bitmap = decodeWithImageDecoder(context, uri)
if (bitmap != null) {
logImage("decoded via ImageDecoder fallback")
}
}
if (bitmap == null) {
logImage("decode failed after all fallbacks")
return@withContext null
}
val decodeOptions =
BitmapFactory.Options().apply {
inSampleSize =
calculateInSampleSize(
boundsOptions.outWidth,
boundsOptions.outHeight,
MAX_IMAGE_SIZE * 2
)
inPreferredConfig = Bitmap.Config.ARGB_8888
val decodedBitmap = bitmap
?: run {
logImage("decode failed: bitmap is null after fallbacks")
return@withContext null
}
var bitmap =
context.contentResolver.openInputStream(uri)?.use { inputStream ->
BitmapFactory.decodeStream(inputStream, null, decodeOptions)
} ?: return@withContext null
// Применяем EXIF ориентацию (поворот/отражение)
bitmap = applyExifOrientation(bitmap, orientation)
val orientedBitmap = applyExifOrientation(decodedBitmap, orientation)
// Масштабируем если слишком большое
val scaledBitmap = scaleDownBitmap(bitmap, MAX_IMAGE_SIZE)
if (scaledBitmap != bitmap) {
bitmap.recycle()
val scaledBitmap = scaleDownBitmap(orientedBitmap, MAX_IMAGE_SIZE)
if (scaledBitmap != orientedBitmap) {
orientedBitmap.recycle()
}
// Конвертируем в PNG Base64
val outputStream = ByteArrayOutputStream()
scaledBitmap.compress(Bitmap.CompressFormat.PNG, IMAGE_QUALITY, outputStream)
val compressed = scaledBitmap.compress(Bitmap.CompressFormat.PNG, IMAGE_QUALITY, outputStream)
if (!compressed) {
logImage("bitmap compress failed")
scaledBitmap.recycle()
return@withContext null
}
val bytes = outputStream.toByteArray()
val base64 = "data:image/png;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
scaledBitmap.recycle()
logImage("encode success: outBytes=${bytes.size}")
base64
} catch (e: Exception) {
logImage("encode exception: ${e.javaClass.simpleName}: ${e.message ?: "unknown"}")
null
} catch (e: OutOfMemoryError) {
logImage("encode OOM: ${e.message ?: "unknown"}")
null
}
}
private fun decodeWithImageDecoder(context: Context, uri: Uri): Bitmap? {
return try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return null
val source = ImageDecoder.createSource(context.contentResolver, uri)
ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
val size = info.size
val maxDimension = maxOf(size.width, size.height)
if (maxDimension > MAX_IMAGE_SIZE * 2) {
val ratio = (MAX_IMAGE_SIZE * 2).toFloat() / maxDimension.toFloat()
val targetW = (size.width * ratio).toInt().coerceAtLeast(1)
val targetH = (size.height * ratio).toInt().coerceAtLeast(1)
decoder.setTargetSize(targetW, targetH)
}
}
} catch (_: Exception) {
null
} catch (_: OutOfMemoryError) {
null
}
}