fix: improve avatar loading and rendering logic to prevent flickering and enhance performance
This commit is contained in:
@@ -65,23 +65,31 @@ fun AvatarImage(
|
|||||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||||
?: remember { mutableStateOf(emptyList()) }
|
?: remember { mutableStateOf(emptyList()) }
|
||||||
|
|
||||||
// Состояние для bitmap
|
// 🔥 FIX: Используем стабильный ключ (timestamp первого аватара) вместо reference списка
|
||||||
var bitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
// Это предотвращает сброс bitmap при recomposition когда данные не изменились
|
||||||
|
val avatarKey = remember(avatars) {
|
||||||
// Логируем для отладки
|
avatars.firstOrNull()?.timestamp ?: 0L
|
||||||
LaunchedEffect(publicKey, avatars) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Декодируем первый аватар
|
// 🔥 FIX: Состояние для bitmap - НЕ сбрасываем при изменении ключа
|
||||||
LaunchedEffect(avatars) {
|
// Показываем предыдущий bitmap пока грузится новый (double buffering)
|
||||||
bitmap = if (avatars.isNotEmpty()) {
|
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val result = AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
// 🔥 FIX: Декодируем только когда avatarKey (timestamp) реально изменился
|
||||||
result
|
// НЕ сбрасываем bitmap в null - показываем старый пока грузится новый
|
||||||
|
LaunchedEffect(avatarKey) {
|
||||||
|
val currentAvatars = avatars
|
||||||
|
if (currentAvatars.isNotEmpty()) {
|
||||||
|
val newBitmap = withContext(Dispatchers.IO) {
|
||||||
|
AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data)
|
||||||
|
}
|
||||||
|
// Устанавливаем новый bitmap только если декодирование успешно
|
||||||
|
if (newBitmap != null) {
|
||||||
|
bitmap = newBitmap
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
// Если avatars пустой - НЕ сбрасываем bitmap в null
|
||||||
|
// Placeholder покажется через условие bitmap == null ниже
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -377,8 +377,10 @@ fun ProfileMetaballOverlay(
|
|||||||
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() }
|
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() }
|
||||||
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() }
|
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() }
|
||||||
|
|
||||||
// Avatar state - computed with Telegram's exact logic
|
// 🔥 FIX: Убраны volatile keys (collapseProgress, expansionProgress) из remember
|
||||||
val avatarState by remember(collapseProgress, expansionProgress, screenWidthPx, statusBarHeightPx, headerHeightPx, hasAvatar, notchCenterY, notchRadiusPx) {
|
// derivedStateOf автоматически отслеживает их как зависимости внутри лямбды
|
||||||
|
// Только стабильные параметры (размеры экрана, notch info) как ключи remember
|
||||||
|
val avatarState by remember(screenWidthPx, statusBarHeightPx, headerHeightPx, notchCenterY, notchRadiusPx) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
computeAvatarState(
|
computeAvatarState(
|
||||||
collapseProgress = collapseProgress,
|
collapseProgress = collapseProgress,
|
||||||
@@ -530,93 +532,93 @@ fun ProfileMetaballOverlay(
|
|||||||
} // END if (showMetaballLayer)
|
} // END if (showMetaballLayer)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// LAYER 2: Avatar content - TELEGRAM STYLE EXPANSION
|
// LAYER 2: Avatar content - UNIFIED BOX with SCALE
|
||||||
// При expansion: плавно растёт через SCALE (не размер!)
|
// 🔥 FIX: ОДИН Box для всех режимов - предотвращает мигание
|
||||||
// Это даёт smooth анимацию как в Telegram
|
// Базовый размер ФИКСИРОВАННЫЙ (baseSizeDp), изменение через graphicsLayer.scale
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
if (avatarState.showBlob) {
|
if (avatarState.showBlob) {
|
||||||
// Базовый размер аватарки
|
// Базовый размер аватарки (ФИКСИРОВАННЫЙ для Box)
|
||||||
val baseSizeDp = ProfileMetaballConstants.AVATAR_SIZE_EXPANDED
|
val baseSizeDp = ProfileMetaballConstants.AVATAR_SIZE_EXPANDED
|
||||||
val baseSizePx = with(density) { baseSizeDp.toPx() }
|
val baseSizePx = with(density) { baseSizeDp.toPx() }
|
||||||
|
|
||||||
// Позиция центра
|
// Позиция центра (из avatarState для collapse, интерполированная для expansion)
|
||||||
val avatarCenterX = avatarState.centerX
|
val avatarCenterX = avatarState.centerX
|
||||||
val avatarCenterY = avatarState.centerY
|
val avatarCenterY = avatarState.centerY
|
||||||
|
|
||||||
// При expansion > 0: используем SCALE для плавного роста
|
// ═══════════════════════════════════════════════════════════
|
||||||
// При collapse: используем обычный размер
|
// UNIFIED SCALE для всех режимов:
|
||||||
val isExpanding = expansionProgress > 0f
|
// - Normal (expansion=0, collapse=0): scale = 1.0
|
||||||
|
// - Expansion (expansion>0): scale растёт до screenWidth/baseSizePx
|
||||||
|
// - Collapse (collapse>0): scale = avatarState.radius * 2 / baseSizePx
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
val uniformScale: Float
|
||||||
|
val currentCenterX: Float
|
||||||
|
val currentCenterY: Float
|
||||||
|
val cornerRadiusPx: Float
|
||||||
|
val applyBlur: Boolean
|
||||||
|
|
||||||
if (isExpanding) {
|
when {
|
||||||
// ═══════════════════════════════════════════════════════════
|
expansionProgress > 0f -> {
|
||||||
// EXPANSION MODE - TELEGRAM STYLE
|
// EXPANSION MODE
|
||||||
// Круг растёт И СРАЗУ превращается в квадрат - ПАРАЛЛЕЛЬНО!
|
val targetSize = screenWidthPx
|
||||||
// Без задержки - всё происходит одновременно
|
uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress)
|
||||||
// Target size = ширина экрана (квадрат на всю ширину)
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// Центр смещается к центру header
|
||||||
val targetSize = screenWidthPx
|
val targetCenterX = screenWidthPx / 2f
|
||||||
|
val targetCenterY = headerHeightPx / 2f
|
||||||
// UNIFORM scale - одинаковый по X и Y
|
currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
|
||||||
val uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress)
|
currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
|
||||||
|
|
||||||
// Центр смещается к центру header блока
|
// Corner radius: круг → квадрат
|
||||||
val targetCenterX = screenWidthPx / 2f
|
cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
|
||||||
val targetCenterY = headerHeightPx / 2f
|
applyBlur = false
|
||||||
val currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
|
}
|
||||||
val currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
|
collapseProgress > 0f -> {
|
||||||
|
// COLLAPSE MODE - используем avatarState.radius через scale
|
||||||
// Offset для позиционирования (scale от центра)
|
uniformScale = (avatarState.radius * 2f) / baseSizePx
|
||||||
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
|
currentCenterX = avatarCenterX
|
||||||
val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() }
|
currentCenterY = avatarCenterY
|
||||||
|
// cornerRadius нужен относительно BASE size, поэтому делим на scale
|
||||||
// Corner radius: круг → квадрат СРАЗУ, без задержки!
|
cornerRadiusPx = avatarState.cornerRadius / uniformScale
|
||||||
// Telegram: lerp(smallRadius, 0, expandProgress) - напрямую!
|
applyBlur = avatarState.blurRadius > 0.5f
|
||||||
val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
|
}
|
||||||
val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() }
|
else -> {
|
||||||
|
// NORMAL MODE
|
||||||
Box(
|
uniformScale = 1f
|
||||||
modifier = Modifier
|
currentCenterX = avatarCenterX
|
||||||
.offset(x = offsetX, y = offsetY)
|
currentCenterY = avatarCenterY
|
||||||
.width(baseSizeDp)
|
cornerRadiusPx = baseSizePx / 2f // Полный круг
|
||||||
.height(baseSizeDp)
|
applyBlur = false
|
||||||
.graphicsLayer {
|
}
|
||||||
this.scaleX = uniformScale
|
|
||||||
this.scaleY = uniformScale
|
|
||||||
alpha = avatarState.opacity
|
|
||||||
}
|
|
||||||
.clip(RoundedCornerShape(cornerRadiusDp)),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
content = avatarContent
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
|
||||||
// COLLAPSE/NORMAL MODE - обычный размер для капли
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
|
||||||
val avatarSizeDp = with(density) { (avatarState.radius * 2f).toDp() }
|
|
||||||
val avatarOffsetX = with(density) { (avatarCenterX - avatarState.radius).toDp() }
|
|
||||||
val avatarOffsetY = with(density) { (avatarCenterY - avatarState.radius).toDp() }
|
|
||||||
val cornerRadiusDp = with(density) { avatarState.cornerRadius.toDp() }
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.offset(x = avatarOffsetX, y = avatarOffsetY)
|
|
||||||
.width(avatarSizeDp)
|
|
||||||
.height(avatarSizeDp)
|
|
||||||
.clip(RoundedCornerShape(cornerRadiusDp))
|
|
||||||
.graphicsLayer {
|
|
||||||
alpha = avatarState.opacity
|
|
||||||
if (avatarState.blurRadius > 0.5f) {
|
|
||||||
renderEffect = RenderEffect.createBlurEffect(
|
|
||||||
avatarState.blurRadius,
|
|
||||||
avatarState.blurRadius,
|
|
||||||
Shader.TileMode.DECAL
|
|
||||||
).asComposeRenderEffect()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
content = avatarContent
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Offset: Box имеет ФИКСИРОВАННЫЙ размер, scale применяется от центра
|
||||||
|
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
|
||||||
|
val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() }
|
||||||
|
val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() }
|
||||||
|
|
||||||
|
// 🔥 ЕДИНЫЙ BOX - без if/else переключения между composables
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(x = offsetX, y = offsetY)
|
||||||
|
.width(baseSizeDp) // ФИКСИРОВАННЫЙ
|
||||||
|
.height(baseSizeDp) // ФИКСИРОВАННЫЙ
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = uniformScale
|
||||||
|
scaleY = uniformScale
|
||||||
|
alpha = avatarState.opacity
|
||||||
|
if (applyBlur) {
|
||||||
|
renderEffect = RenderEffect.createBlurEffect(
|
||||||
|
avatarState.blurRadius,
|
||||||
|
avatarState.blurRadius,
|
||||||
|
Shader.TileMode.DECAL
|
||||||
|
).asComposeRenderEffect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clip(RoundedCornerShape(cornerRadiusDp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
content = avatarContent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -657,7 +659,9 @@ fun ProfileMetaballOverlayCompat(
|
|||||||
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() }
|
val dp18 = with(density) { ProfileMetaballConstants.THRESHOLD_ALPHA_END.toPx() }
|
||||||
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() }
|
val dp22 = with(density) { ProfileMetaballConstants.ROUNDED_RECT_RADIUS.toPx() }
|
||||||
|
|
||||||
val avatarState by remember(collapseProgress, expansionProgress, screenWidthPx, statusBarHeightPx, headerHeightPx, hasAvatar) {
|
// 🔥 FIX: Убраны volatile keys (collapseProgress, expansionProgress) из remember
|
||||||
|
// derivedStateOf автоматически отслеживает их как зависимости
|
||||||
|
val avatarState by remember(screenWidthPx, statusBarHeightPx, headerHeightPx) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
computeAvatarState(
|
computeAvatarState(
|
||||||
collapseProgress = collapseProgress,
|
collapseProgress = collapseProgress,
|
||||||
@@ -684,62 +688,68 @@ fun ProfileMetaballOverlayCompat(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(RectangleShape) // Clip to bounds - prevents avatar overflow when expanded
|
.clip(RectangleShape) // Clip to bounds - prevents avatar overflow when expanded
|
||||||
) {
|
) {
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 🔥 FIX: ОДИН Box для всех режимов - предотвращает мигание
|
||||||
|
// Базовый размер ФИКСИРОВАННЫЙ, изменение через graphicsLayer.scale
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
if (avatarState.showBlob) {
|
if (avatarState.showBlob) {
|
||||||
val baseSizeDp = ProfileMetaballConstants.AVATAR_SIZE_EXPANDED
|
val baseSizeDp = ProfileMetaballConstants.AVATAR_SIZE_EXPANDED
|
||||||
val baseSizePx = with(density) { baseSizeDp.toPx() }
|
val baseSizePx = with(density) { baseSizeDp.toPx() }
|
||||||
val avatarCenterX = avatarState.centerX
|
val avatarCenterX = avatarState.centerX
|
||||||
val avatarCenterY = avatarState.centerY
|
val avatarCenterY = avatarState.centerY
|
||||||
val isExpanding = expansionProgress > 0f
|
|
||||||
|
|
||||||
if (isExpanding) {
|
// UNIFIED SCALE для всех режимов
|
||||||
// EXPANSION MODE - scale + corner radius СРАЗУ без задержки
|
val uniformScale: Float
|
||||||
// Target size = ширина экрана (квадрат на всю ширину)
|
val currentCenterX: Float
|
||||||
val targetSize = screenWidthPx
|
val currentCenterY: Float
|
||||||
val uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress)
|
val cornerRadiusPx: Float
|
||||||
val targetCenterX = screenWidthPx / 2f
|
|
||||||
val targetCenterY = headerHeightPx / 2f
|
when {
|
||||||
val currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
|
expansionProgress > 0f -> {
|
||||||
val currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
|
// EXPANSION MODE
|
||||||
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
|
val targetSize = screenWidthPx
|
||||||
val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() }
|
uniformScale = lerpFloat(1f, targetSize / baseSizePx, expansionProgress)
|
||||||
// Corner radius уменьшается СРАЗУ, без порога!
|
val targetCenterX = screenWidthPx / 2f
|
||||||
val cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
|
val targetCenterY = headerHeightPx / 2f
|
||||||
val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() }
|
currentCenterX = lerpFloat(avatarCenterX, targetCenterX, expansionProgress)
|
||||||
|
currentCenterY = lerpFloat(avatarCenterY, targetCenterY, expansionProgress)
|
||||||
Box(
|
cornerRadiusPx = lerpFloat(baseSizePx / 2f, 0f, expansionProgress)
|
||||||
modifier = Modifier
|
}
|
||||||
.offset(x = offsetX, y = offsetY)
|
collapseProgress > 0f -> {
|
||||||
.width(baseSizeDp)
|
// COLLAPSE MODE - используем avatarState.radius через scale
|
||||||
.height(baseSizeDp)
|
uniformScale = (avatarState.radius * 2f) / baseSizePx
|
||||||
.graphicsLayer {
|
currentCenterX = avatarCenterX
|
||||||
this.scaleX = uniformScale
|
currentCenterY = avatarCenterY
|
||||||
this.scaleY = uniformScale
|
cornerRadiusPx = avatarState.cornerRadius / uniformScale
|
||||||
alpha = avatarState.opacity
|
}
|
||||||
}
|
else -> {
|
||||||
.clip(RoundedCornerShape(cornerRadiusDp)),
|
// NORMAL MODE
|
||||||
contentAlignment = Alignment.Center,
|
uniformScale = 1f
|
||||||
content = avatarContent
|
currentCenterX = avatarCenterX
|
||||||
)
|
currentCenterY = avatarCenterY
|
||||||
} else {
|
cornerRadiusPx = baseSizePx / 2f
|
||||||
// COLLAPSE/NORMAL MODE
|
}
|
||||||
val avatarSizeDp = with(density) { (avatarState.radius * 2f).toDp() }
|
|
||||||
val avatarOffsetX = with(density) { (avatarCenterX - avatarState.radius).toDp() }
|
|
||||||
val avatarOffsetY = with(density) { (avatarCenterY - avatarState.radius).toDp() }
|
|
||||||
val cornerRadiusDp = with(density) { avatarState.cornerRadius.toDp() }
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.offset(x = avatarOffsetX, y = avatarOffsetY)
|
|
||||||
.width(avatarSizeDp)
|
|
||||||
.height(avatarSizeDp)
|
|
||||||
.clip(RoundedCornerShape(cornerRadiusDp))
|
|
||||||
.graphicsLayer {
|
|
||||||
alpha = avatarState.opacity
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
content = avatarContent
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val offsetX = with(density) { (currentCenterX - baseSizePx / 2f).toDp() }
|
||||||
|
val offsetY = with(density) { (currentCenterY - baseSizePx / 2f).toDp() }
|
||||||
|
val cornerRadiusDp = with(density) { cornerRadiusPx.toDp() }
|
||||||
|
|
||||||
|
// 🔥 ЕДИНЫЙ BOX
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(x = offsetX, y = offsetY)
|
||||||
|
.width(baseSizeDp)
|
||||||
|
.height(baseSizeDp)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = uniformScale
|
||||||
|
scaleY = uniformScale
|
||||||
|
alpha = avatarState.opacity
|
||||||
|
}
|
||||||
|
.clip(RoundedCornerShape(cornerRadiusDp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
content = avatarContent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1047,26 +1047,42 @@ private fun FullSizeAvatar(
|
|||||||
avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||||
?: remember { mutableStateOf(emptyList()) }
|
?: remember { mutableStateOf(emptyList()) }
|
||||||
|
|
||||||
// Сохраняем bitmap в remember чтобы не мигал при recomposition
|
// 🔥 FIX: Используем стабильный ключ (timestamp) вместо reference списка
|
||||||
var bitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
|
// Это предотвращает перезагрузку bitmap при recomposition когда данные не изменились
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
val avatarKey = remember(avatars) {
|
||||||
|
avatars.firstOrNull()?.timestamp ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(avatars) {
|
// 🔥 FIX: Сохраняем bitmap БЕЗ сброса при изменении ключа
|
||||||
if (avatars.isNotEmpty()) {
|
// Предыдущий bitmap показывается пока загружается новый (double buffering)
|
||||||
|
var bitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
|
||||||
|
|
||||||
|
// 🔥 FIX: isLoading только для первой загрузки, не сбрасываем если bitmap уже есть
|
||||||
|
var initialLoadComplete by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 🔥 FIX: Декодируем только когда avatarKey (timestamp) реально изменился
|
||||||
|
// НЕ сбрасываем bitmap в null перед загрузкой - показываем старый пока грузится новый
|
||||||
|
LaunchedEffect(avatarKey) {
|
||||||
|
val currentAvatars = avatars
|
||||||
|
if (currentAvatars.isNotEmpty()) {
|
||||||
val newBitmap = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
val newBitmap = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||||
com.rosetta.messenger.utils.AvatarFileManager.base64ToBitmap(
|
com.rosetta.messenger.utils.AvatarFileManager.base64ToBitmap(
|
||||||
avatars.first().base64Data
|
currentAvatars.first().base64Data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bitmap = newBitmap
|
// Устанавливаем новый bitmap только если декодирование успешно
|
||||||
isLoading = false
|
if (newBitmap != null) {
|
||||||
|
bitmap = newBitmap
|
||||||
|
}
|
||||||
|
initialLoadComplete = true
|
||||||
} else {
|
} else {
|
||||||
isLoading = false
|
// Нет аватарки - помечаем загрузку завершенной
|
||||||
|
initialLoadComplete = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем картинку если есть, иначе placeholder
|
// Показываем картинку если есть, иначе placeholder
|
||||||
// НО не показываем placeholder пока идёт загрузка (чтобы не мигало)
|
// 🔥 FIX: Показываем bitmap сразу если он есть (даже во время загрузки нового)
|
||||||
when {
|
when {
|
||||||
bitmap != null -> {
|
bitmap != null -> {
|
||||||
Image(
|
Image(
|
||||||
@@ -1076,8 +1092,8 @@ private fun FullSizeAvatar(
|
|||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
!isLoading -> {
|
initialLoadComplete -> {
|
||||||
// Placeholder только когда точно нет аватарки
|
// Placeholder только когда точно нет аватарки И загрузка завершена
|
||||||
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
|
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize().background(avatarColors.backgroundColor),
|
modifier = Modifier.fillMaxSize().background(avatarColors.backgroundColor),
|
||||||
@@ -1091,7 +1107,8 @@ private fun FullSizeAvatar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Пока isLoading=true - ничего не показываем (прозрачно)
|
// Пока initialLoadComplete=false - ничего не показываем (прозрачно)
|
||||||
|
// Это только самый первый момент загрузки
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user