From 787f64b85f2770285488882762ca81b50c45ffc4 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Mon, 6 May 2024 19:41:14 +0200 Subject: [PATCH] Location item design --- .../mm20/launcher2/ui/component/RatingBar.kt | 69 ++++ .../launcher/search/location/LocationItem.kt | 365 +++++------------- .../locations/LocationsSettingsScreen.kt | 9 - 3 files changed, 169 insertions(+), 274 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/component/RatingBar.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/RatingBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/RatingBar.kt new file mode 100644 index 00000000..b423ddfa --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/RatingBar.kt @@ -0,0 +1,69 @@ +package de.mm20.launcher2.ui.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.StarHalf +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.StarOutline +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.round + +/** + * A star rating bar that displays a rating from 0 to 5 stars. + * @param rating in the range of 0..1 + */ +@Composable +fun RatingBar( + rating: Float, + modifier: Modifier = Modifier, + tint: Color = MaterialTheme.colorScheme.primary, + starSize: Dp = 16.dp +) { + val starRating = round(rating * 10f).toInt() + val fullStars = starRating / 2 + val halfStar = starRating % 2 == 1 + val emptyStars = 5 - fullStars - if (halfStar) 1 else 0 + val iconModifier = Modifier.size(starSize) + Row( + modifier = modifier, + ) { + for (i in 0 until fullStars) { + Icon( + imageVector = Icons.Rounded.Star, + contentDescription = null, + tint = tint, + modifier = iconModifier + ) + } + if (halfStar) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.StarHalf, + contentDescription = null, + tint = tint, + modifier = iconModifier + ) + } + for (i in 0 until emptyStars) { + Icon( + imageVector = Icons.Rounded.StarOutline, + contentDescription = null, + tint = tint, + modifier = iconModifier + ) + } + } +} + +@Preview +@Composable +fun RatingBarPreview() { + RatingBar(0.67f) +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt index 2ede0fc0..6fe2aeb4 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt @@ -30,7 +30,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.NavigateNext import androidx.compose.material.icons.rounded.BugReport -import androidx.compose.material.icons.rounded.FiberManualRecord import androidx.compose.material.icons.rounded.Language import androidx.compose.material.icons.rounded.Map import androidx.compose.material.icons.rounded.Navigation @@ -41,7 +40,7 @@ import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -57,10 +56,12 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect +import androidx.compose.ui.unit.times import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.mm20.launcher2.i18n.R import de.mm20.launcher2.ktx.tryStartActivity @@ -68,6 +69,7 @@ import de.mm20.launcher2.search.Location import de.mm20.launcher2.search.OpeningHours import de.mm20.launcher2.search.OpeningSchedule import de.mm20.launcher2.ui.component.DefaultToolbarAction +import de.mm20.launcher2.ui.component.RatingBar import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.Toolbar import de.mm20.launcher2.ui.component.ToolbarAction @@ -122,6 +124,7 @@ fun LocationItem( val imperialUnits by viewModel.imperialUnits.collectAsState() + val showMap by viewModel.showMap.collectAsState() val isUpToDate by viewModel.isUpToDate.collectAsState() val distance = userLocation?.distanceTo(location.toAndroidLocation()) @@ -175,6 +178,7 @@ fun LocationItem( style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary, modifier = Modifier + .padding(top = 2.dp) .sharedElement( rememberSharedContentState("sublabel"), this@AnimatedContent @@ -182,46 +186,36 @@ fun LocationItem( ) } } - Box( - modifier = Modifier - .animateEnterExit( - enter = slideIn { IntOffset(it.width, 0) } + fadeIn(), - exit = slideOut { IntOffset(it.width, 0) } + fadeOut(), - ) - .padding(end = 8.dp) - .size(48.dp) - .background( - MaterialTheme.colorScheme.secondaryContainer, - MaterialTheme.shapes.small - ), - contentAlignment = Alignment.Center - ) { - Icon( - if (targetHeading != null) Icons.Rounded.Navigation else Icons.Rounded.FiberManualRecord, - null, - modifier = Modifier - .size(20.dp) - .rotate(targetHeading ?: 0f), - tint = MaterialTheme.colorScheme.onSecondaryContainer, - ) - } + Compass( + targetHeading = targetHeading, + modifier = Modifier.padding(end = 8.dp) then + if (!showMap) { + Modifier.sharedBounds( + rememberSharedContentState("compass"), + this@AnimatedContent + ) + } else { + Modifier.animateEnterExit( + enter = slideIn { IntOffset(it.width, 0) } + fadeIn(), + exit = slideOut { IntOffset(it.width, 0) } + fadeOut(), + ) + } + ) } } else { - val showMap by viewModel.showMap.collectAsState() Column { if (showMap) { val tileServerUrl by viewModel.mapTileServerUrl.collectAsState() val shape = MaterialTheme.shapes.small val applyTheming by viewModel.applyMapTheming.collectAsState() - val showPositionOnMap by viewModel.showPositionOnMap.collectAsState() MapTiles( modifier = Modifier .animateEnterExit( enter = expandVertically(), exit = shrinkOut(), ) - .padding(12.dp) + .padding(top = 12.dp, start = 12.dp, end = 12.dp) .align(Alignment.CenterHorizontally) .fillMaxWidth() .border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape) @@ -235,22 +229,29 @@ fun LocationItem( tiles = IntSize(3, 2), applyTheming = applyTheming, userLocation = { - if (showPositionOnMap) userLocation?.let { + userLocation?.let { UserLocation( it.latitude, it.longitude, heading = userHeading, ) - } else null + } }, ) } Row( - modifier = Modifier.padding(horizontal = 12.dp), + modifier = Modifier.padding( + top = 12.dp, + start = 12.dp, + end = 12.dp, + bottom = 4.dp + ), verticalAlignment = Alignment.CenterVertically, ) { - Column { + Column( + modifier = Modifier.weight(1f), + ) { Text( text = location.labelOverride ?: location.label, style = MaterialTheme.typography.titleMedium, @@ -275,12 +276,32 @@ fun LocationItem( style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary, modifier = Modifier + .padding(top = 2.dp) .sharedElement( rememberSharedContentState("sublabel"), this@AnimatedContent ) ) } + // TODO: add rating to location + if (!showMap && false) { + RatingBar(0.66f, modifier = Modifier.padding(top = 4.dp)) + } + } + //TODO: add rating to location + if (showMap && false) { + RatingBar(0.66f) + } else { + Compass( + targetHeading = targetHeading, + modifier = Modifier + .align(Alignment.Top) + .sharedBounds( + rememberSharedContentState("compass"), + this@AnimatedContent + ), + size = 56.dp, + ) } } @@ -289,12 +310,11 @@ fun LocationItem( var showOpeningSchedule by remember(openingSchedule) { mutableStateOf(false) } - Surface( + OutlinedCard( modifier = Modifier .fillMaxWidth() .padding(start = 12.dp, end = 12.dp, top = 12.dp), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.surfaceContainer, + shape = MaterialTheme.shapes.small, onClick = { if (!openingSchedule.isTwentyFourSeven) { showOpeningSchedule = true @@ -331,10 +351,11 @@ fun LocationItem( formattedTime ) } else { - val dow = currentOpeningTime.dayOfWeek.getDisplayName( - TextStyle.SHORT, - Locale.getDefault() - ) + val dow = + currentOpeningTime.dayOfWeek.getDisplayName( + TextStyle.SHORT, + Locale.getDefault() + ) context.getString( R.string.location_closes_other_day, dow, @@ -345,18 +366,21 @@ fun LocationItem( } else { val nextOpeningTime = openingSchedule.getNextOpeningHours() - val isSameDay = nextOpeningTime.dayOfWeek == LocalDateTime.now().dayOfWeek - val formattedTime = timeFormat.format(nextOpeningTime.startTime) + val isSameDay = + nextOpeningTime.dayOfWeek == LocalDateTime.now().dayOfWeek + val formattedTime = + timeFormat.format(nextOpeningTime.startTime) val openingTime = if (isSameDay) { context.getString( R.string.location_opens, formattedTime ) } else { - val dow = nextOpeningTime.dayOfWeek.getDisplayName( - TextStyle.SHORT, - Locale.getDefault() - ) + val dow = + nextOpeningTime.dayOfWeek.getDisplayName( + TextStyle.SHORT, + Locale.getDefault() + ) context.getString( R.string.location_opens_other_day, dow, @@ -367,7 +391,11 @@ fun LocationItem( } } - Text(text = text, style = MaterialTheme.typography.labelMedium, modifier = Modifier.weight(1f)) + Text( + text = text, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.weight(1f) + ) Icon(Icons.AutoMirrored.Rounded.NavigateNext, null) } @@ -551,228 +579,35 @@ fun LocationItem( } } } +} - /*Row(modifier = modifier) { - Column { - Row( +@Composable +fun Compass( + targetHeading: Float?, + modifier: Modifier = Modifier, + size: Dp = 48.dp +) { + Box( + modifier = modifier + .size(size) + .background( + MaterialTheme.colorScheme.secondaryContainer, + MaterialTheme.shapes.small + ), + contentAlignment = Alignment.Center + ) { + if (targetHeading != null) { + Icon( + Icons.Rounded.Navigation, + null, modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.padding(start = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = Modifier - .size(52.dp) - .aspectRatio(1f) - ) { - ShapedLauncherIcon( - size = 48.dp, - icon = { icon }, - badge = { badge }, - ) - val targetIconAnimationValue = if (isUpToDate) 0f else 1f - val animatedIconAlpha by animateFloatAsState( - targetValue = targetIconAnimationValue, - animationSpec = tween(delayMillis = 275) - ) - val animatedIconSize by animateDpAsState( - targetValue = targetIconAnimationValue * 20.dp, - animationSpec = tween(delayMillis = 275, easing = EaseOutBack) - ) - Box( - Modifier - .size(22.dp) - .align(Alignment.BottomEnd) - ) { - Icon( - modifier = Modifier - .size(animatedIconSize) - .alpha(animatedIconAlpha) - .align(Alignment.Center) - .clickable(!isUpToDate) { - Toast - .makeText( - context, - R.string.cached_searchable, - Toast.LENGTH_SHORT - ) - .show() - }, - imageVector = Icons.TwoTone.CloudOff, - contentDescription = null - ) - } - } - } - Column( - modifier = Modifier.fillMaxWidth(.75f), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - val textStyle by animateTextStyleAsState( - if (showDetails) MaterialTheme.typography.titleMedium - else MaterialTheme.typography.titleSmall - ) - val titleAlignment by animateHorizontalAlignmentAsState( - targetAlignment = if (showDetails) Alignment.CenterHorizontally else Alignment.Start - ) - Text( - text = location.labelOverride ?: location.label, - modifier = Modifier.align(titleAlignment), - style = textStyle, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - softWrap = true, - ) - if (!location.openingSchedule?.openingHours.isNullOrEmpty()) { - val isOpen = location.openingSchedule!!.isOpen - AnimatedVisibility(!showDetails) { - Text( - modifier = Modifier - .padding(top = 4.dp) - .fillMaxWidth(), - text = context.getString(if (isOpen) R.string.location_open else R.string.location_closed), - style = MaterialTheme.typography.labelSmall, - color = if (isOpen) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.secondary, - textAlign = TextAlign.Start, - ) - } - } - } - Column( - modifier = Modifier.padding(end = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceAround, - ) { - - if (targetHeading != null) { - val directionArrowAngle by animateValueAsState( - targetValue = targetHeading!!, - typeConverter = Float.DegreesConverter - ) - Icon( - modifier = Modifier.rotate(directionArrowAngle), - imageVector = Icons.Rounded.ArrowUpward, - contentDescription = null - ) - } - if (distance != null) { - Text( - text = distance.metersToLocalizedString( - context, imperialUnits - ), style = MaterialTheme.typography.labelSmall - ) - } - } - } - AnimatedVisibility(showDetails) { - Column { - val isTwentyFourSeven = location.openingSchedule?.isTwentyFourSeven ?: false - val hasOpeningHours = !location.openingSchedule?.openingHours.isNullOrEmpty() - val daysOfWeek = enumValues() - - val javaLocale = java.util.Locale.forLanguageTag(Locale.current.toLanguageTag()) - val timeFormatter = DateTimeFormatter - .ofLocalizedTime(FormatStyle.SHORT) - .withLocale(javaLocale) - - if (isTwentyFourSeven) { - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = 8.dp), - text = stringResource(id = R.string.location_open_24_7), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.tertiary, - ) - } else if (hasOpeningHours) { - val oh = location.openingSchedule!!.openingHours - val openIndex = oh.indexOfFirst { it.isOpen } - if (openIndex != -1) { - val todaySchedule = oh[openIndex] - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = 8.dp), - text = stringResource( - R.string.location_open_until, - (todaySchedule.startTime + todaySchedule.duration).format( - timeFormatter - ) - ), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.tertiary, - ) - } - } - - - HorizontalDivider() - - - val address = buildAddress(location.street, location.houseNumber) - if (address != null) { - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = 8.dp), - text = address, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.secondary - ) - } - - HorizontalDivider(Modifier.padding(top = 8.dp)) - - if (!isTwentyFourSeven && hasOpeningHours) { - val today = LocalDateTime.now().dayOfWeek - val oh = location.openingSchedule!!.openingHours - val nextOpeningTime = - (0..DayOfWeek.SUNDAY.ordinal) - .firstNotNullOfOrNull { - val dow = - daysOfWeek[(today.ordinal + it) % (DayOfWeek.SUNDAY.ordinal + 1)] - oh.filter { - it.dayOfWeek == dow - }.firstOrNull { - it.dayOfWeek != today || it.startTime.isAfter(LocalTime.now()) - } - } ?: oh.first() - - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = 8.dp), - text = stringResource( - if (nextOpeningTime.dayOfWeek == today) R.string.location_open_next - else R.string.location_open_next_day, - if (nextOpeningTime.dayOfWeek == today) { - val untilOpenToday = Duration.between( - LocalTime.now(), - nextOpeningTime.startTime, - ) - val hours = untilOpenToday.toHours() - val minutes = untilOpenToday.toMinutes() % 60L - if (hours > 0L) "${hours}h ${minutes}m" - else "${minutes}m" - } else "${ - nextOpeningTime.dayOfWeek.getDisplayName( - TextStyle.FULL_STANDALONE, - javaLocale - ) - } ${nextOpeningTime.startTime.format(timeFormatter)}" - ), - style = MaterialTheme.typography.labelMedium, - ) - } - } - } + .size(20f / 48f * size) + .rotate(targetHeading) + .offset(y = -1f / 48f * size), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + ) } - }*/ + } } @Composable diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locations/LocationsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locations/LocationsSettingsScreen.kt index 990881c0..5a1f17a2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locations/LocationsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locations/LocationsSettingsScreen.kt @@ -118,15 +118,6 @@ fun LocationsSettingsScreen() { viewModel.setThemeMap(it) } ) - SwitchPreference( - title = stringResource(R.string.preference_search_locations_show_position_on_map), - summary = stringResource(R.string.preference_search_locations_show_position_on_map_summary), - value = showPositionOnMap == true, - enabled = locations == true && showMap == true, - onValueChanged = { - viewModel.setShowPositionOnMap(it) - } - ) } }