feat: Update version to 1.0.6 and enhance release notes with new features and improvements
This commit is contained in:
@@ -92,6 +92,15 @@ fun SelectAccountScreen(
|
||||
LaunchedEffect(Unit) {
|
||||
visible = true
|
||||
}
|
||||
|
||||
// Status bar icons: black on light theme, white on dark
|
||||
val view = androidx.compose.ui.platform.LocalView.current
|
||||
DisposableEffect(isDarkTheme) {
|
||||
val window = (view.context as android.app.Activity).window
|
||||
val insetsController = androidx.core.view.WindowCompat.getInsetsController(window, view)
|
||||
insetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -653,7 +653,7 @@ fun ChatsListScreen(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
if (isDarkTheme) Color(0xFF2A2A2A) else PrimaryBlue
|
||||
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF228BE6)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1117,7 +1117,36 @@ fun ChatsListScreen(
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Telegram-style scale animation: main content shrinks when drawer opens
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val drawerWidthPx = with(LocalDensity.current) { 300.dp.toPx() }
|
||||
val translateXPx = with(LocalDensity.current) { 4.dp.toPx() }
|
||||
val density = LocalDensity.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
.graphicsLayer {
|
||||
@Suppress("DEPRECATION")
|
||||
val offset = drawerState.offset.value
|
||||
val progress = if (offset.isNaN()) 0f
|
||||
else ((drawerWidthPx + offset) / drawerWidthPx).coerceIn(0f, 1f)
|
||||
|
||||
if (progress > 0.001f) {
|
||||
val s = 1f - 0.07f * progress
|
||||
scaleX = s
|
||||
scaleY = s
|
||||
translationX = translateXPx * progress
|
||||
transformOrigin = androidx.compose.ui.graphics.TransformOrigin(1f, 0f)
|
||||
val cornerPx = with(density) { (16.dp * progress).toPx() }
|
||||
shape = RoundedCornerShape(cornerPx)
|
||||
clip = true
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
key(isDarkTheme, showRequestsScreen, isSelectionMode) {
|
||||
@@ -1247,8 +1276,8 @@ fun ChatsListScreen(
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||
scrolledContainerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF228BE6),
|
||||
scrolledContainerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF228BE6),
|
||||
navigationIconContentColor = Color.White,
|
||||
titleContentColor = Color.White,
|
||||
actionIconContentColor = Color.White
|
||||
@@ -1320,7 +1349,7 @@ fun ChatsListScreen(
|
||||
.defaultMinSize(minWidth = 20.dp, minHeight = 20.dp)
|
||||
.clip(badgeShape)
|
||||
.background(
|
||||
color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4)
|
||||
color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF228BE6)
|
||||
)
|
||||
.padding(2.dp)
|
||||
.clip(badgeShape)
|
||||
@@ -1331,7 +1360,7 @@ fun ChatsListScreen(
|
||||
text = badgeText,
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF0D8CF4),
|
||||
color = Color(0xFF228BE6),
|
||||
lineHeight = 10.sp,
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||
)
|
||||
@@ -1420,9 +1449,9 @@ fun ChatsListScreen(
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor =
|
||||
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF228BE6),
|
||||
scrolledContainerColor =
|
||||
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF228BE6),
|
||||
navigationIconContentColor =
|
||||
Color.White,
|
||||
titleContentColor =
|
||||
|
||||
@@ -197,8 +197,8 @@ fun ImageEditorScreen(
|
||||
animationProgress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 250,
|
||||
easing = FastOutSlowInEasing
|
||||
durationMillis = 300,
|
||||
easing = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -417,6 +417,7 @@ fun ImageEditorScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = animatedBackgroundAlpha }
|
||||
.background(Color.Black)
|
||||
.zIndex(100f)
|
||||
.pointerInput(Unit) {
|
||||
@@ -429,7 +430,17 @@ fun ImageEditorScreen(
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.graphicsLayer { alpha = animationProgress.value }
|
||||
.graphicsLayer {
|
||||
scaleX = animatedScale
|
||||
scaleY = animatedScale
|
||||
translationX = animatedTranslationX
|
||||
translationY = animatedTranslationY
|
||||
if (animatedCornerRadius > 0f) {
|
||||
shape = RoundedCornerShape(animatedCornerRadius)
|
||||
clip = true
|
||||
}
|
||||
alpha = animationProgress.value
|
||||
}
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📸 FULLSCREEN PHOTO - занимает ВЕСЬ экран, не реагирует на клавиатуру
|
||||
@@ -443,10 +454,10 @@ fun ImageEditorScreen(
|
||||
setPadding(0, 0, 0, 0)
|
||||
setBackgroundColor(android.graphics.Color.BLACK)
|
||||
|
||||
// Простой FIT_CENTER - показывает ВСЁ фото, центрирует
|
||||
// Edge-to-edge: фото заполняет весь экран без чёрных полос
|
||||
source.apply {
|
||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
adjustViewBounds = true
|
||||
scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
adjustViewBounds = false
|
||||
setPadding(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@ 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.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -20,6 +23,7 @@ import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.utils.AvatarFileManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -29,14 +33,6 @@ import kotlinx.coroutines.withContext
|
||||
* Компонент для отображения размытого фона аватарки
|
||||
* Используется в профиле и сайдбаре
|
||||
* ВАЖНО: Должен вызываться внутри BoxScope чтобы matchParentSize() работал
|
||||
*
|
||||
* @param publicKey Публичный ключ пользователя
|
||||
* @param avatarRepository Репозиторий аватаров
|
||||
* @param fallbackColor Цвет фона если нет аватарки
|
||||
* @param blurRadius Радиус размытия (в пикселях) - применяется при обработке
|
||||
* @param alpha Прозрачность (0.0 - 1.0)
|
||||
* @param overlayColors Опциональные цвета overlay поверх blur. Если null — стандартное поведение.
|
||||
* Если 1 цвет — сплошной overlay, если 2+ — градиент.
|
||||
*/
|
||||
@Composable
|
||||
fun BoxScope.BlurredAvatarBackground(
|
||||
@@ -50,16 +46,13 @@ fun BoxScope.BlurredAvatarBackground(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// Загрузка и blur аватарки — нужна ВСЕГДА (и для overlay-цветов, и для обычного режима)
|
||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||
?: remember { mutableStateOf(emptyList()) }
|
||||
|
||||
// Stable key based on content, not list reference — prevents bitmap reset during recomposition
|
||||
val avatarKey = remember(avatars) {
|
||||
avatars.firstOrNull()?.timestamp ?: 0L
|
||||
}
|
||||
|
||||
// Don't reset bitmap to null when key changes — keep showing old blur until new one is ready
|
||||
var originalBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
var blurredBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
@@ -72,7 +65,7 @@ fun BoxScope.BlurredAvatarBackground(
|
||||
if (newOriginal != null) {
|
||||
originalBitmap = newOriginal
|
||||
blurredBitmap = withContext(Dispatchers.Default) {
|
||||
gaussianBlur(context, newOriginal, radius = 25f, passes = 3)
|
||||
gaussianBlur(context, newOriginal, radius = 20f, passes = 2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -82,24 +75,22 @@ fun BoxScope.BlurredAvatarBackground(
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Если выбран overlay-цвет — рисуем blur + пастельный overlay
|
||||
// (повторяет логику ProfileBlurPreview из AppearanceScreen)
|
||||
// Режим с overlay-цветом — blur + пастельный overlay
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||
// LAYER 1: Blurred avatar underneath (если есть)
|
||||
// LAYER 1: Blurred avatar (яркий, видный)
|
||||
if (blurredBitmap != null) {
|
||||
Image(
|
||||
bitmap = blurredBitmap!!.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.matchParentSize()
|
||||
.graphicsLayer { this.alpha = 0.35f },
|
||||
.graphicsLayer { this.alpha = 0.85f },
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
// LAYER 2: Цвет/градиент overlay с пониженной прозрачностью
|
||||
// С blur-подложкой — 55%, без — 85% (как в AppearanceScreen preview)
|
||||
val colorAlpha = if (blurredBitmap != null) 0.55f else 0.85f
|
||||
// LAYER 2: Цвет/градиент overlay
|
||||
val colorAlpha = if (blurredBitmap != null) 0.4f else 0.85f
|
||||
val overlayMod = if (overlayColors.size == 1) {
|
||||
Modifier.matchParentSize()
|
||||
.background(overlayColors[0].copy(alpha = colorAlpha))
|
||||
@@ -112,30 +103,58 @@ fun BoxScope.BlurredAvatarBackground(
|
||||
)
|
||||
}
|
||||
Box(modifier = overlayMod)
|
||||
|
||||
// LAYER 3: Нижний градиент для читаемости текста
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.2f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Стандартный режим (нет overlay-цвета) — blur аватарки или fallback
|
||||
// Повторяет логику AppearanceScreen → ProfileBlurPreview:
|
||||
// blur 35% alpha + цвет-тинт 30% alpha
|
||||
// Стандартный режим (нет overlay-цвета) — blur аватарки
|
||||
// Telegram-style: яркий видный блюр + мягкое затемнение
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (blurredBitmap != null) {
|
||||
// LAYER 1: Размытая аватарка — яркая и видная
|
||||
Image(
|
||||
bitmap = blurredBitmap!!.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.matchParentSize()
|
||||
.graphicsLayer { this.alpha = 0.35f },
|
||||
.graphicsLayer { this.alpha = 0.9f },
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
// Тонкий цветовой тинт поверх blur (как в AppearanceScreen preview)
|
||||
// LAYER 2: Лёгкое тонирование цветом аватара
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(fallbackColor.copy(alpha = 0.3f))
|
||||
.background(fallbackColor.copy(alpha = 0.12f))
|
||||
)
|
||||
// LAYER 3: Мягкий нижний градиент для текста
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.25f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Нет фото — fallback: серый в светлой, тёмный в тёмной (как в AppearanceScreen)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
@@ -147,17 +166,47 @@ fun BoxScope.BlurredAvatarBackground(
|
||||
}
|
||||
|
||||
/**
|
||||
* Proper Gaussian blur via RenderScript — smooth, non-pixelated.
|
||||
* Scales down to 1/4 for performance, applies ScriptIntrinsicBlur (radius 25 max),
|
||||
* then repeats for heavier blur. Result stays at 1/4 scale (enough for backgrounds).
|
||||
* Gaussian blur via RenderScript.
|
||||
* Pads the image with mirrored edges before blurring to eliminate edge banding artifacts,
|
||||
* then crops back to original size.
|
||||
*/
|
||||
@Suppress("deprecation")
|
||||
private fun gaussianBlur(context: Context, source: Bitmap, radius: Float = 25f, passes: Int = 3): Bitmap {
|
||||
// Scale down for performance (1/4 res is plenty for a background)
|
||||
val w = (source.width / 4).coerceAtLeast(8)
|
||||
val h = (source.height / 4).coerceAtLeast(8)
|
||||
var current = Bitmap.createScaledBitmap(source, w, h, true)
|
||||
.copy(Bitmap.Config.ARGB_8888, true)
|
||||
private fun gaussianBlur(context: Context, source: Bitmap, radius: Float = 20f, passes: Int = 2): Bitmap {
|
||||
// Scale down for performance
|
||||
val w = (source.width / 3).coerceAtLeast(32)
|
||||
val h = (source.height / 3).coerceAtLeast(32)
|
||||
val scaled = Bitmap.createScaledBitmap(source, w, h, true)
|
||||
|
||||
// Add padding around the image to prevent edge clamping artifacts
|
||||
val pad = (radius * passes).toInt().coerceAtLeast(8)
|
||||
val paddedW = w + pad * 2
|
||||
val paddedH = h + pad * 2
|
||||
val padded = Bitmap.createBitmap(paddedW, paddedH, Bitmap.Config.ARGB_8888)
|
||||
val canvas = android.graphics.Canvas(padded)
|
||||
|
||||
// Draw center
|
||||
canvas.drawBitmap(scaled, pad.toFloat(), pad.toFloat(), null)
|
||||
|
||||
// Mirror edges: left, right, top, bottom strips
|
||||
for (x in 0 until pad) {
|
||||
for (y in 0 until h) {
|
||||
val pixel = scaled.getPixel((pad - x - 1).coerceIn(0, w - 1), y)
|
||||
padded.setPixel(x, y + pad, pixel)
|
||||
val pixel2 = scaled.getPixel((w - 1 - (x.coerceIn(0, w - 1))), y)
|
||||
padded.setPixel(paddedW - 1 - x, y + pad, pixel2)
|
||||
}
|
||||
}
|
||||
for (y in 0 until pad) {
|
||||
for (x in 0 until paddedW) {
|
||||
val srcX = (x - pad).coerceIn(0, w - 1)
|
||||
val pixel = scaled.getPixel(srcX, (pad - y - 1).coerceIn(0, h - 1))
|
||||
padded.setPixel(x, y, pixel)
|
||||
val pixel2 = scaled.getPixel(srcX, (h - 1 - y.coerceIn(0, h - 1)))
|
||||
padded.setPixel(x, paddedH - 1 - y, pixel2)
|
||||
}
|
||||
}
|
||||
|
||||
var current = padded.copy(Bitmap.Config.ARGB_8888, true)
|
||||
|
||||
val rs = RenderScript.create(context)
|
||||
val blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
|
||||
@@ -175,5 +224,7 @@ private fun gaussianBlur(context: Context, source: Bitmap, radius: Float = 25f,
|
||||
|
||||
blur.destroy()
|
||||
rs.destroy()
|
||||
return current
|
||||
|
||||
// Crop back to original size (remove padding)
|
||||
return Bitmap.createBitmap(current, pad, pad, w, h)
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ private fun ProfileBlurPreview(
|
||||
if (decoded != null) {
|
||||
avatarBitmap = decoded
|
||||
blurredBitmap = withContext(Dispatchers.Default) {
|
||||
appearanceGaussianBlur(blurContext, decoded, radius = 25f, passes = 3)
|
||||
appearanceGaussianBlur(blurContext, decoded, radius = 20f, passes = 2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -373,15 +373,17 @@ private fun ProfileBlurPreview(
|
||||
.height(280.dp + statusBarHeight)
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════
|
||||
// LAYER 1: Blurred avatar background (как в профиле)
|
||||
// LAYER 1: Blurred avatar background (идентично BlurredAvatarBackground)
|
||||
// ═══════════════════════════════════════════════════
|
||||
if (blurredBitmap != null) {
|
||||
// overlay-режим: 0.85f, стандартный: 0.9f (как в BlurredAvatarBackground)
|
||||
val blurImgAlpha = if (overlayColors != null && overlayColors.isNotEmpty()) 0.85f else 0.9f
|
||||
Image(
|
||||
bitmap = blurredBitmap!!.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = 0.35f },
|
||||
.graphicsLayer { alpha = blurImgAlpha },
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
@@ -400,25 +402,25 @@ private fun ProfileBlurPreview(
|
||||
val overlayMod = if (overlayColors.size == 1) {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(overlayColors[0].copy(alpha = if (blurredBitmap != null) 0.55f else 0.85f))
|
||||
.background(overlayColors[0].copy(alpha = if (blurredBitmap != null) 0.4f else 0.85f))
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = overlayColors.map {
|
||||
it.copy(alpha = if (blurredBitmap != null) 0.55f else 0.85f)
|
||||
it.copy(alpha = if (blurredBitmap != null) 0.4f else 0.85f)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
Box(modifier = overlayMod)
|
||||
} else if (blurredBitmap != null) {
|
||||
// Стандартный затемняющий overlay (как в профиле)
|
||||
// Стандартный тинт (идентичен BlurredAvatarBackground)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(avatarColors.backgroundColor.copy(alpha = 0.3f))
|
||||
.background(avatarColors.backgroundColor.copy(alpha = 0.12f))
|
||||
)
|
||||
} else {
|
||||
// Нет аватарки и нет overlay — fallback цвет
|
||||
@@ -432,18 +434,17 @@ private fun ProfileBlurPreview(
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// LAYER 3: Тонкий нижний градиент-затемнение
|
||||
// LAYER 3: Нижний градиент (идентичен BlurredAvatarBackground)
|
||||
// ═══════════════════════════════════════════════════
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.35f)
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = if (selectedId == "none") 0f else 0.2f)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -731,13 +732,37 @@ private fun ColorCircleItem(
|
||||
|
||||
/**
|
||||
* Proper Gaussian blur via RenderScript — smooth, non-pixelated.
|
||||
* Mirrors edges to eliminate banding artifacts.
|
||||
*/
|
||||
@Suppress("deprecation")
|
||||
private fun appearanceGaussianBlur(context: Context, source: Bitmap, radius: Float = 25f, passes: Int = 3): Bitmap {
|
||||
val w = (source.width / 4).coerceAtLeast(8)
|
||||
val h = (source.height / 4).coerceAtLeast(8)
|
||||
var current = Bitmap.createScaledBitmap(source, w, h, true)
|
||||
.copy(Bitmap.Config.ARGB_8888, true)
|
||||
private fun appearanceGaussianBlur(context: Context, source: Bitmap, radius: Float = 20f, passes: Int = 2): Bitmap {
|
||||
val w = (source.width / 3).coerceAtLeast(32)
|
||||
val h = (source.height / 3).coerceAtLeast(32)
|
||||
val scaled = Bitmap.createScaledBitmap(source, w, h, true)
|
||||
|
||||
// Pad with mirrored edges to eliminate edge banding
|
||||
val pad = (radius * passes).toInt().coerceAtLeast(8)
|
||||
val paddedW = w + pad * 2
|
||||
val paddedH = h + pad * 2
|
||||
val padded = Bitmap.createBitmap(paddedW, paddedH, Bitmap.Config.ARGB_8888)
|
||||
val canvas = android.graphics.Canvas(padded)
|
||||
canvas.drawBitmap(scaled, pad.toFloat(), pad.toFloat(), null)
|
||||
|
||||
for (x in 0 until pad) {
|
||||
for (y in 0 until h) {
|
||||
padded.setPixel(x, y + pad, scaled.getPixel((pad - x - 1).coerceIn(0, w - 1), y))
|
||||
padded.setPixel(paddedW - 1 - x, y + pad, scaled.getPixel((w - 1 - x.coerceIn(0, w - 1)), y))
|
||||
}
|
||||
}
|
||||
for (y in 0 until pad) {
|
||||
for (x in 0 until paddedW) {
|
||||
val srcX = (x - pad).coerceIn(0, w - 1)
|
||||
padded.setPixel(x, y, scaled.getPixel(srcX, (pad - y - 1).coerceIn(0, h - 1)))
|
||||
padded.setPixel(x, paddedH - 1 - y, scaled.getPixel(srcX, (h - 1 - y.coerceIn(0, h - 1))))
|
||||
}
|
||||
}
|
||||
|
||||
var current = padded.copy(Bitmap.Config.ARGB_8888, true)
|
||||
|
||||
val rs = RenderScript.create(context)
|
||||
val blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
|
||||
@@ -755,6 +780,6 @@ private fun appearanceGaussianBlur(context: Context, source: Bitmap, radius: Flo
|
||||
|
||||
blur.destroy()
|
||||
rs.destroy()
|
||||
return current
|
||||
return Bitmap.createBitmap(current, pad, pad, w, h)
|
||||
}
|
||||
|
||||
|
||||
@@ -1936,7 +1936,7 @@ private fun CollapsingOtherProfileHeader(
|
||||
verified = if (verified > 0) verified else 1,
|
||||
size = (nameFontSize.value * 0.8f).toInt(),
|
||||
isDarkTheme = isDarkTheme,
|
||||
badgeTint = if (isRosettaOfficial) rosettaBadgeBlue else null
|
||||
badgeTint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1403,7 +1403,7 @@ private fun CollapsingProfileHeader(
|
||||
verified = 2,
|
||||
size = (nameFontSize.value * 0.8f).toInt(),
|
||||
isDarkTheme = isDarkTheme,
|
||||
badgeTint = rosettaBadgeBlue
|
||||
badgeTint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user