feat(splash+icon): фон иконки-градиент под тему + темо-зависимый сплэш
- Подложка adaptive-иконки: градиент под акцент темы + радиальное свечение + мягкая тень от логотипа (ic_bg_<тема>, было плоским цветом). Иконку-лого не трогал. - Сплэш под выбранную тему: системный сплэш Android 12+ нельзя перекрасить под выбор пользователя (alias-тема на ColorOS игнорится), поэтому системный = просто тёмный (splash_transparent), а красивый сплэш рисуем сами на Compose (SplashOverlay): 3D-лого + акцентное свечение + тень + анимация, цвет берём из текущей темы. - Тему на старте читаем синхронно из SharedPreferences (мгновенно, без блокировки кадра). - Ускорен холодный старт до первого кадра 1.48с→1.11с: сплэш рисуется на первом дешёвом кадре, тяжёлый контент (ViewModels/плеер) композится под ним; старт PlayerService уведён с критического пути. Остаток — оверхед debug-сборки.
This commit is contained in:
@@ -61,20 +61,39 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
startService(Intent(this, PlayerService::class.java))
|
||||
lifecycleScope.launch {
|
||||
tokenDataStore.preload()
|
||||
// Старт плеер-сервиса уводим с критического пути запуска — ускоряет
|
||||
// появление первого кадра (сплэша).
|
||||
startService(Intent(this@MainActivity, PlayerService::class.java))
|
||||
}
|
||||
ensureBackgroundPlaybackAllowed()
|
||||
enableEdgeToEdge()
|
||||
// Тему берём из быстрого SharedPreferences (его пишет LauncherIconManager при
|
||||
// смене темы) — синхронно и МГНОВЕННО, без блокировки первого кадра. Так сплэш
|
||||
// и приложение сразу нужного цвета, и тёмный системный сплэш не висит лишнее.
|
||||
val initialPaletteId = getSharedPreferences("radiola_prefs", MODE_PRIVATE)
|
||||
.getString("icon_alias", "forest") ?: "forest"
|
||||
setContent {
|
||||
// Выбранная цветовая тема (мгновенно перекрашивает всё приложение).
|
||||
val paletteId by settingsRepository.getThemePalette().collectAsState(initial = "forest")
|
||||
val paletteId by settingsRepository.getThemePalette().collectAsState(initial = initialPaletteId)
|
||||
// Иконка лаунчера следует теме (срабатывает на старте и при смене темы).
|
||||
LaunchedEffect(paletteId) {
|
||||
launcherIconManager.applyIfNeeded(com.radiola.ui.theme.ThemePalette.fromId(paletteId))
|
||||
}
|
||||
RadiolaTheme(palette = com.radiola.ui.theme.ThemePalette.fromId(paletteId)) {
|
||||
// Сплэш рисуем на ПЕРВОМ (дешёвом) кадре; тяжёлый контент (ViewModels,
|
||||
// плеер) композим следующим кадром ПОД сплэшем — так логотип появляется
|
||||
// почти сразу, без долгого тёмного ожидания холодного старта.
|
||||
var showSplash by remember { mutableStateOf(true) }
|
||||
var contentReady by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) { contentReady = true }
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(1600)
|
||||
showSplash = false
|
||||
}
|
||||
|
||||
if (contentReady) {
|
||||
val navController = rememberNavController()
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
var showPlayer by remember { mutableStateOf(false) }
|
||||
@@ -298,6 +317,20 @@ class MainActivity : ComponentActivity() {
|
||||
onDismiss = { if (!update.forceUpdate) pendingUpdate = null }
|
||||
)
|
||||
}
|
||||
|
||||
} // конец if (contentReady): тяжёлый контент композится под сплэшем
|
||||
|
||||
// Тематический экран загрузки поверх всего (рисуем сами — системный
|
||||
// сплэш Android 12+ нельзя перекрасить под выбранную тему).
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = showSplash,
|
||||
enter = androidx.compose.animation.EnterTransition.None,
|
||||
exit = androidx.compose.animation.fadeOut(androidx.compose.animation.core.tween(450))
|
||||
) {
|
||||
com.radiola.ui.components.SplashOverlay(
|
||||
com.radiola.ui.theme.ThemePalette.fromId(paletteId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
app/src/main/java/com/radiola/ui/components/SplashOverlay.kt
Normal file
96
app/src/main/java/com/radiola/ui/components/SplashOverlay.kt
Normal file
@@ -0,0 +1,96 @@
|
||||
package com.radiola.ui.components
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.radiola.ui.theme.RadiolaWordmark
|
||||
import com.radiola.ui.theme.ThemePalette
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Свой экран загрузки: тематический 3D-логотип с акцентным свечением под цвет темы,
|
||||
* мягкой тенью и плавным появлением. Рисуем сами (а не системный сплэш), потому что
|
||||
* Android 12+ не даёт менять иконку системного сплэша под выбранную пользователем тему.
|
||||
*/
|
||||
@Composable
|
||||
fun SplashOverlay(palette: ThemePalette, modifier: Modifier = Modifier) {
|
||||
val colors = palette.colors
|
||||
// Появление снапное: лого видно почти сразу (короткий fade), затем мягкий «вдох».
|
||||
val scale = remember { Animatable(0.92f) }
|
||||
val fade = remember { Animatable(0f) }
|
||||
LaunchedEffect(Unit) {
|
||||
launch { fade.animateTo(1f, tween(200)) }
|
||||
launch { scale.animateTo(1f, tween(420, easing = FastOutSlowInEasing)) }
|
||||
}
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize().background(colors.bgBase),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Акцентное свечение под цвет темы
|
||||
Box(
|
||||
Modifier
|
||||
.size(380.dp)
|
||||
.graphicsLayer { alpha = fade.value }
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
listOf(
|
||||
colors.accent.copy(alpha = 0.32f),
|
||||
colors.accent.copy(alpha = 0.10f),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
// Мягкая тень логотипа
|
||||
Image(
|
||||
painter = painterResource(palette.logoRes),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(Color.Black.copy(alpha = 0.5f)),
|
||||
modifier = Modifier
|
||||
.size(176.dp)
|
||||
.scale(scale.value)
|
||||
.offset(y = 14.dp)
|
||||
.blur(18.dp)
|
||||
.graphicsLayer { alpha = fade.value }
|
||||
)
|
||||
// Логотип
|
||||
Image(
|
||||
painter = painterResource(palette.logoRes),
|
||||
contentDescription = "radiOLA",
|
||||
modifier = Modifier
|
||||
.size(176.dp)
|
||||
.scale(scale.value)
|
||||
.graphicsLayer { alpha = fade.value }
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Box(Modifier.graphicsLayer { alpha = fade.value }) {
|
||||
RadiolaWordmark(fontSize = 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user