Skip to content

Commit

Permalink
UI: Implement DateTime selection and navigation flow (#400)
Browse files Browse the repository at this point in the history
### TL;DR
Added date/time selection functionality to the trip planner, allowing users to specify when they want to leave or arrive.

### What changed?
- Added a new date/time selector screen with options to choose between "Leave at" or "Arrive by"
- Implemented time picker and date selection with navigation between days
- Added a reset option to return to "Leaving now"
- Created data persistence between screens using NavController's savedStateHandle
- Updated the time table screen to display the selected date/time
- Removed animation from radio buttons for better performance
- Increased icon button size from 24dp to 32dp

### Screenshots

https://github.com/user-attachments/assets/2bde833a-a014-4892-8cdb-b6dff695d15c


### Why make this change?
To provide users with more flexibility in planning their journeys by allowing them to view schedules for specific dates and times, rather than being limited to current departure times only.
  • Loading branch information
ksharma-xyz authored Nov 30, 2024
1 parent 799c981 commit 45e1e6b
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ fun decrementDateByOneDay(date: LocalDate): LocalDate {
return date.plus(-1, DateTimeUnit.DAY)
}

@Composable
fun formatTime(hour: Int, minute: Int): String {
val displayHour = if (hour == 0 || hour == 12) 12 else hour % 12
val amPm = if (hour < 12) "AM" else "PM"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fun IconButton(
painter = painter,
contentDescription = null,
colorFilter = ColorFilter.tint(color),
modifier = Modifier.size(24.dp),
modifier = Modifier.size(32.dp),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,6 @@ fun RadioButton(
selected: Boolean = false,
onClick: () -> Unit = {},
) {
val animationProgress by animateFloatAsState(
targetValue = if (selected) 1f else 0f,
animationSpec = tween(durationMillis = 250)
)

val borderThickness = lerp(start = 0.dp, stop = 4.dp, fraction = animationProgress)
val animatedBackgroundColor =
androidx.compose.ui.graphics.lerp(
start = Color.Transparent,
stop = themeColor,
fraction = animationProgress,
)

Box(
modifier = modifier
.height(
Expand All @@ -52,11 +39,11 @@ fun RadioButton(
)
.clip(shape = RoundedCornerShape(8.dp))
.background(
color = animatedBackgroundColor,
color = if (selected) themeColor else Color.Transparent,
shape = RoundedCornerShape(8.dp)
)
.border(
width = borderThickness, // Dynamic border thickness
width = 2.dp, // Dynamic border thickness
color = themeColor, // Border color
shape = RoundedCornerShape(8.dp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@ package xyz.ksharma.krail.trip.planner.ui.datetimeselector
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import org.koin.compose.viewmodel.koinViewModel
import xyz.ksharma.krail.trip.planner.ui.navigation.DateTimeSelectorRoute

internal fun NavGraphBuilder.dateTimeSelectorDestination(navController: NavHostController) {
composable<DateTimeSelectorRoute> { backStackEntry ->
val viewModel: DateTimeSelectorViewModel = koinViewModel<DateTimeSelectorViewModel>()

DateTimeSelectorScreen(onBackClick = {
navController.popBackStack()
})
DateTimeSelectorScreen(
onBackClick = {
navController.popBackStack()
},
onDateTimeSelected = { dateTimeSelection ->
navController.previousBackStackEntry?.savedStateHandle?.set(
key = DateTimeSelectorRoute.DATE_TIME_TEXT_KEY,
value = dateTimeSelection,
)
navController.popBackStack()
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import androidx.compose.foundation.Image
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.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
Expand All @@ -19,6 +17,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -34,30 +33,35 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.plus
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.Serializable
import xyz.ksharma.krail.core.datetime.decrementDateByOneDay
import xyz.ksharma.krail.core.datetime.formatDate
import xyz.ksharma.krail.core.datetime.formatTime
import xyz.ksharma.krail.core.datetime.incrementDateByOneDay
import xyz.ksharma.krail.core.datetime.rememberCurrentDateTime
import xyz.ksharma.krail.taj.LocalThemeColor
import xyz.ksharma.krail.taj.components.Divider
import xyz.ksharma.krail.taj.components.Text
import xyz.ksharma.krail.taj.components.TitleBar
import xyz.ksharma.krail.taj.theme.KrailTheme
import xyz.ksharma.krail.trip.planner.ui.components.hexToComposeColor
import xyz.ksharma.krail.trip.planner.ui.components.themeBackgroundColor
import xyz.ksharma.krail.trip.planner.ui.components.themeContentColor
import xyz.ksharma.krail.trip.planner.ui.datetimeselector.JourneyTimeOptions.ARRIVE
import xyz.ksharma.krail.trip.planner.ui.datetimeselector.JourneyTimeOptions.LEAVE
import xyz.ksharma.krail.trip.planner.ui.timetable.ActionButton

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DateTimeSelectorScreen(
modifier: Modifier = Modifier,
onBackClick: () -> Unit = {},
onDateTimeSelected: () -> Unit = {},
onDateTimeSelected: (String?) -> Unit = {},
) {
// Colors
val themeColorHex by LocalThemeColor.current
Expand All @@ -80,6 +84,13 @@ fun DateTimeSelectorScreen(
val maxDate = remember { today.plus(7, DateTimeUnit.DAY) }
var selectedDate by remember { mutableStateOf(today) }

// Reset
var reset by remember { mutableStateOf(false) }
LaunchedEffect(timePickerState, journeyTimeOption, selectedDate) {
// if any of the date / time value changes, then reset is invalid.
reset = false
}

Column(
modifier = modifier.fillMaxSize().background(color = KrailTheme.colors.surface),
) {
Expand All @@ -96,18 +107,17 @@ fun DateTimeSelectorScreen(
)
}
}, actions = {
Text(
text = "Reset",
Text(text = "Reset",
style = KrailTheme.typography.titleSmall.copy(fontWeight = FontWeight.Normal),
modifier = Modifier
.padding(10.dp)
modifier = Modifier.padding(10.dp)
.background(color = themeBackgroundColor(), shape = RoundedCornerShape(50))
.padding(horizontal = 12.dp, vertical = 6.dp)
.clickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
reset = true
val now: LocalDateTime =
Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
selectedDate = now.date
Expand All @@ -121,28 +131,19 @@ fun DateTimeSelectorScreen(
JourneyTimeOptionsGroup(
selectedOption = journeyTimeOption,
themeColor = themeColor,
onOptionSelected = { journeyTimeOption = it },
onOptionSelected = {
journeyTimeOption = it
},
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
)
}

item {
TimeSelection(
timePickerState = timePickerState,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
.align(Alignment.CenterHorizontally),
)
}
Divider(modifier = Modifier.padding(horizontal = 16.dp))

item {
DateSelection(
themeColor = themeColor,
date = "${formatDate(selectedDate)}, ${
formatTime(
hour = timePickerState.hour,
minute = timePickerState.minute,
)
}",
date = formatDate(selectedDate),
onNextClicked = {
if (selectedDate < maxDate) {
selectedDate = incrementDateByOneDay(selectedDate)
Expand All @@ -153,43 +154,83 @@ fun DateTimeSelectorScreen(
selectedDate = decrementDateByOneDay(selectedDate)
}
},
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
)

Divider(modifier = Modifier.padding(horizontal = 16.dp))
}

item {
TimeSelection(
timePickerState = timePickerState,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
.align(Alignment.CenterHorizontally),
)
}

item {
Text(
text = "Done",
text = if (reset) {
"Leaving: Now"
} else {
DateTimeSelectionItem(
option = journeyTimeOption,
hour = timePickerState.hour,
minute = timePickerState.minute,
date = selectedDate,
).toDateTimeText()
},
textAlign = TextAlign.Center,
color = themeContentColor(),
style = KrailTheme.typography.titleMedium,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp)
.clip(RoundedCornerShape(50))
.background(color = themeColor).clickable(
.background(color = themeColor)
.clickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
onDateTimeSelected()
}.padding(vertical = 10.dp),
onClick = {
onDateTimeSelected(
if (reset) null
else {
DateTimeSelectionItem(
option = journeyTimeOption,
hour = timePickerState.hour,
minute = timePickerState.minute,
date = selectedDate,
).toDateTimeText()
}
)
},
).padding(vertical = 10.dp)
)
}
}
}
}

@Composable
private fun SelectedDateTimeRow(
dateTimeText: String,
modifier: Modifier = Modifier,
onResetClick: () -> Unit = {},
@Serializable
data class DateTimeSelectionItem(
val option: JourneyTimeOptions,
val hour: Int,
val minute: Int,
val date: LocalDate,
) {
Row(
modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
Text("Reset", modifier = Modifier.clickable { onResetClick() })
Text(dateTimeText)
fun toDateTimeText(): String = when (option) {
LEAVE -> {
"Leave: ${formatDate(date)} ${formatTime(hour, minute)}"
}

ARRIVE -> {
"Arrive: ${formatDate(date)} ${formatTime(hour, minute)}"
}
}

@Suppress("ConstPropertyName")
companion object {
private const val serialVersionUID: Long = 1L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,11 @@ internal data class ServiceAlertRoute(
data object SettingsRoute

@Serializable
data object DateTimeSelectorRoute
data class DateTimeSelectorRoute(
// Noop, need x coz it's data class, need to put keys in companion obj rather than elsewhere.
val x: String = "",
) {
companion object {
const val DATE_TIME_TEXT_KEY = "DateTimeSelectionKey"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package xyz.ksharma.krail.trip.planner.ui.timetable

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
Expand Down Expand Up @@ -27,6 +28,19 @@ internal fun NavGraphBuilder.timeTableDestination(navController: NavHostControll
val isActive by viewModel.isActive.collectAsStateWithLifecycle()
val expandedJourneyId: String? by viewModel.expandedJourneyId.collectAsStateWithLifecycle()

// Arguments
// Cannot use 'rememberSaveable' here because DateTimeSelectionItem is not Parcelable.
// But it's saved in backStackEntry.savedStateHandle as json, so it's able to
// handle config changes properly.
val dateTimeSelectionText: String =
backStackEntry.savedStateHandle.get<String>(key = DateTimeSelectorRoute.DATE_TIME_TEXT_KEY)
?: "Leave: Now"

// Lookout for new updates
LaunchedEffect(dateTimeSelectionText) {
println("Changed dateTimeSelectionItem: $dateTimeSelectionText")
}

TimeTableScreen(
timeTableState = timeTableState,
expandedJourneyId = expandedJourneyId,
Expand All @@ -44,9 +58,10 @@ internal fun NavGraphBuilder.timeTableDestination(navController: NavHostControll
}
}
},
dateTimeSelectionText = dateTimeSelectionText,
dateTimeSelectorClicked = {
navController.navigate(
route = DateTimeSelectorRoute,
route = DateTimeSelectorRoute(),
navOptions = NavOptions.Builder().setLaunchSingleTop(singleTop = true).build(),
)
},
Expand Down
Loading

0 comments on commit 45e1e6b

Please sign in to comment.