mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
car: a faint album-colour glow on the player card, screens fade in as they open, and the phone app follows the Dark mode setting
This commit is contained in:
1 parent
08eef32e59
commit
608d72b856
4 files changed
+81
-16
No files matched your search
@@ -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<List<Color>>(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<Color>(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<Color> {
|
||||
// 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
|
||||
|
||||
@@ -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>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user