feat: Add BlurredAvatarBackground component and integrate it into ChatsListScreen and ProfileScreen

This commit is contained in:
2026-01-24 20:18:27 +05:00
parent 23e1d72ac0
commit dc548a3c7a
8 changed files with 420 additions and 29 deletions

View File

@@ -0,0 +1,219 @@
package com.rosetta.messenger.ui.components
import android.graphics.Bitmap
import android.os.Build
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.utils.AvatarFileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Компонент для отображения размытого фона аватарки
* Используется в профиле и сайдбаре
* ВАЖНО: Должен вызываться внутри BoxScope чтобы matchParentSize() работал
*
* @param publicKey Публичный ключ пользователя
* @param avatarRepository Репозиторий аватаров
* @param fallbackColor Цвет фона если нет аватарки
* @param blurRadius Радиус размытия (в пикселях) - применяется при обработке
* @param alpha Прозрачность (0.0 - 1.0)
*/
@Composable
fun BoxScope.BlurredAvatarBackground(
publicKey: String,
avatarRepository: AvatarRepository?,
fallbackColor: Color,
blurRadius: Float = 25f,
alpha: Float = 0.3f
) {
// Получаем аватары из репозитория
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
?: remember { mutableStateOf(emptyList()) }
// Состояние для bitmap и размытого bitmap
var originalBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
var blurredBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
// Декодируем и размываем аватар
LaunchedEffect(avatars) {
if (avatars.isNotEmpty()) {
originalBitmap = withContext(Dispatchers.IO) {
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
}
// Размываем bitmap (уменьшаем для производительности, затем применяем blur)
originalBitmap?.let { bitmap ->
blurredBitmap = withContext(Dispatchers.Default) {
// Уменьшаем размер для быстрого blur
val scaledBitmap = Bitmap.createScaledBitmap(
bitmap,
bitmap.width / 4,
bitmap.height / 4,
true
)
// Применяем blur несколько раз для более гладкого эффекта
var result = scaledBitmap
repeat(3) {
result = fastBlur(result, (blurRadius / 4).toInt().coerceAtLeast(1))
}
result
}
}
} else {
originalBitmap = null
blurredBitmap = null
}
}
// Используем matchParentSize() чтобы занимать только размер родителя
Box(modifier = Modifier.matchParentSize()) {
if (blurredBitmap != null) {
// Показываем размытое изображение
Image(
bitmap = blurredBitmap!!.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
this.alpha = alpha
},
contentScale = ContentScale.Crop
)
// Дополнительный overlay для затемнения
Box(
modifier = Modifier
.fillMaxSize()
.background(fallbackColor.copy(alpha = 0.3f))
)
} else {
// Fallback: цветной фон
Box(
modifier = Modifier
.fillMaxSize()
.background(fallbackColor)
)
}
}
}
/**
* Быстрое размытие по Гауссу (Box Blur - упрощенная версия)
* Основано на Stack Blur Algorithm от Mario Klingemann
*/
private fun fastBlur(source: Bitmap, radius: Int): Bitmap {
if (radius < 1) return source
val w = source.width
val h = source.height
val bitmap = source.copy(source.config, true)
val pixels = IntArray(w * h)
bitmap.getPixels(pixels, 0, w, 0, 0, w, h)
// Применяем горизонтальное размытие
for (y in 0 until h) {
blurRow(pixels, y, w, h, radius)
}
// Применяем вертикальное размытие
for (x in 0 until w) {
blurColumn(pixels, x, w, h, radius)
}
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
return bitmap
}
private fun blurRow(pixels: IntArray, y: Int, w: Int, h: Int, radius: Int) {
var sumR = 0
var sumG = 0
var sumB = 0
var sumA = 0
val dv = radius * 2 + 1
val offset = y * w
// Инициализация суммы
for (i in -radius..radius) {
val x = i.coerceIn(0, w - 1)
val pixel = pixels[offset + x]
sumA += (pixel shr 24) and 0xff
sumR += (pixel shr 16) and 0xff
sumG += (pixel shr 8) and 0xff
sumB += pixel and 0xff
}
// Применяем blur
for (x in 0 until w) {
pixels[offset + x] = ((sumA / dv) shl 24) or
((sumR / dv) shl 16) or
((sumG / dv) shl 8) or
(sumB / dv)
// Обновляем сумму для следующего пикселя
val xLeft = (x - radius).coerceIn(0, w - 1)
val xRight = (x + radius + 1).coerceIn(0, w - 1)
val leftPixel = pixels[offset + xLeft]
val rightPixel = pixels[offset + xRight]
sumA += ((rightPixel shr 24) and 0xff) - ((leftPixel shr 24) and 0xff)
sumR += ((rightPixel shr 16) and 0xff) - ((leftPixel shr 16) and 0xff)
sumG += ((rightPixel shr 8) and 0xff) - ((leftPixel shr 8) and 0xff)
sumB += (rightPixel and 0xff) - (leftPixel and 0xff)
}
}
private fun blurColumn(pixels: IntArray, x: Int, w: Int, h: Int, radius: Int) {
var sumR = 0
var sumG = 0
var sumB = 0
var sumA = 0
val dv = radius * 2 + 1
// Инициализация суммы
for (i in -radius..radius) {
val y = i.coerceIn(0, h - 1)
val pixel = pixels[y * w + x]
sumA += (pixel shr 24) and 0xff
sumR += (pixel shr 16) and 0xff
sumG += (pixel shr 8) and 0xff
sumB += pixel and 0xff
}
// Применяем blur
for (y in 0 until h) {
val offset = y * w + x
pixels[offset] = ((sumA / dv) shl 24) or
((sumR / dv) shl 16) or
((sumG / dv) shl 8) or
(sumB / dv)
// Обновляем сумму для следующего пикселя
val yTop = (y - radius).coerceIn(0, h - 1)
val yBottom = (y + radius + 1).coerceIn(0, h - 1)
val topPixel = pixels[yTop * w + x]
val bottomPixel = pixels[yBottom * w + x]
sumA += ((bottomPixel shr 24) and 0xff) - ((topPixel shr 24) and 0xff)
sumR += ((bottomPixel shr 16) and 0xff) - ((topPixel shr 16) and 0xff)
sumG += ((bottomPixel shr 8) and 0xff) - ((topPixel shr 8) and 0xff)
sumB += (bottomPixel and 0xff) - (topPixel and 0xff)
}
}