diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/weather/WeatherIconColors.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/weather/WeatherIconColors.kt index 9f36e80c..4b2aa9d3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/weather/WeatherIconColors.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/weather/WeatherIconColors.kt @@ -1,6 +1,7 @@ package de.mm20.launcher2.ui.component.weather import android.content.Context +import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -10,6 +11,8 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import blend.Blend import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.ktx.atTone +import de.mm20.launcher2.ui.locals.LocalDarkTheme data class WeatherIconColors( @@ -38,31 +41,35 @@ object WeatherIconDefaults { val context = LocalContext.current val themeColor = MaterialTheme.colorScheme.primary + val darkTheme = LocalDarkTheme.current + + val neutral1 = MaterialTheme.colorScheme.outline + val neutral2 = MaterialTheme.colorScheme.outline return remember(themeColor) { WeatherIconColors( - sun = harmonize(context, R.color.weather_sun, themeColor), - moon = harmonize(context, R.color.weather_moon, themeColor), - cloudDark1 = harmonize(context, R.color.weather_cloud_dark_1, themeColor), - cloudDark2 = harmonize(context, R.color.weather_cloud_dark_2, themeColor), - cloudMedium1 = harmonize(context, R.color.weather_cloud_medium_1, themeColor), - cloudMedium2 = harmonize(context, R.color.weather_cloud_medium_2, themeColor), - cloudLight1 = harmonize(context, R.color.weather_cloud_light_1, themeColor), - cloudLight2 = harmonize(context, R.color.weather_cloud_light_2, themeColor), - rain = harmonize(context, R.color.weather_rain, themeColor), - snow = harmonize(context, R.color.weather_snow, themeColor), - hail = harmonize(context, R.color.weather_hail, themeColor), - fog = harmonize(context, R.color.weather_fog, themeColor), - wind = harmonize(context, R.color.weather_wind, themeColor), - windDark = harmonize(context, R.color.weather_wind_dark, themeColor), - lightningBolt = harmonize(context, R.color.weather_lightning_bolt, themeColor), - hot = harmonize(context, R.color.weather_hot, themeColor), - cold = harmonize(context, R.color.weather_cold, themeColor), + sun = harmonize(context, 0xFFFFB300.toInt(), themeColor), + moon = harmonize(context, 0xFF9E9E9E.toInt(), themeColor), + cloudDark1 = harmonize(context, neutral1.atTone(if (darkTheme) 40 else 30).toArgb(), themeColor), + cloudDark2 = harmonize(context, neutral1.atTone(if (darkTheme) 30 else 20).toArgb(), themeColor), + cloudMedium1 = harmonize(context, neutral1.atTone(if (darkTheme) 60 else 50).toArgb(), themeColor), + cloudMedium2 = harmonize(context, neutral1.atTone(if (darkTheme) 50 else 40).toArgb(), themeColor), + cloudLight1 = harmonize(context, neutral1.atTone(if (darkTheme) 95 else 85).toArgb(), themeColor), + cloudLight2 = harmonize(context, neutral1.atTone(if (darkTheme) 85 else 75).toArgb(), themeColor), + rain = harmonize(context, if (darkTheme) 0xFF64B5F6.toInt() else 0xFF1E88E5.toInt(), themeColor), + snow = harmonize(context, if (darkTheme) 0xFFF5F5F5.toInt() else 0xFFE0E0E0.toInt(), themeColor), + hail = harmonize(context, if (darkTheme) 0xFFF5F5F5.toInt() else 0xFFE0E0E0.toInt(), themeColor), + fog = harmonize(context, neutral1.atTone(if (darkTheme) 95 else 85).toArgb(), themeColor), + wind = harmonize(context, neutral2.atTone(if (darkTheme) 70 else 75).toArgb(), themeColor), + windDark = harmonize(context, neutral2.atTone(if (darkTheme) 40 else 45).toArgb(), themeColor), + lightningBolt = harmonize(context, 0xFFFFB300.toInt(), themeColor), + hot = harmonize(context, if (darkTheme) 0xFFE57373.toInt() else 0xFFE53935.toInt(), themeColor), + cold = harmonize(context, if (darkTheme) 0xFF4FC3F7.toInt() else 0xFF039BE5.toInt(), themeColor), ) } } } -private fun harmonize(context: Context, @ColorRes baseColor: Int, themeColor: Color): Color { - return Color(Blend.harmonize(context.getColor(baseColor), themeColor.toArgb())) +private fun harmonize(context: Context, @ColorInt baseColor: Int, themeColor: Color): Color { + return Color(Blend.harmonize(baseColor, themeColor.toArgb())) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt index f4854938..ab588516 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt @@ -8,22 +8,30 @@ import android.text.format.DateUtils import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.OpenInNew @@ -35,7 +43,6 @@ import androidx.compose.material.icons.rounded.MyLocation import androidx.compose.material.icons.rounded.North import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -50,16 +57,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.app.ActivityOptionsCompat @@ -78,7 +86,6 @@ import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.Tooltip import de.mm20.launcher2.ui.component.weather.AnimatedWeatherIcon import de.mm20.launcher2.ui.component.weather.WeatherIcon -import de.mm20.launcher2.ui.ktx.blendIntoViewScale import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme import de.mm20.launcher2.weather.DailyForecast import de.mm20.launcher2.weather.Forecast @@ -180,11 +187,11 @@ fun WeatherWidget(widget: WeatherWidget) { val currentDayForecasts by viewModel.currentDayForecasts Surface( - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = LocalTransparencyScheme.current.surface), + color = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = LocalTransparencyScheme.current.surface), modifier = Modifier.fillMaxWidth() ) { Column( - modifier = Modifier.padding(top = 8.dp, bottom = 12.dp) + modifier = Modifier.padding(top = 12.dp, bottom = 12.dp) ) { WeatherTimeSelector( forecasts = currentDayForecasts, @@ -193,11 +200,7 @@ fun WeatherWidget(widget: WeatherWidget) { onTimeSelected = { viewModel.selectForecast(it) }, - modifier = Modifier.padding(bottom = 8.dp) - ) - Divider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.12f), + modifier = Modifier.padding(bottom = 12.dp) ) selectedDayForecast?.let { WeatherDaySelector( @@ -207,7 +210,6 @@ fun WeatherWidget(widget: WeatherWidget) { viewModel.selectDay(it) }, imperialUnits = imperialUnits, - modifier = Modifier.padding(top = 8.dp) ) } } @@ -453,49 +455,74 @@ fun WeatherTimeSelector( state = listState, modifier = modifier .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(start = 16.dp, end = 16.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + contentPadding = PaddingValues(start = 12.dp, end = 12.dp), verticalAlignment = Alignment.CenterVertically ) { itemsIndexed(forecasts, key = { idx, _ -> idx }) { idx, fc -> - val backgroundAlpha = if (fc == selectedForecast) 0.2f else 0.0f + val selected = fc == selectedForecast + val sm = MaterialTheme.shapes.small + val xs = MaterialTheme.shapes.extraSmall Surface( - shape = MaterialTheme.shapes.extraSmall, + shape = when (idx) { + 0 -> xs.copy( + topStart = sm.topStart, + bottomStart = sm.bottomStart + ) + + forecasts.lastIndex -> xs.copy( + topEnd = sm.topEnd, + bottomEnd = sm.bottomEnd + ) + + else -> MaterialTheme.shapes.extraSmall + }, modifier = Modifier - .widthIn(min = 56.dp) - .graphicsLayer { - alpha = listState.layoutInfo.blendIntoViewScale(idx, 2f) - }, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(backgroundAlpha) + .widthIn(min = 60.dp), + color = MaterialTheme.colorScheme.surface, ) { Column( modifier = Modifier .clickable { onTimeSelected(idx) } - .padding(4.dp), + .padding(start = 4.dp, end = 4.dp, top = 8.dp, bottom = 4.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly ) { WeatherIcon( modifier = Modifier .align(Alignment.CenterHorizontally) .semantics { contentDescription = fc.condition - }, + } + .padding(bottom = 4.dp), icon = weatherIconById(fc.icon), night = fc.night ) Text( text = dateFormat.format(fc.timestamp), style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, softWrap = false, modifier = Modifier.align(Alignment.CenterHorizontally) ) - Text( - text = "${convertTemperature(imperialUnits, fc.temperature)}°", - softWrap = false, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) + Box( + contentAlignment = Alignment.Center + ) { + val alpha by animateFloatAsState(if (selected) 0f else 1f) + Text( + modifier = Modifier + .alpha(alpha), + text = "${convertTemperature(imperialUnits, fc.temperature)}°", + softWrap = false, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Box( + modifier = Modifier + .alpha(1f - alpha) + .requiredSize(8.dp) + .background(MaterialTheme.colorScheme.primary, CircleShape) + ) + } } } } @@ -517,44 +544,60 @@ fun WeatherDaySelector( state = listState, modifier = modifier .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(start = 16.dp, end = 16.dp), + contentPadding = PaddingValues(start = 12.dp, end = 12.dp), ) { itemsIndexed(days, key = { idx, _ -> idx }) { idx, day -> - val backgroundAlpha = if (day == selectedDay) 0.2f else 0.0f + val selected = day == selectedDay + val sm = MaterialTheme.shapes.small + val xs = MaterialTheme.shapes.extraSmall Surface( - modifier = Modifier.graphicsLayer { - alpha = listState.layoutInfo.blendIntoViewScale(idx, 0.5f) + shape = when (idx) { + 0 -> xs.copy( + topStart = sm.topStart, + bottomStart = sm.bottomStart + ) + + days.lastIndex -> xs.copy( + topEnd = sm.topEnd, + bottomEnd = sm.bottomEnd + ) + + else -> MaterialTheme.shapes.extraSmall }, - shape = MaterialTheme.shapes.extraSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(backgroundAlpha) + color = MaterialTheme.colorScheme.surface, ) { Row( modifier = Modifier .clickable { onDaySelected(idx) } - .padding(4.dp), - horizontalArrangement = Arrangement.SpaceAround, + .padding(top = 4.dp, bottom = 4.dp, start = 4.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically ) { WeatherIcon(icon = weatherIconById(day.icon)) Text( - text = dateFormat.format(day.timestamp), - style = MaterialTheme.typography.labelSmall, - softWrap = false, - modifier = Modifier.padding(start = 12.dp, end = 6.dp) - ) - Text( - text = "${ - convertTemperature( - imperialUnits, - day.minTemp - ) - }° / ${convertTemperature(imperialUnits, day.maxTemp)}°", + modifier = Modifier.padding(start = 8.dp), + text = "${dateFormat.format(day.timestamp)} " + + "${convertTemperature(imperialUnits, day.minTemp)}° / " + + "${convertTemperature(imperialUnits, day.maxTemp)}°", softWrap = false, style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + val spec = MaterialTheme.motionScheme.fastSpatialSpec() + AnimatedVisibility( + selected, + enter = fadeIn() + expandHorizontally(spec), + exit = fadeOut() + shrinkHorizontally(spec), + ) { + Box( + modifier = Modifier + .padding(start = 8.dp) + .size(8.dp) + .background(MaterialTheme.colorScheme.primary, CircleShape) + ) + } } } }