feat: Add BlurredAvatarBackground component and integrate it into ChatsListScreen and ProfileScreen
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user