Skip to content

Instantly share code, notes, and snippets.

@wilinz
Last active March 3, 2024 15:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wilinz/20c9b36493649e165d3f5600e897774f to your computer and use it in GitHub Desktop.
Save wilinz/20c9b36493649e165d3f5600e897774f to your computer and use it in GitHub Desktop.
Compose Material3 TimePickerDialog
@Composable
fun Sample() {
val timePickerState =
rememberTimePickerState(
is24Hour = true,
initialHour = 12,
initialMinute = 0
)
var isShowTimePicker by remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = isShowTimePicker) {
TimePickerDialog(
state = timePickerState,
title = {
Text(text = "Select Time")
},
onDismissRequest = { isShowTimePicker = false },
confirmButton = {
TextButton(onClick = {
timePickerState.let {
state.startTime =
dataTime.withHour(it.hour).withMinute(it.minute).toInstant()
.toEpochMilli()
}
isShowTimePicker = false
}) {
Text(text = "OK")
}
},
contentDescription = TimePickerDialogContentDescription(
toggleKeyboardButton = "Currently in clock mode, click to switch",
toggleScheduleButton = "Currently in keyboard mode, click to switch"
)
)
}
}
package com.wilinz.xxx
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.TimeInput
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
@ExperimentalMaterial3Api
@Stable
object TimePickerDefaults {
val shape: Shape @Composable get() = MaterialTheme.shapes.extraLarge
val TonalElevation: Dp = 6.0.dp
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickerDialog(
state: TimePickerState,
onDismissRequest: () -> Unit,
confirmButton: @Composable () -> Unit,
modifier: Modifier = Modifier,
dismissButton: @Composable (() -> Unit)? = null,
title: @Composable () -> Unit,
shape: Shape = TimePickerDefaults.shape,
tonalElevation: Dp = TimePickerDefaults.TonalElevation,
color: Color = MaterialTheme.colorScheme.surface,
properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
contentDescription: TimePickerDialogContentDescription,
) {
var mode: DisplayMode by rememberTimePickerDisplayMode()
// TimePicker does not provide a default TimePickerDialog, so we use our own PickerDialog:
// https://issuetracker.google.com/issues/288311426
PickerDialog(
modifier = modifier,
onDismissRequest = onDismissRequest,
title = title,
buttons = {
DisplayModeToggleButton(
displayMode = mode,
onDisplayModeChange = { mode = it },
contentDescription = contentDescription,
)
Spacer(Modifier.weight(1f))
dismissButton?.invoke()
confirmButton.invoke()
},
shape = shape,
tonalElevation = tonalElevation,
color = color,
properties = properties,
) {
val contentModifier = Modifier.padding(horizontal = 24.dp)
when (mode) {
DisplayMode.Picker -> TimePicker(modifier = contentModifier, state = state)
DisplayMode.Input -> TimeInput(modifier = contentModifier, state = state)
}
}
}
data class TimePickerDialogContentDescription(
val toggleKeyboardButton: String,
val toggleScheduleButton: String,
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DisplayModeToggleButton(
displayMode: DisplayMode,
onDisplayModeChange: (DisplayMode) -> Unit,
modifier: Modifier = Modifier,
contentDescription: TimePickerDialogContentDescription,
) {
when (displayMode) {
DisplayMode.Picker -> IconButton(
modifier = modifier,
onClick = { onDisplayModeChange(DisplayMode.Input) },
) {
Icon(
TimePickerDialogIcons.Keyboard,
contentDescription = contentDescription.modeToggleButtonKeyboard,
)
}
DisplayMode.Input -> IconButton(
modifier = modifier,
onClick = { onDisplayModeChange(DisplayMode.Picker) },
) {
Icon(
TimePickerDialogIcons.Schedule,
contentDescription = contentDescription.modeToggleButtonSchedule,
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberTimePickerDisplayMode(defaultMode: DisplayMode = DisplayMode.Picker): MutableState<DisplayMode> {
return rememberSaveable(saver = DisplayModeSaver(defaultMode)) {
mutableStateOf(defaultMode)
}
}
@OptIn(ExperimentalMaterial3Api::class)
class DisplayModeSaver(private val defaultMode: DisplayMode) :
Saver<MutableState<DisplayMode>, Int> {
override fun restore(value: Int): MutableState<DisplayMode> {
return mutableStateOf(
when (value) {
0 -> DisplayMode.Picker
1 -> DisplayMode.Input
else -> defaultMode
}
)
}
override fun SaverScope.save(value: MutableState<DisplayMode>): Int {
return when (value.value) {
DisplayMode.Picker -> 0
DisplayMode.Input -> 1
else -> throw IllegalArgumentException("Unknown DisplayMode value: $value")
}
}
}
private object TimePickerModalTokens {
val ContainerWidth = 360.0.dp
val ContainerHeight = 568.0.dp
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PickerDialog(
onDismissRequest: () -> Unit,
title: @Composable () -> Unit,
buttons: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
shape: Shape = TimePickerDefaults.shape,
tonalElevation: Dp = TimePickerDefaults.TonalElevation,
color: Color = MaterialTheme.colorScheme.surface,
properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
content: @Composable ColumnScope.() -> Unit
) {
BasicAlertDialog(
modifier = modifier
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min),
onDismissRequest = onDismissRequest,
properties = properties,
) {
Surface(
// shape = MaterialTheme.shapes.extraLarge,
// tonalElevation = 6.dp,
modifier = Modifier
.requiredWidth(TimePickerModalTokens.ContainerWidth)
.heightIn(max = TimePickerModalTokens.ContainerHeight),
shape = shape,
tonalElevation = tonalElevation,
color = color
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// Title
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
Box(
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 24.dp)
.padding(top = 16.dp, bottom = 20.dp),
) {
title()
}
}
}
// Content
CompositionLocalProvider(LocalContentColor provides AlertDialogDefaults.textContentColor) {
content()
}
// Buttons
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
// TODO This should wrap on small screens, but we can't use AlertDialogFlowRow as it is no public
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp, end = 6.dp, start = 6.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) {
buttons()
}
}
}
}
}
}
}
private object TimePickerDialogIcons
private val TimePickerDialogIcons.Keyboard: ImageVector
get() {
if (_keyboard != null) {
return _keyboard!!
}
_keyboard = materialIcon(name = "Filled.Keyboard") {
materialPath {
moveTo(20.0f, 5.0f)
lineTo(4.0f, 5.0f)
curveToRelative(-1.1f, 0.0f, -1.99f, 0.9f, -1.99f, 2.0f)
lineTo(2.0f, 17.0f)
curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
horizontalLineToRelative(16.0f)
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
lineTo(22.0f, 7.0f)
curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
close()
moveTo(11.0f, 8.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
horizontalLineToRelative(-2.0f)
lineTo(11.0f, 8.0f)
close()
moveTo(11.0f, 11.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
horizontalLineToRelative(-2.0f)
verticalLineToRelative(-2.0f)
close()
moveTo(8.0f, 8.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
lineTo(8.0f, 10.0f)
lineTo(8.0f, 8.0f)
close()
moveTo(8.0f, 11.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
lineTo(8.0f, 13.0f)
verticalLineToRelative(-2.0f)
close()
moveTo(7.0f, 13.0f)
lineTo(5.0f, 13.0f)
verticalLineToRelative(-2.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
close()
moveTo(7.0f, 10.0f)
lineTo(5.0f, 10.0f)
lineTo(5.0f, 8.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
close()
moveTo(16.0f, 17.0f)
lineTo(8.0f, 17.0f)
verticalLineToRelative(-2.0f)
horizontalLineToRelative(8.0f)
verticalLineToRelative(2.0f)
close()
moveTo(16.0f, 13.0f)
horizontalLineToRelative(-2.0f)
verticalLineToRelative(-2.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
close()
moveTo(16.0f, 10.0f)
horizontalLineToRelative(-2.0f)
lineTo(14.0f, 8.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
close()
moveTo(19.0f, 13.0f)
horizontalLineToRelative(-2.0f)
verticalLineToRelative(-2.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
close()
moveTo(19.0f, 10.0f)
horizontalLineToRelative(-2.0f)
lineTo(17.0f, 8.0f)
horizontalLineToRelative(2.0f)
verticalLineToRelative(2.0f)
close()
}
}
return _keyboard!!
}
private var _keyboard: ImageVector? = null
private val TimePickerDialogIcons.Schedule: ImageVector
get() {
if (_schedule != null) {
return _schedule!!
}
_schedule = materialIcon(name = "Filled.Schedule") {
materialPath {
moveTo(11.99f, 2.0f)
curveTo(6.47f, 2.0f, 2.0f, 6.48f, 2.0f, 12.0f)
reflectiveCurveToRelative(4.47f, 10.0f, 9.99f, 10.0f)
curveTo(17.52f, 22.0f, 22.0f, 17.52f, 22.0f, 12.0f)
reflectiveCurveTo(17.52f, 2.0f, 11.99f, 2.0f)
close()
moveTo(12.0f, 20.0f)
curveToRelative(-4.42f, 0.0f, -8.0f, -3.58f, -8.0f, -8.0f)
reflectiveCurveToRelative(3.58f, -8.0f, 8.0f, -8.0f)
reflectiveCurveToRelative(8.0f, 3.58f, 8.0f, 8.0f)
reflectiveCurveToRelative(-3.58f, 8.0f, -8.0f, 8.0f)
close()
}
materialPath {
moveTo(12.5f, 7.0f)
horizontalLineTo(11.0f)
verticalLineToRelative(6.0f)
lineToRelative(5.25f, 3.15f)
lineToRelative(0.75f, -1.23f)
lineToRelative(-4.5f, -2.67f)
close()
}
}
return _schedule!!
}
private var _schedule: ImageVector? = null
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment