Skip to content

Commit 1c0bb48

Browse files
committed
Migrate Timepicker (wip)
1 parent b63fa79 commit 1c0bb48

File tree

4 files changed

+333
-49
lines changed

4 files changed

+333
-49
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.datacapture.views.compose
18+
19+
import androidx.compose.foundation.interaction.MutableInteractionSource
20+
import androidx.compose.foundation.interaction.PressInteraction
21+
import androidx.compose.foundation.text.KeyboardActions
22+
import androidx.compose.foundation.text.KeyboardOptions
23+
import androidx.compose.material3.Icon
24+
import androidx.compose.material3.IconButton
25+
import androidx.compose.material3.OutlinedTextField
26+
import androidx.compose.material3.Text
27+
import androidx.compose.runtime.Composable
28+
import androidx.compose.runtime.LaunchedEffect
29+
import androidx.compose.runtime.getValue
30+
import androidx.compose.runtime.mutableStateOf
31+
import androidx.compose.runtime.remember
32+
import androidx.compose.runtime.setValue
33+
import androidx.compose.ui.Modifier
34+
import androidx.compose.ui.focus.FocusDirection
35+
import androidx.compose.ui.focus.onFocusChanged
36+
import androidx.compose.ui.platform.LocalFocusManager
37+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
38+
import androidx.compose.ui.platform.testTag
39+
import androidx.compose.ui.res.painterResource
40+
import androidx.compose.ui.res.stringResource
41+
import androidx.compose.ui.semantics.error
42+
import androidx.compose.ui.semantics.semantics
43+
import androidx.compose.ui.text.input.ImeAction
44+
import androidx.compose.ui.text.input.KeyboardType
45+
import androidx.compose.ui.tooling.preview.Preview
46+
import com.google.android.fhir.datacapture.R
47+
import java.time.LocalTime
48+
49+
@Composable
50+
internal fun TimePickerItem(
51+
modifier: Modifier = Modifier,
52+
selectedTime: String?,
53+
enabled: Boolean,
54+
hint: String,
55+
supportingHelperText: String?,
56+
isError: Boolean,
57+
onTimeChanged: (LocalTime) -> Unit,
58+
) {
59+
val focusManager = LocalFocusManager.current
60+
val keyboardController = LocalSoftwareKeyboardController.current
61+
var selectedTimeText by remember(selectedTime) { mutableStateOf(selectedTime ?: "") }
62+
var showTimePickerModal by remember { mutableStateOf(false) }
63+
64+
OutlinedTextField(
65+
value = selectedTimeText,
66+
onValueChange = {},
67+
singleLine = true,
68+
label = { Text(hint) },
69+
modifier =
70+
modifier
71+
.testTag(TIME_PICKER_INPUT_FIELD)
72+
.onFocusChanged {
73+
if (!it.isFocused) {
74+
keyboardController?.hide()
75+
}
76+
}
77+
.semantics {
78+
if (isError && !supportingHelperText.isNullOrBlank()) error(supportingHelperText)
79+
},
80+
supportingText = { supportingHelperText?.let { Text(it) } },
81+
isError = isError,
82+
trailingIcon = {
83+
IconButton(
84+
onClick = {
85+
// todo: show with time picker input
86+
showTimePickerModal = true
87+
},
88+
enabled = enabled,
89+
) {
90+
Icon(
91+
painterResource(R.drawable.gm_schedule_24),
92+
contentDescription = stringResource(R.string.select_time),
93+
)
94+
}
95+
},
96+
readOnly = true,
97+
enabled = enabled,
98+
keyboardOptions =
99+
KeyboardOptions(
100+
autoCorrectEnabled = false,
101+
keyboardType = KeyboardType.Number,
102+
imeAction = ImeAction.Done,
103+
),
104+
keyboardActions =
105+
KeyboardActions(
106+
onNext = { focusManager.moveFocus(FocusDirection.Down) },
107+
),
108+
interactionSource =
109+
remember { MutableInteractionSource() }
110+
.also { interactionSource ->
111+
LaunchedEffect(interactionSource) {
112+
interactionSource.interactions.collect {
113+
if (it is PressInteraction.Release) {
114+
// todo: show with keyboard input
115+
showTimePickerModal = true
116+
}
117+
}
118+
}
119+
},
120+
)
121+
122+
if (showTimePickerModal) {
123+
TimePickerSwitchable(onDismiss = { showTimePickerModal = false }) { hour, min ->
124+
val localTime = LocalTime.of(hour, min)
125+
onTimeChanged(localTime)
126+
}
127+
}
128+
}
129+
130+
@Composable
131+
@Preview
132+
fun PreviewTimePickerItem() {
133+
TimePickerItem(Modifier, null, true, stringResource(R.string.time), null, false) {}
134+
}
135+
136+
const val TIME_PICKER_INPUT_FIELD = "time_picker_text_field"
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.datacapture.views.compose
18+
19+
import androidx.compose.foundation.layout.Arrangement
20+
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.Row
22+
import androidx.compose.foundation.layout.fillMaxWidth
23+
import androidx.compose.material.icons.Icons
24+
import androidx.compose.material.icons.filled.Call
25+
import androidx.compose.material.icons.filled.Edit
26+
import androidx.compose.material.icons.outlined.Call
27+
import androidx.compose.material.icons.outlined.Edit
28+
import androidx.compose.material3.AlertDialog
29+
import androidx.compose.material3.ExperimentalMaterial3Api
30+
import androidx.compose.material3.Icon
31+
import androidx.compose.material3.IconButton
32+
import androidx.compose.material3.Text
33+
import androidx.compose.material3.TextButton
34+
import androidx.compose.material3.TimeInput
35+
import androidx.compose.material3.TimePicker
36+
import androidx.compose.material3.rememberTimePickerState
37+
import androidx.compose.runtime.Composable
38+
import androidx.compose.runtime.getValue
39+
import androidx.compose.runtime.mutableStateOf
40+
import androidx.compose.runtime.remember
41+
import androidx.compose.runtime.setValue
42+
import androidx.compose.ui.Alignment
43+
import androidx.compose.ui.Modifier
44+
import androidx.compose.ui.res.stringResource
45+
import androidx.compose.ui.tooling.preview.Preview
46+
import com.google.android.fhir.datacapture.R
47+
48+
@OptIn(ExperimentalMaterial3Api::class)
49+
@Composable
50+
fun TimePickerSwitchable(
51+
showPicker: Boolean = true,
52+
onDismiss: () -> Unit,
53+
onConfirm: (Int, Int) -> Unit,
54+
) {
55+
val timePickerState = rememberTimePickerState()
56+
var showTimePicker by remember(showPicker) { mutableStateOf(showPicker) }
57+
58+
AlertDialog(
59+
onDismissRequest = onDismiss,
60+
confirmButton = {
61+
TextButton(
62+
onClick = {
63+
onConfirm(timePickerState.hour, timePickerState.minute)
64+
onDismiss()
65+
},
66+
) {
67+
Text("OK")
68+
}
69+
},
70+
dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } },
71+
text = {
72+
Column(
73+
verticalArrangement = Arrangement.Center,
74+
horizontalAlignment = Alignment.CenterHorizontally,
75+
) {
76+
if (showTimePicker) {
77+
TimePicker(state = timePickerState)
78+
} else {
79+
TimeInput(state = timePickerState)
80+
}
81+
Row(
82+
modifier = Modifier.fillMaxWidth(),
83+
horizontalArrangement = Arrangement.Start,
84+
verticalAlignment = Alignment.CenterVertically,
85+
) {
86+
IconButton(onClick = { showTimePicker = !showTimePicker }) {
87+
val icon = if (showTimePicker) Icons.Filled.Edit else Icons.Filled.Call
88+
Icon(
89+
icon,
90+
contentDescription =
91+
if (showTimePicker) "Switch to text input" else "Switch to clock input",
92+
)
93+
}
94+
}
95+
}
96+
},
97+
title = { Text(stringResource(R.string.select_time)) },
98+
)
99+
}
100+
101+
@Preview
102+
@Composable
103+
fun TimePickerSwitchablePreview() {
104+
TimePickerSwitchable(onDismiss = {}, onConfirm = { _, _ -> })
105+
}

datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ internal object DatePickerViewHolderFactory : QuestionnaireItemComposeViewHolder
8888
)
8989
}
9090
val questionnaireItemAnswerLocalDate =
91-
questionnaireViewItem.answers.singleOrNull()?.valueDateType?.localDate
91+
remember(questionnaireViewItem.answers) {
92+
questionnaireViewItem.answers.singleOrNull()?.valueDateType?.localDate
93+
}
9294
val questionnaireItemAnswerDateInMillis =
9395
remember(questionnaireItemAnswerLocalDate) {
9496
questionnaireItemAnswerLocalDate?.atStartOfDay(ZONE_ID_UTC)?.toInstant()?.toEpochMilli()

0 commit comments

Comments
 (0)