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.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<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,
)
}
}
}
.size(20f / 48f * size)
.rotate(targetHeading)
.offset(y = -1f / 48f * size),
tint = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}*/
}
}
@Composable

View File

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