Skip to content

Commit

Permalink
Add a new Time selector widget
Browse files Browse the repository at this point in the history
  • Loading branch information
aditya-07 committed Dec 23, 2024
1 parent d37ef08 commit 152c446
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 1 deletion.
36 changes: 36 additions & 0 deletions catalog/src/main/assets/component_time_picker.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"text": "Enter a time",
"type": "time",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
"valueString": "hh-mm"
}
],
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
}
],
"linkId": "1-most-recent",
"text": "Use keyboard entry or time picker",
"type": "display"
}
]
}
]
}
37 changes: 37 additions & 0 deletions catalog/src/main/assets/component_time_picker_with_validation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"text": "Enter a time",
"type": "time",
"required": true,
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
"valueString": "hh-mm"
}
],
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
}
],
"linkId": "1-most-recent",
"text": "Use keyboard entry or time picker",
"type": "display"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ class ComponentListViewModel(application: Application, private val state: SavedS
"component_date_picker.json",
"component_date_picker_with_validation.json",
),
TIME_PICKER(
R.drawable.ic_timepicker,
R.string.component_name_time_picker,
"component_time_picker.json",
"component_time_picker_with_validation.json",
),
DATE_TIME_PICKER(
R.drawable.ic_timepicker,
R.string.component_name_date_time_picker,
Expand Down Expand Up @@ -171,6 +177,7 @@ class ComponentListViewModel(application: Application, private val state: SavedS
ViewItem.ComponentItem(Component.TEXT_FIELD),
ViewItem.ComponentItem(Component.AUTO_COMPLETE),
ViewItem.ComponentItem(Component.DATE_PICKER),
ViewItem.ComponentItem(Component.TIME_PICKER),
ViewItem.ComponentItem(Component.DATE_TIME_PICKER),
ViewItem.ComponentItem(Component.SLIDER),
ViewItem.ComponentItem(Component.QUANTITY),
Expand Down
1 change: 1 addition & 0 deletions catalog/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<string name="component_name_text_field">Text field </string>
<string name="component_name_auto_complete">Auto Complete</string>
<string name="component_name_date_picker">Date picker</string>
<string name="component_name_time_picker">Time picker</string>
<string name="component_name_date_time_picker">DateTime picker</string>
<string name="component_name_slider">Slider</string>
<string name="component_name_quantity">Quantity</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemView
import com.google.android.fhir.datacapture.views.factories.RadioGroupViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.RepeatedGroupHeaderItemViewHolder
import com.google.android.fhir.datacapture.views.factories.SliderViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.TimePickerViewHolderFactory
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType

internal class QuestionnaireEditAdapter(
Expand Down Expand Up @@ -103,6 +104,7 @@ internal class QuestionnaireEditAdapter(
QuestionnaireViewHolderType.GROUP -> GroupViewHolderFactory
QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER -> BooleanChoiceViewHolderFactory
QuestionnaireViewHolderType.DATE_PICKER -> DatePickerViewHolderFactory
QuestionnaireViewHolderType.TIME_PICKER -> TimePickerViewHolderFactory
QuestionnaireViewHolderType.DATE_TIME_PICKER -> DateTimePickerViewHolderFactory
QuestionnaireViewHolderType.EDIT_TEXT_SINGLE_LINE -> EditTextSingleLineViewHolderFactory
QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE -> EditTextMultiLineViewHolderFactory
Expand Down Expand Up @@ -223,6 +225,7 @@ internal class QuestionnaireEditAdapter(
QuestionnaireItemType.GROUP -> QuestionnaireViewHolderType.GROUP
QuestionnaireItemType.BOOLEAN -> QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER
QuestionnaireItemType.DATE -> QuestionnaireViewHolderType.DATE_PICKER
QuestionnaireItemType.TIME -> QuestionnaireViewHolderType.TIME_PICKER
QuestionnaireItemType.DATETIME -> QuestionnaireViewHolderType.DATE_TIME_PICKER
QuestionnaireItemType.STRING -> getStringViewHolderType(questionnaireViewItem)
QuestionnaireItemType.TEXT -> QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Google LLC
* Copyright 2023-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -45,6 +45,7 @@ enum class QuestionnaireViewHolderType(val value: Int) {
SLIDER(15),
PHONE_NUMBER(16),
ATTACHMENT(17),
TIME_PICKER(18),
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.datacapture.views.factories

import android.annotation.SuppressLint
import android.content.Context
import android.text.InputType
import android.text.format.DateFormat
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.getRequiredOrOptionalText
import com.google.android.fhir.datacapture.extensions.toLocalizedString
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import com.google.android.fhir.datacapture.views.HeaderView
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.MaterialTimePicker.INPUT_MODE_CLOCK
import com.google.android.material.timepicker.MaterialTimePicker.INPUT_MODE_KEYBOARD
import com.google.android.material.timepicker.TimeFormat
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.TimeType

object TimePickerViewHolderFactory : QuestionnaireItemViewHolderFactory(R.layout.time_picker_view) {

override fun getQuestionnaireItemViewHolderDelegate() =
object : QuestionnaireItemViewHolderDelegate {
private val TAG = "time-picker"
private lateinit var context: AppCompatActivity
private lateinit var header: HeaderView
private lateinit var timeInputLayout: TextInputLayout
private lateinit var timeInputEditText: TextInputEditText
override lateinit var questionnaireViewItem: QuestionnaireViewItem

override fun init(itemView: View) {
context = itemView.context.tryUnwrapContext()!!
header = itemView.findViewById(R.id.header)
timeInputLayout = itemView.findViewById(R.id.text_input_layout)
timeInputEditText = itemView.findViewById(R.id.text_input_edit_text)
timeInputEditText.inputType = InputType.TYPE_NULL
timeInputEditText.hint = itemView.context.getString(R.string.time)

timeInputLayout.setEndIconOnClickListener {
// The application is wrapped in a ContextThemeWrapper in QuestionnaireFragment
// and again in TextInputEditText during layout inflation. As a result, it is
// necessary to access the base context twice to retrieve the application object
// from the view's context.
val context = itemView.context.tryUnwrapContext()!!
buildMaterialTimePicker(context, INPUT_MODE_CLOCK)
}
timeInputEditText.setOnClickListener {
buildMaterialTimePicker(itemView.context, INPUT_MODE_KEYBOARD)
}
}

@SuppressLint("NewApi") // java.time APIs can be used due to desugaring
override fun bind(questionnaireViewItem: QuestionnaireViewItem) {
clearPreviousState()
header.bind(questionnaireViewItem)
timeInputLayout.helperText = getRequiredOrOptionalText(questionnaireViewItem, context)

val questionnaireItemViewItemDateTimeAnswer =
questionnaireViewItem.answers.singleOrNull()?.valueTimeType?.localTime

// If there is no set answer in the QuestionnaireItemViewItem, make the time field empty.
timeInputEditText.setText(
questionnaireItemViewItemDateTimeAnswer?.toLocalizedString(timeInputEditText.context)
?: "",
)
}

override fun setReadOnly(isReadOnly: Boolean) {
// The system outside this delegate should only be able to mark it read only. Otherwise, it
// will change the state set by this delegate in bindView().
if (isReadOnly) {
timeInputEditText.isEnabled = false
timeInputLayout.isEnabled = false
}
}

private fun buildMaterialTimePicker(context: Context, inputMode: Int) {
val selectedTime =
questionnaireViewItem.answers.singleOrNull()?.valueTimeType?.localTime ?: LocalTime.now()
val timeFormat =
if (DateFormat.is24HourFormat(context)) {
TimeFormat.CLOCK_24H
} else {
TimeFormat.CLOCK_12H
}
MaterialTimePicker.Builder()
.setTitleText(R.string.select_time)
.setHour(selectedTime.hour)
.setMinute(selectedTime.minute)
.setTimeFormat(timeFormat)
.setInputMode(inputMode)
.build()
.apply {
addOnPositiveButtonClickListener {
with(LocalTime.of(this.hour, this.minute, 0)) {
timeInputEditText.setText(this.toLocalizedString(context))
setQuestionnaireItemViewItemAnswer(this)
timeInputEditText.clearFocus()
}
}
}
.show(context.tryUnwrapContext()!!.supportFragmentManager, TAG)
}

/** Set the answer in the [QuestionnaireResponse]. */
private fun setQuestionnaireItemViewItemAnswer(localDateTime: LocalTime) =
context.lifecycleScope.launch {
questionnaireViewItem.setAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
.setValue(TimeType(localDateTime.format(DateTimeFormatter.ISO_TIME))),
)
}

private fun clearPreviousState() {
timeInputEditText.isEnabled = true
timeInputLayout.isEnabled = true
}
}

private val TimeType.localTime
get() =
LocalTime.of(
hour,
minute,
second.toInt(),
)
}
57 changes: 57 additions & 0 deletions datacapture/src/main/res/layout/time_picker_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2020 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/item_margin_horizontal"
android:layout_marginVertical="@dimen/item_margin_vertical"
android:orientation="vertical"
>

<com.google.android.fhir.datacapture.views.HeaderView
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

<com.google.android.fhir.datacapture.views.MediaView
android:id="@+id/item_media"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

<com.google.android.material.textfield.TextInputLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/text_input_layout"
style="?attr/questionnaireTextInputLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:endIconDrawable="@drawable/gm_schedule_24"
app:endIconMode="custom"
app:errorIconDrawable="@null"
app:hintEnabled="true"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_input_edit_text"
style="?attr/questionnaireTextInputEditTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="time"
android:singleLine="true"
/>

</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

0 comments on commit 152c446

Please sign in to comment.