fix: add caption support to image viewer with animated display

This commit is contained in:
2026-02-03 21:50:44 +05:00
parent c2283fe0e5
commit 6bb0a90ea0
5 changed files with 351 additions and 329 deletions

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.core.view.WindowCompat
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@@ -69,6 +70,38 @@ private val COLLAPSED_HEADER_HEIGHT_OTHER = 64.dp
private val AVATAR_SIZE_EXPANDED_OTHER = 120.dp
private val AVATAR_SIZE_COLLAPSED_OTHER = 36.dp
/**
* Вычисляет средний цвет bitmap (для случаев когда Palette не может извлечь swatch)
* Используется для белых/чёрных/однотонных изображений
*/
private fun calculateAverageColor(bitmap: android.graphics.Bitmap): Color {
var redSum = 0L
var greenSum = 0L
var blueSum = 0L
// Сэмплируем каждый 4-й пиксель для производительности
val step = 4
var sampledCount = 0
for (y in 0 until bitmap.height step step) {
for (x in 0 until bitmap.width step step) {
val pixel = bitmap.getPixel(x, y)
redSum += android.graphics.Color.red(pixel)
greenSum += android.graphics.Color.green(pixel)
blueSum += android.graphics.Color.blue(pixel)
sampledCount++
}
}
if (sampledCount == 0) return Color.White
return Color(
red = (redSum / sampledCount) / 255f,
green = (greenSum / sampledCount) / 255f,
blue = (blueSum / sampledCount) / 255f
)
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun OtherProfileScreen(
@@ -474,12 +507,10 @@ private fun CollapsingOtherProfileHeader(
val nameFontSize = androidx.compose.ui.unit.lerp(24.sp, 18.sp, collapseProgress)
val onlineFontSize = androidx.compose.ui.unit.lerp(14.sp, 13.sp, collapseProgress)
// Определяем цвет текста на основе фона
val textColor by remember(hasAvatar, avatarColors) {
derivedStateOf {
if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White
}
}
// ═══════════════════════════════════════════════════════════
// 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой
// ═══════════════════════════════════════════════════════════
val textColor = if (isDarkTheme) Color.White else Color.Black
Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) {
// ═══════════════════════════════════════════════════════════

View File

@@ -112,6 +112,41 @@ fun isColorLight(color: Color): Boolean {
return luminance > 0.5f
}
/**
* Вычисляет средний цвет bitmap (для случаев когда Palette не может извлечь swatch)
* Используется для белых/чёрных/однотонных изображений
*/
private fun calculateAverageColor(bitmap: android.graphics.Bitmap): Color {
var redSum = 0L
var greenSum = 0L
var blueSum = 0L
val width = bitmap.width
val height = bitmap.height
val pixelCount = width * height
// Сэмплируем каждый 4-й пиксель для производительности
val step = 4
var sampledCount = 0
for (y in 0 until height step step) {
for (x in 0 until width step step) {
val pixel = bitmap.getPixel(x, y)
redSum += android.graphics.Color.red(pixel)
greenSum += android.graphics.Color.green(pixel)
blueSum += android.graphics.Color.blue(pixel)
sampledCount++
}
}
if (sampledCount == 0) return Color.White
return Color(
red = (redSum / sampledCount) / 255f,
green = (greenSum / sampledCount) / 255f,
blue = (blueSum / sampledCount) / 255f
)
}
fun getAvatarColor(name: String, isDarkTheme: Boolean): AvatarColors {
val cacheKey = "${name}_${if (isDarkTheme) "dark" else "light"}"
return avatarColorCache.getOrPut(cacheKey) {
@@ -800,62 +835,9 @@ private fun CollapsingProfileHeader(
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
// ═══════════════════════════════════════════════════════════
// 🎨 DOMINANT COLOR - извлекаем из аватарки для контраста текста
// 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой
// ═══════════════════════════════════════════════════════════
val avatars by
avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
?: remember { mutableStateOf(emptyList()) }
var avatarBitmap by remember(avatars) { mutableStateOf<android.graphics.Bitmap?>(null) }
var dominantColor by remember { mutableStateOf<Color?>(null) }
LaunchedEffect(avatars, publicKey) {
if (avatars.isNotEmpty()) {
val loadedBitmap = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
}
avatarBitmap = loadedBitmap
// Извлекаем доминантный цвет из нижней части аватарки (где будет текст)
loadedBitmap?.let { bitmap ->
try {
// Берем нижнюю треть изображения для более точного определения
val bottomThird =
android.graphics.Bitmap.createBitmap(
bitmap,
0,
(bitmap.height * 2 / 3).coerceAtLeast(1),
bitmap.width,
(bitmap.height / 3).coerceAtLeast(1)
)
val palette = AndroidPalette.from(bottomThird).generate()
// Используем доминантный цвет или muted swatch
val swatch = palette.dominantSwatch ?: palette.mutedSwatch
if (swatch != null) {
val extractedColor = Color(swatch.rgb)
dominantColor = extractedColor
} else {
}
} catch (e: Exception) {
}
}
} else {
avatarBitmap = null
dominantColor = null
}
}
// Определяем цвет текста на основе фона - derivedStateOf для реактивности
val textColor by remember {
derivedStateOf {
if (hasAvatar && dominantColor != null) {
val isLight = isColorLight(dominantColor!!)
if (isLight) Color.Black else Color.White
} else {
if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White
}
}
}
val textColor = if (isDarkTheme) Color.White else Color.Black
// ═══════════════════════════════════════════════════════════
// 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll