Last active
March 3, 2024 15:12
-
-
Save wilinz/20c9b36493649e165d3f5600e897774f to your computer and use it in GitHub Desktop.
Compose Material3 TimePickerDialog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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" | |
) | |
) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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