diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarHomeScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarHomeScreen.kt index c33ca80..4919429 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarHomeScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarHomeScreen.kt @@ -212,9 +212,10 @@ private fun DockedPlayer(now: NowPlaying, modifier: Modifier) { val scheme = MaterialTheme.colorScheme val art by produceState(now.art, now.art, now.artUri) { value = now.art ?: MediaArt.load(context, now.artUri) } - // The theme's surface rather than the art's colour: a slab of album colour beside the map - // fought it for attention, and the art says the same thing in less space. + // The theme's surface, with a faint wash of the cover's colours behind the top of it. A solid + // slab of album colour beside the map fought it for attention; this only hints at the song. Surface(modifier = modifier.gsPanelShadow(CARD_SHAPE), shape = CARD_SHAPE, color = scheme.surfaceContainer) { + AlbumGlow(art, Modifier.fillMaxSize()) Box(Modifier.fillMaxSize().padding(12.dp)) { Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { // The cover gets everything the controls leave, as big a square as fits, sitting @@ -268,6 +269,46 @@ private fun DockedPlayer(now: NowPlaying, modifier: Modifier) { } } +/** + * Three soft pools of the cover's colours: top left, top right and the middle, read off a 3x3 + * shrink of it. Still, and redrawn only while a new song's colours fade in, so it costs the video + * stream nothing between songs. Fainter on a light card, where colour shows more. + */ +@Composable +private fun AlbumGlow(art: android.graphics.Bitmap?, modifier: Modifier) { + val spots by produceState>(emptyList(), art) { + value = art?.let { kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Default) { glowColours(it) } }.orEmpty() + } + val strength = if (MaterialTheme.colorScheme.surface.luminance() < 0.5f) 0.30f else 0.18f + val fade = tween(600) + val first by androidx.compose.animation.animateColorAsState(spots.getOrNull(0)?.copy(alpha = strength) ?: Color.Transparent, fade, label = "glow") + val second by androidx.compose.animation.animateColorAsState(spots.getOrNull(1)?.copy(alpha = strength) ?: Color.Transparent, fade, label = "glow") + val third by androidx.compose.animation.animateColorAsState(spots.getOrNull(2)?.copy(alpha = strength * 0.8f) ?: Color.Transparent, fade, label = "glow") + androidx.compose.foundation.Canvas(modifier) { + fun pool(colour: Color, x: Float, y: Float, reach: Float) { + if (colour.alpha <= 0f) return + drawRect( + androidx.compose.ui.graphics.Brush.radialGradient( + listOf(colour, Color.Transparent), + center = androidx.compose.ui.geometry.Offset(size.width * x, size.height * y), + radius = size.width * reach, + ), + ) + } + pool(third, 0.5f, 0.5f, 0.85f) + pool(first, 0.2f, 0.1f, 0.9f) + pool(second, 0.85f, 0.22f, 0.8f) + } +} + +/** The cover's colours at its top left, top right and middle. */ +private fun glowColours(art: android.graphics.Bitmap): List { + // A hardware bitmap can't be scaled or read on the CPU, so copy it out first. + val readable = if (art.config == android.graphics.Bitmap.Config.HARDWARE) art.copy(android.graphics.Bitmap.Config.ARGB_8888, false) else art + val small = android.graphics.Bitmap.createScaledBitmap(readable ?: return emptyList(), 3, 3, true) + return listOf(small.getPixel(0, 0), small.getPixel(2, 0), small.getPixel(1, 1)).map { Color(it).copy(alpha = 1f) } +} + /** * Lyrics alone beside the map, big enough to read at a glance. The header is slim, since the * player goes back to the nav bar meanwhile and names the song there. Its note button swaps the diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt index aaae20d..6c148a1 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt @@ -62,6 +62,8 @@ import androidx.compose.runtime.setValue import androidx.compose.animation.animateContentSize import android.media.session.PlaybackState import androidx.compose.runtime.remember +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -275,30 +277,46 @@ fun CarUi() { // Every other screen, and everything that pops up, in a layer over Home rather // than inside its area: full screen grows this layer over the nav bar's row while // Home, and the map's surface in it, keeps its size. + // Each new screen fades in while rising a little, once, over a fifth of a second. + // Coming from Home the backdrop fades in with it, over the map; between two other + // screens it stays put, so the map never flashes through. + val lastScreen = remember { arrayOf(CarScreen.Home) } + val fromHome = remember(screen) { lastScreen[0] == CarScreen.Home } + SideEffect { lastScreen[0] = screen } + val enter = remember(screen) { androidx.compose.animation.core.Animatable(0f) } + LaunchedEffect(screen) { enter.animateTo(1f, androidx.compose.animation.core.tween(180)) } if (safetyWarningSeen) { Box(Modifier.fillMaxSize()) { if (screen != CarScreen.Home) { Box( Modifier .fillMaxSize() + .graphicsLayer { if (fromHome) alpha = enter.value } .background(MaterialTheme.colorScheme.background) // The bar never hides here, so keep the screen clear of it. .padding(bottom = if (!keyboardVisible && !fullscreen) NAV_BAR_HEIGHT else 0.dp) // Keeps touches that miss this screen's controls off the map. .pointerInput(Unit) { detectTapGestures { } }, ) { - when (screen) { - CarScreen.Home -> Unit - CarScreen.Apps -> CarLauncher() - CarScreen.Media -> mediaApp?.let { MediaScreen(it) { navigator.back() } } - CarScreen.Browse -> BrowseAppScreen(navigator) - CarScreen.Settings -> CarSettingsScreen() - CarScreen.Dashboard -> CarDashboardScreen() - CarScreen.Weather -> WeatherScreen() - CarScreen.VehicleData -> VehicleDataScreen() - is CarScreen.Notifications -> NotificationsScreen(screen.replyTo) - is CarScreen.Messages -> MessagesScreen(screen.packageName) - is CarScreen.App -> CarApps.find(screen.id)?.content?.invoke() + Box( + Modifier.fillMaxSize().graphicsLayer { + if (!fromHome) alpha = enter.value + translationY = (1f - enter.value) * 16.dp.toPx() + }, + ) { + when (screen) { + CarScreen.Home -> Unit + CarScreen.Apps -> CarLauncher() + CarScreen.Media -> mediaApp?.let { MediaScreen(it) { navigator.back() } } + CarScreen.Browse -> BrowseAppScreen(navigator) + CarScreen.Settings -> CarSettingsScreen() + CarScreen.Dashboard -> CarDashboardScreen() + CarScreen.Weather -> WeatherScreen() + CarScreen.VehicleData -> VehicleDataScreen() + is CarScreen.Notifications -> NotificationsScreen(screen.replyTo) + is CarScreen.Messages -> MessagesScreen(screen.packageName) + is CarScreen.App -> CarApps.find(screen.id)?.content?.invoke() + } } } } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/theme/CarColors.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/theme/CarColors.kt index 8a6642b..877fc9f 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/theme/CarColors.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/theme/CarColors.kt @@ -60,6 +60,8 @@ private fun ColorScheme.withDarkStyle(style: DarkStyle): ColorScheme { } } return copy( + // Grey text that read fine on grey looks dim on black, so it moves a little towards white. + onSurfaceVariant = lerp(onSurfaceVariant, onSurface, 0.3f), background = at(background, 0), surface = at(surface, 0), surfaceDim = at(surfaceDim, 0), diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipTheme.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipTheme.kt index bf100fa..7e4bff2 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipTheme.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipTheme.kt @@ -5,17 +5,21 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import app.seb3thehacker.gearslip.car.theme.withTintedSurfaces /** * Material You: the palette comes from the wallpaper (minSdk 35, so dynamic color always exists), - * with the same accent tint over the surfaces as the car screen, so the two look like one app. + * with the same surfaces as the car screen, so the two look like one app. */ @Composable fun GearslipTheme(content: @Composable () -> Unit) { val context = LocalContext.current val dark = isSystemInDarkTheme() - val scheme = (if (dark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)).withTintedSurfaces(dark) + // The car's Dark mode setting, so the phone and the car screen stay one look. + val darkStyle by app.seb3thehacker.gearslip.car.CarSettings.darkStyle.collectAsState() + val scheme = (if (dark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)).withTintedSurfaces(dark, darkStyle) MaterialTheme(colorScheme = scheme, content = content) }