|
16 | 16 |
|
17 | 17 | package com.google.android.fhir.datacapture.views.factories |
18 | 18 |
|
19 | | -import android.view.LayoutInflater |
20 | | -import android.view.View |
21 | | -import android.view.ViewGroup |
22 | | -import android.widget.RadioButton |
23 | | -import androidx.appcompat.app.AppCompatActivity |
24 | | -import androidx.constraintlayout.helper.widget.Flow |
25 | | -import androidx.constraintlayout.widget.ConstraintLayout |
26 | | -import androidx.core.view.children |
27 | | -import androidx.lifecycle.lifecycleScope |
| 19 | +import androidx.compose.foundation.layout.Arrangement |
| 20 | +import androidx.compose.foundation.layout.Column |
| 21 | +import androidx.compose.foundation.layout.FlowRow |
| 22 | +import androidx.compose.foundation.layout.fillMaxWidth |
| 23 | +import androidx.compose.foundation.layout.padding |
| 24 | +import androidx.compose.foundation.selection.selectableGroup |
| 25 | +import androidx.compose.runtime.Composable |
| 26 | +import androidx.compose.runtime.getValue |
| 27 | +import androidx.compose.runtime.mutableStateOf |
| 28 | +import androidx.compose.runtime.remember |
| 29 | +import androidx.compose.runtime.rememberCoroutineScope |
| 30 | +import androidx.compose.runtime.setValue |
| 31 | +import androidx.compose.ui.Modifier |
| 32 | +import androidx.compose.ui.platform.LocalContext |
| 33 | +import androidx.compose.ui.platform.testTag |
| 34 | +import androidx.compose.ui.res.dimensionResource |
| 35 | +import androidx.compose.ui.text.AnnotatedString |
28 | 36 | import com.google.android.fhir.datacapture.R |
29 | 37 | import com.google.android.fhir.datacapture.extensions.ChoiceOrientationTypes |
30 | 38 | import com.google.android.fhir.datacapture.extensions.choiceOrientation |
31 | | -import com.google.android.fhir.datacapture.extensions.displayStringSpanned |
| 39 | +import com.google.android.fhir.datacapture.extensions.displayString |
32 | 40 | import com.google.android.fhir.datacapture.extensions.itemAnswerOptionImage |
33 | | -import com.google.android.fhir.datacapture.extensions.tryUnwrapContext |
34 | | -import com.google.android.fhir.datacapture.views.HeaderView |
| 41 | +import com.google.android.fhir.datacapture.extensions.itemMedia |
35 | 42 | import com.google.android.fhir.datacapture.views.QuestionnaireViewItem |
| 43 | +import com.google.android.fhir.datacapture.views.compose.ChoiceRadioButton |
| 44 | +import com.google.android.fhir.datacapture.views.compose.Header |
| 45 | +import com.google.android.fhir.datacapture.views.compose.MediaItem |
| 46 | +import kotlinx.coroutines.Dispatchers |
36 | 47 | import kotlinx.coroutines.launch |
37 | | -import org.hl7.fhir.r4.model.Questionnaire |
38 | 48 | import org.hl7.fhir.r4.model.QuestionnaireResponse |
39 | 49 |
|
40 | | -internal object RadioGroupViewHolderFactory : |
41 | | - QuestionnaireItemAndroidViewHolderFactory(R.layout.radio_group_view) { |
| 50 | +internal object RadioGroupViewHolderFactory : QuestionnaireItemComposeViewHolderFactory { |
42 | 51 | override fun getQuestionnaireItemViewHolderDelegate() = |
43 | | - object : QuestionnaireItemAndroidViewHolderDelegate { |
44 | | - private lateinit var appContext: AppCompatActivity |
45 | | - private lateinit var header: HeaderView |
46 | | - private lateinit var radioGroup: ConstraintLayout |
47 | | - private lateinit var flow: Flow |
48 | | - override lateinit var questionnaireViewItem: QuestionnaireViewItem |
| 52 | + object : QuestionnaireItemComposeViewHolderDelegate { |
49 | 53 |
|
50 | | - override fun init(itemView: View) { |
51 | | - appContext = itemView.context.tryUnwrapContext()!! |
52 | | - header = itemView.findViewById(R.id.header) |
53 | | - radioGroup = itemView.findViewById(R.id.radio_group) |
54 | | - flow = itemView.findViewById(R.id.flow) |
55 | | - } |
56 | | - |
57 | | - override fun bind(questionnaireViewItem: QuestionnaireViewItem) { |
58 | | - header.bind( |
59 | | - questionnaireViewItem, |
60 | | - showRequiredOrOptionalText = true, |
61 | | - displayValidationResult = true, |
62 | | - ) |
63 | | - // Keep the Flow layout which is the first child |
64 | | - radioGroup.removeViews(1, radioGroup.childCount - 1) |
65 | | - val choiceOrientation = |
66 | | - questionnaireViewItem.questionnaireItem.choiceOrientation |
67 | | - ?: ChoiceOrientationTypes.VERTICAL |
68 | | - when (choiceOrientation) { |
69 | | - ChoiceOrientationTypes.HORIZONTAL -> { |
70 | | - flow.setOrientation(Flow.HORIZONTAL) |
71 | | - flow.setWrapMode(Flow.WRAP_CHAIN) |
| 54 | + @Composable |
| 55 | + override fun Content(questionnaireViewItem: QuestionnaireViewItem) { |
| 56 | + val context = LocalContext.current |
| 57 | + val coroutineScope = rememberCoroutineScope { Dispatchers.Main } |
| 58 | + val readOnly = |
| 59 | + remember(questionnaireViewItem) { questionnaireViewItem.questionnaireItem.readOnly } |
| 60 | + val choiceOrientationType = |
| 61 | + remember(questionnaireViewItem) { |
| 62 | + questionnaireViewItem.questionnaireItem.choiceOrientation |
| 63 | + ?: ChoiceOrientationTypes.VERTICAL |
72 | 64 | } |
73 | | - ChoiceOrientationTypes.VERTICAL -> { |
74 | | - flow.setOrientation(Flow.VERTICAL) |
75 | | - flow.setWrapMode(Flow.WRAP_NONE) |
| 65 | + val enabledAnswerOptions = |
| 66 | + remember(questionnaireViewItem) { questionnaireViewItem.enabledAnswerOptions } |
| 67 | + var selectedAnswerOption by |
| 68 | + remember(questionnaireViewItem) { |
| 69 | + mutableStateOf( |
| 70 | + enabledAnswerOptions.singleOrNull { |
| 71 | + questionnaireViewItem.isAnswerOptionSelected(it) |
| 72 | + }, |
| 73 | + ) |
76 | 74 | } |
77 | | - } |
78 | | - questionnaireViewItem.enabledAnswerOptions |
79 | | - .map { answerOption -> View.generateViewId() to answerOption } |
80 | | - .onEach { populateViewWithAnswerOption(it.first, it.second, choiceOrientation) } |
81 | | - .map { it.first } |
82 | | - .let { flow.referencedIds = it.toIntArray() } |
83 | | - } |
84 | 75 |
|
85 | | - override fun setReadOnly(isReadOnly: Boolean) { |
86 | | - // The Flow layout has index 0. The radio button indices start from 1. |
87 | | - for (i in 1 until radioGroup.childCount) { |
88 | | - val view = radioGroup.getChildAt(i) |
89 | | - view.isEnabled = !isReadOnly |
90 | | - } |
91 | | - } |
92 | | - |
93 | | - private fun populateViewWithAnswerOption( |
94 | | - viewId: Int, |
95 | | - answerOption: Questionnaire.QuestionnaireItemAnswerOptionComponent, |
96 | | - choiceOrientation: ChoiceOrientationTypes, |
97 | | - ) { |
98 | | - val radioButtonItem = |
99 | | - LayoutInflater.from(radioGroup.context).inflate(R.layout.radio_button, null) |
100 | | - var isCurrentlySelected = questionnaireViewItem.isAnswerOptionSelected(answerOption) |
101 | | - val radioButton = |
102 | | - radioButtonItem.findViewById<RadioButton>(R.id.radio_button).apply { |
103 | | - id = viewId |
104 | | - text = answerOption.value.displayStringSpanned(header.context) |
105 | | - setCompoundDrawablesRelative( |
106 | | - answerOption.itemAnswerOptionImage(radioGroup.context), |
107 | | - null, |
108 | | - null, |
109 | | - null, |
110 | | - ) |
111 | | - layoutParams = |
112 | | - ViewGroup.LayoutParams( |
113 | | - when (choiceOrientation) { |
114 | | - ChoiceOrientationTypes.HORIZONTAL -> ViewGroup.LayoutParams.WRAP_CONTENT |
115 | | - ChoiceOrientationTypes.VERTICAL -> ViewGroup.LayoutParams.MATCH_PARENT |
116 | | - }, |
117 | | - ViewGroup.LayoutParams.WRAP_CONTENT, |
118 | | - ) |
119 | | - isChecked = isCurrentlySelected |
120 | | - setOnClickListener { radioButton -> |
121 | | - appContext.lifecycleScope.launch { |
122 | | - isCurrentlySelected = !isCurrentlySelected |
123 | | - when (isCurrentlySelected) { |
124 | | - true -> { |
125 | | - updateAnswer(answerOption) |
126 | | - val buttons = radioGroup.children.asIterable().filterIsInstance<RadioButton>() |
127 | | - buttons.forEach { button -> uncheckIfNotButtonId(radioButton.id, button) } |
128 | | - } |
129 | | - false -> { |
130 | | - questionnaireViewItem.clearAnswer() |
131 | | - (radioButton as RadioButton).isChecked = false |
132 | | - } |
| 76 | + @Suppress("LocalVariableName") |
| 77 | + val AnswerOptionRadioButtons: @Composable (Modifier) -> Unit = { modifier -> |
| 78 | + enabledAnswerOptions.forEach { |
| 79 | + val labelText = remember(it) { AnnotatedString(it.value.displayString(context)) } |
| 80 | + ChoiceRadioButton( |
| 81 | + label = labelText, |
| 82 | + selected = it == selectedAnswerOption, |
| 83 | + enabled = !readOnly, |
| 84 | + modifier = modifier.testTag(RADIO_OPTION_TAG), |
| 85 | + image = it.itemAnswerOptionImage(context), |
| 86 | + ) { |
| 87 | + coroutineScope.launch { |
| 88 | + if (selectedAnswerOption != it) { |
| 89 | + selectedAnswerOption = it |
| 90 | + questionnaireViewItem.setAnswer( |
| 91 | + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { |
| 92 | + value = it.value |
| 93 | + }, |
| 94 | + ) |
| 95 | + } else { |
| 96 | + selectedAnswerOption = null |
| 97 | + questionnaireViewItem.clearAnswer() |
133 | 98 | } |
134 | 99 | } |
135 | 100 | } |
136 | 101 | } |
137 | | - radioGroup.addView(radioButton) |
138 | | - flow.addView(radioButton) |
139 | | - } |
| 102 | + } |
140 | 103 |
|
141 | | - private fun uncheckIfNotButtonId(checkedId: Int, button: RadioButton) { |
142 | | - if (button.id != checkedId) button.isChecked = false |
143 | | - } |
| 104 | + Column( |
| 105 | + modifier = |
| 106 | + Modifier.fillMaxWidth() |
| 107 | + .padding( |
| 108 | + horizontal = dimensionResource(R.dimen.item_margin_horizontal), |
| 109 | + vertical = dimensionResource(R.dimen.item_margin_vertical), |
| 110 | + ), |
| 111 | + ) { |
| 112 | + Header( |
| 113 | + questionnaireViewItem, |
| 114 | + showRequiredOrOptionalText = true, |
| 115 | + displayValidationResult = true, |
| 116 | + ) |
| 117 | + questionnaireViewItem.questionnaireItem.itemMedia?.let { MediaItem(it) } |
144 | 118 |
|
145 | | - private suspend fun updateAnswer( |
146 | | - answerOption: Questionnaire.QuestionnaireItemAnswerOptionComponent, |
147 | | - ) { |
148 | | - questionnaireViewItem.setAnswer( |
149 | | - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { |
150 | | - value = answerOption.value |
151 | | - }, |
152 | | - ) |
| 119 | + when (choiceOrientationType) { |
| 120 | + ChoiceOrientationTypes.HORIZONTAL -> { |
| 121 | + FlowRow( |
| 122 | + modifier = Modifier.selectableGroup().fillMaxWidth(), |
| 123 | + horizontalArrangement = |
| 124 | + Arrangement.spacedBy(dimensionResource(R.dimen.option_item_margin_horizontal)), |
| 125 | + verticalArrangement = |
| 126 | + Arrangement.spacedBy(dimensionResource(R.dimen.option_item_margin_vertical)), |
| 127 | + ) { |
| 128 | + AnswerOptionRadioButtons(Modifier.weight(1f)) |
| 129 | + } |
| 130 | + } |
| 131 | + ChoiceOrientationTypes.VERTICAL -> { |
| 132 | + Column( |
| 133 | + modifier = Modifier.selectableGroup().fillMaxWidth(), |
| 134 | + verticalArrangement = |
| 135 | + Arrangement.spacedBy(dimensionResource(R.dimen.option_item_margin_vertical)), |
| 136 | + ) { |
| 137 | + AnswerOptionRadioButtons(Modifier.fillMaxWidth()) |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | + } |
153 | 142 | } |
154 | 143 | } |
155 | 144 | } |
| 145 | + |
| 146 | +const val RADIO_OPTION_TAG = "radio_group_option" |
0 commit comments