Location item design

This commit is contained in:
MM20 2024-05-06 19:41:14 +02:00
parent 53ca2b525d
commit 787f64b85f
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
3 changed files with 169 additions and 274 deletions

View File

@ -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)
}

View File

@ -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.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.NavigateNext import androidx.compose.material.icons.automirrored.rounded.NavigateNext
import androidx.compose.material.icons.rounded.BugReport 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.Language
import androidx.compose.material.icons.rounded.Map import androidx.compose.material.icons.rounded.Map
import androidx.compose.material.icons.rounded.Navigation 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.AssistChipDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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.graphics.TransformOrigin
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.roundToIntRect
import androidx.compose.ui.unit.times
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.mm20.launcher2.i18n.R import de.mm20.launcher2.i18n.R
import de.mm20.launcher2.ktx.tryStartActivity 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.OpeningHours
import de.mm20.launcher2.search.OpeningSchedule import de.mm20.launcher2.search.OpeningSchedule
import de.mm20.launcher2.ui.component.DefaultToolbarAction 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.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.Toolbar import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction import de.mm20.launcher2.ui.component.ToolbarAction
@ -122,6 +124,7 @@ fun LocationItem(
val imperialUnits by viewModel.imperialUnits.collectAsState() val imperialUnits by viewModel.imperialUnits.collectAsState()
val showMap by viewModel.showMap.collectAsState()
val isUpToDate by viewModel.isUpToDate.collectAsState() val isUpToDate by viewModel.isUpToDate.collectAsState()
val distance = userLocation?.distanceTo(location.toAndroidLocation()) val distance = userLocation?.distanceTo(location.toAndroidLocation())
@ -175,6 +178,7 @@ fun LocationItem(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
modifier = Modifier modifier = Modifier
.padding(top = 2.dp)
.sharedElement( .sharedElement(
rememberSharedContentState("sublabel"), rememberSharedContentState("sublabel"),
this@AnimatedContent this@AnimatedContent
@ -182,46 +186,36 @@ fun LocationItem(
) )
} }
} }
Box( Compass(
modifier = Modifier targetHeading = targetHeading,
.animateEnterExit( modifier = Modifier.padding(end = 8.dp) then
enter = slideIn { IntOffset(it.width, 0) } + fadeIn(), if (!showMap) {
exit = slideOut { IntOffset(it.width, 0) } + fadeOut(), Modifier.sharedBounds(
) rememberSharedContentState("compass"),
.padding(end = 8.dp) this@AnimatedContent
.size(48.dp) )
.background( } else {
MaterialTheme.colorScheme.secondaryContainer, Modifier.animateEnterExit(
MaterialTheme.shapes.small enter = slideIn { IntOffset(it.width, 0) } + fadeIn(),
), exit = slideOut { IntOffset(it.width, 0) } + fadeOut(),
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,
)
}
} }
} else { } else {
val showMap by viewModel.showMap.collectAsState()
Column { Column {
if (showMap) { if (showMap) {
val tileServerUrl by viewModel.mapTileServerUrl.collectAsState() val tileServerUrl by viewModel.mapTileServerUrl.collectAsState()
val shape = MaterialTheme.shapes.small val shape = MaterialTheme.shapes.small
val applyTheming by viewModel.applyMapTheming.collectAsState() val applyTheming by viewModel.applyMapTheming.collectAsState()
val showPositionOnMap by viewModel.showPositionOnMap.collectAsState()
MapTiles( MapTiles(
modifier = Modifier modifier = Modifier
.animateEnterExit( .animateEnterExit(
enter = expandVertically(), enter = expandVertically(),
exit = shrinkOut(), exit = shrinkOut(),
) )
.padding(12.dp) .padding(top = 12.dp, start = 12.dp, end = 12.dp)
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)
.fillMaxWidth() .fillMaxWidth()
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape) .border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape)
@ -235,22 +229,29 @@ fun LocationItem(
tiles = IntSize(3, 2), tiles = IntSize(3, 2),
applyTheming = applyTheming, applyTheming = applyTheming,
userLocation = { userLocation = {
if (showPositionOnMap) userLocation?.let { userLocation?.let {
UserLocation( UserLocation(
it.latitude, it.latitude,
it.longitude, it.longitude,
heading = userHeading, heading = userHeading,
) )
} else null }
}, },
) )
} }
Row( 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, verticalAlignment = Alignment.CenterVertically,
) { ) {
Column { Column(
modifier = Modifier.weight(1f),
) {
Text( Text(
text = location.labelOverride ?: location.label, text = location.labelOverride ?: location.label,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@ -275,12 +276,32 @@ fun LocationItem(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
modifier = Modifier modifier = Modifier
.padding(top = 2.dp)
.sharedElement( .sharedElement(
rememberSharedContentState("sublabel"), rememberSharedContentState("sublabel"),
this@AnimatedContent 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) { var showOpeningSchedule by remember(openingSchedule) {
mutableStateOf(false) mutableStateOf(false)
} }
Surface( OutlinedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, top = 12.dp), .padding(start = 12.dp, end = 12.dp, top = 12.dp),
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceContainer,
onClick = { onClick = {
if (!openingSchedule.isTwentyFourSeven) { if (!openingSchedule.isTwentyFourSeven) {
showOpeningSchedule = true showOpeningSchedule = true
@ -331,10 +351,11 @@ fun LocationItem(
formattedTime formattedTime
) )
} else { } else {
val dow = currentOpeningTime.dayOfWeek.getDisplayName( val dow =
TextStyle.SHORT, currentOpeningTime.dayOfWeek.getDisplayName(
Locale.getDefault() TextStyle.SHORT,
) Locale.getDefault()
)
context.getString( context.getString(
R.string.location_closes_other_day, R.string.location_closes_other_day,
dow, dow,
@ -345,18 +366,21 @@ fun LocationItem(
} else { } else {
val nextOpeningTime = val nextOpeningTime =
openingSchedule.getNextOpeningHours() openingSchedule.getNextOpeningHours()
val isSameDay = nextOpeningTime.dayOfWeek == LocalDateTime.now().dayOfWeek val isSameDay =
val formattedTime = timeFormat.format(nextOpeningTime.startTime) nextOpeningTime.dayOfWeek == LocalDateTime.now().dayOfWeek
val formattedTime =
timeFormat.format(nextOpeningTime.startTime)
val openingTime = if (isSameDay) { val openingTime = if (isSameDay) {
context.getString( context.getString(
R.string.location_opens, R.string.location_opens,
formattedTime formattedTime
) )
} else { } else {
val dow = nextOpeningTime.dayOfWeek.getDisplayName( val dow =
TextStyle.SHORT, nextOpeningTime.dayOfWeek.getDisplayName(
Locale.getDefault() TextStyle.SHORT,
) Locale.getDefault()
)
context.getString( context.getString(
R.string.location_opens_other_day, R.string.location_opens_other_day,
dow, 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) Icon(Icons.AutoMirrored.Rounded.NavigateNext, null)
} }
@ -551,228 +579,35 @@ fun LocationItem(
} }
} }
} }
}
/*Row(modifier = modifier) { @Composable
Column { fun Compass(
Row( 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 modifier = Modifier
.fillMaxWidth() .size(20f / 48f * size)
.padding(top = 8.dp, bottom = 8.dp), .rotate(targetHeading)
horizontalArrangement = Arrangement.SpaceBetween, .offset(y = -1f / 48f * size),
verticalAlignment = Alignment.CenterVertically, tint = MaterialTheme.colorScheme.onSecondaryContainer,
) { )
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<DayOfWeek>()
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,
)
}
}
}
} }
}*/ }
} }
@Composable @Composable

View File

@ -118,15 +118,6 @@ fun LocationsSettingsScreen() {
viewModel.setThemeMap(it) 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)
}
)
} }
} }