1616
1717package com.google.android.fhir.datacapture.views.factories
1818
19- import android.view.View
20- import android.widget.AdapterView
21- import android.widget.ArrayAdapter
22- import android.widget.TextView
23- import androidx.appcompat.app.AppCompatActivity
2419import androidx.compose.foundation.layout.Column
2520import androidx.compose.foundation.layout.fillMaxWidth
2621import androidx.compose.foundation.layout.padding
@@ -33,47 +28,23 @@ import androidx.compose.runtime.setValue
3328import androidx.compose.ui.Modifier
3429import androidx.compose.ui.platform.LocalContext
3530import androidx.compose.ui.res.dimensionResource
36- import androidx.core.view.children
37- import androidx.core.view.get
38- import androidx.core.view.isEmpty
39- import androidx.lifecycle.lifecycleScope
4031import com.google.android.fhir.datacapture.R
4132import com.google.android.fhir.datacapture.extensions.displayString
4233import com.google.android.fhir.datacapture.extensions.identifierString
4334import com.google.android.fhir.datacapture.extensions.itemMedia
44- import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
4535import com.google.android.fhir.datacapture.validation.Invalid
46- import com.google.android.fhir.datacapture.validation.NotValidated
47- import com.google.android.fhir.datacapture.validation.Valid
48- import com.google.android.fhir.datacapture.validation.ValidationResult
49- import com.google.android.fhir.datacapture.views.HeaderView
5036import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
5137import com.google.android.fhir.datacapture.views.compose.Header
5238import com.google.android.fhir.datacapture.views.compose.MediaItem
5339import com.google.android.fhir.datacapture.views.compose.MultiAutoCompleteTextItem
54- import com.google.android.material.chip.Chip
55- import com.google.android.material.chip.ChipGroup
56- import com.google.android.material.textfield.MaterialAutoCompleteTextView
57- import com.google.android.material.textfield.TextInputLayout
5840import kotlinx.coroutines.Dispatchers
5941import kotlinx.coroutines.launch
60- import org.hl7.fhir.r4.model.Coding
6142import org.hl7.fhir.r4.model.QuestionnaireResponse
6243
6344internal object AutoCompleteViewHolderFactory : QuestionnaireItemComposeViewHolderFactory {
6445
6546 override fun getQuestionnaireItemViewHolderDelegate () =
6647 object : QuestionnaireItemComposeViewHolderDelegate {
67- private lateinit var context: AppCompatActivity
68- private lateinit var header: HeaderView
69- private lateinit var autoCompleteTextView: MaterialAutoCompleteTextView
70- private lateinit var chipContainer: ChipGroup
71- private lateinit var textInputLayout: TextInputLayout
72- private val canHaveMultipleAnswers
73- get() = questionnaireViewItem.questionnaireItem.repeats
74-
75- lateinit var questionnaireViewItem: QuestionnaireViewItem
76- private lateinit var errorTextView: TextView
7748
7849 @Composable
7950 override fun Content (questionnaireViewItem : QuestionnaireViewItem ) {
@@ -192,191 +163,5 @@ internal object AutoCompleteViewHolderFactory : QuestionnaireItemComposeViewHold
192163 )
193164 }
194165 }
195-
196- fun init (itemView : View ) {
197- context = itemView.context.tryUnwrapContext()!!
198- header = itemView.findViewById(R .id.header)
199- autoCompleteTextView = itemView.findViewById(R .id.autoCompleteTextView)
200- chipContainer = itemView.findViewById(R .id.chipContainer)
201- textInputLayout = itemView.findViewById(R .id.text_input_layout)
202- errorTextView = itemView.findViewById(R .id.error)
203- autoCompleteTextView.onItemClickListener =
204- AdapterView .OnItemClickListener { _, _, position, _ ->
205- val answer =
206- QuestionnaireResponse .QuestionnaireResponseItemAnswerComponent ().apply {
207- value =
208- questionnaireViewItem.enabledAnswerOptions
209- .first {
210- it.value.identifierString(header.context) ==
211- (autoCompleteTextView.adapter.getItem(position)
212- as AutoCompleteViewAnswerOption )
213- .answerId
214- }
215- .valueCoding
216- }
217-
218- handleAnswerSelection(answer)
219- autoCompleteTextView.setText(" " )
220- }
221- }
222-
223- fun bind (questionnaireViewItem : QuestionnaireViewItem ) {
224- header.bind(questionnaireViewItem, showRequiredOrOptionalText = true )
225- val answerOptionValues =
226- questionnaireViewItem.enabledAnswerOptions.map {
227- AutoCompleteViewAnswerOption (
228- answerId = it.value.identifierString(header.context),
229- answerDisplay = it.value.displayString(header.context),
230- )
231- }
232- val adapter =
233- ArrayAdapter (
234- header.context,
235- R .layout.drop_down_list_item,
236- R .id.answer_option_textview,
237- answerOptionValues,
238- )
239- autoCompleteTextView.setAdapter(adapter)
240- // Remove chips if any from the last bindView call on this VH.
241- chipContainer.removeAllViews()
242- presetValuesIfAny()
243-
244- displayValidationResult(questionnaireViewItem.validationResult)
245- }
246-
247- fun setReadOnly (isReadOnly : Boolean ) {
248- for (i in 0 until chipContainer.childCount) {
249- val view = chipContainer.getChildAt(i)
250- view.isEnabled = ! isReadOnly
251- if (view is Chip && isReadOnly) {
252- view.setOnCloseIconClickListener(null )
253- }
254- }
255- textInputLayout.isEnabled = ! isReadOnly
256- }
257-
258- private fun presetValuesIfAny () {
259- questionnaireViewItem.answers.map { answer -> addNewChipIfNotPresent(answer) }
260- }
261-
262- private fun handleAnswerSelection (
263- answer : QuestionnaireResponse .QuestionnaireResponseItemAnswerComponent ,
264- ) {
265- if (canHaveMultipleAnswers) {
266- handleSelectionWhenQuestionCanHaveMultipleAnswers(answer)
267- } else {
268- handleSelectionWhenQuestionCanHaveSingleAnswer(answer)
269- }
270- }
271-
272- /* *
273- * Adds a new chip if it not already present in [chipContainer].It returns [true] if a new
274- * Chip is added and [false] if the Chip is already present for the selected answer. The later
275- * will happen if the user selects an already selected answer.
276- */
277- private fun addNewChipIfNotPresent (
278- answer : QuestionnaireResponse .QuestionnaireResponseItemAnswerComponent ,
279- ): Boolean {
280- if (chipIsAlreadyPresent(answer)) return false
281-
282- val chip = Chip (chipContainer.context, null , R .attr.questionnaireChipStyle)
283- chip.id = View .generateViewId()
284- chip.text = answer.valueCoding.displayOrCode
285- chip.isCloseIconVisible = true
286- chip.isClickable = true
287- chip.isCheckable = false
288- chip.tag = answer
289- chip.setOnCloseIconClickListener {
290- chipContainer.removeView(chip)
291- onChipRemoved(chip)
292- }
293-
294- chipContainer.addView(chip)
295- return true
296- }
297-
298- private fun chipIsAlreadyPresent (
299- answer : QuestionnaireResponse .QuestionnaireResponseItemAnswerComponent ,
300- ): Boolean {
301- return chipContainer.children.any { chip ->
302- (chip.tag as QuestionnaireResponse .QuestionnaireResponseItemAnswerComponent )
303- .value
304- .equalsDeep(answer.value)
305- }
306- }
307-
308- private fun handleSelectionWhenQuestionCanHaveSingleAnswer (
309- answer : QuestionnaireResponse .QuestionnaireResponseItemAnswerComponent ,
310- ) {
311- if (chipContainer.isEmpty()) {
312- addNewChipIfNotPresent(answer)
313- } else {
314- (chipContainer[0 ] as Chip ).apply {
315- text = answer.valueCoding.displayOrCode
316- tag = answer
317- }
318- }
319- context.lifecycleScope.launch { questionnaireViewItem.setAnswer(answer) }
320- }
321-
322- private fun handleSelectionWhenQuestionCanHaveMultipleAnswers (
323- answer : QuestionnaireResponse .QuestionnaireResponseItemAnswerComponent ,
324- ) {
325- val answerNotPresent =
326- questionnaireViewItem.answers.none { it.value.equalsDeep(answer.value) }
327-
328- if (answerNotPresent) {
329- addNewChipIfNotPresent(answer)
330- context.lifecycleScope.launch { questionnaireViewItem.addAnswer(answer) }
331- }
332- }
333-
334- private fun onChipRemoved (chip : Chip ) {
335- context.lifecycleScope.launch {
336- if (canHaveMultipleAnswers) {
337- (chip.tag as QuestionnaireResponse .QuestionnaireResponseItemAnswerComponent ).let {
338- questionnaireViewItem.removeAnswer(it)
339- }
340- } else {
341- questionnaireViewItem.clearAnswer()
342- }
343- }
344- }
345-
346- private fun displayValidationResult (validationResult : ValidationResult ) {
347- // https://github.com/material-components/material-components-android/issues/1435
348- // Because of the above issue, we use separate error textview. But we still use
349- // textInputLayout to show the error icon and the box color.
350- when (validationResult) {
351- is NotValidated ,
352- Valid , -> {
353- errorTextView.visibility = View .GONE
354- textInputLayout.error = null
355- }
356- is Invalid -> {
357- errorTextView.text = validationResult.getSingleStringValidationMessage()
358- errorTextView.visibility = View .VISIBLE
359- textInputLayout.error = " " // non empty text
360- }
361- }
362- }
363-
364- private val Coding .displayOrCode: String
365- get() =
366- if (display.isNullOrBlank()) {
367- code
368- } else {
369- display
370- }
371166 }
372167}
373-
374- /* *
375- * An answer option that would show up as a dropdown item in an [AutoCompleteViewHolderFactory]
376- * textview
377- */
378- internal data class AutoCompleteViewAnswerOption (val answerId : String , val answerDisplay : String ) {
379- override fun toString (): String {
380- return this .answerDisplay
381- }
382- }
0 commit comments