Skip to content

Commit 1c71e2f

Browse files
committed
Migrate SliderViewHolderFactory to compose
1 parent ec95f09 commit 1c71e2f

File tree

3 files changed

+169
-17
lines changed

3 files changed

+169
-17
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.padding
20+
import androidx.compose.material3.MaterialTheme
21+
import androidx.compose.material3.Text
22+
import androidx.compose.runtime.Composable
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.res.dimensionResource
25+
import com.google.android.fhir.datacapture.R
26+
27+
@Composable
28+
fun ErrorText(validationMessage: String) {
29+
Text(
30+
text = validationMessage,
31+
style = MaterialTheme.typography.bodySmall,
32+
color = MaterialTheme.colorScheme.error,
33+
modifier = Modifier.padding(start = dimensionResource(R.dimen.error_text_margin_horizontal)),
34+
)
35+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.Column
20+
import androidx.compose.foundation.layout.fillMaxWidth
21+
import androidx.compose.material3.Slider
22+
import androidx.compose.material3.Text
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.getValue
25+
import androidx.compose.runtime.mutableFloatStateOf
26+
import androidx.compose.runtime.remember
27+
import androidx.compose.runtime.setValue
28+
import androidx.compose.ui.Modifier
29+
import kotlin.math.roundToInt
30+
31+
@Composable
32+
fun SliderItem(
33+
startPosition: Float,
34+
steps: Int,
35+
valueRange: ClosedFloatingPointRange<Float>,
36+
enabled: Boolean,
37+
onPositionChanged: (Float) -> Unit,
38+
) {
39+
var sliderPosition by remember { mutableFloatStateOf(startPosition) }
40+
41+
Column {
42+
Text(text = sliderPosition.roundToInt().toString())
43+
44+
Slider(
45+
value = sliderPosition,
46+
onValueChange = { sliderPosition = it },
47+
onValueChangeFinished = { onPositionChanged(sliderPosition) },
48+
steps = steps,
49+
valueRange = valueRange,
50+
modifier = Modifier.fillMaxWidth(),
51+
enabled = enabled,
52+
)
53+
}
54+
}

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

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,19 @@ package com.google.android.fhir.datacapture.views.factories
1919
import android.view.View
2020
import android.widget.TextView
2121
import androidx.appcompat.app.AppCompatActivity
22+
import androidx.compose.foundation.layout.Column
23+
import androidx.compose.foundation.layout.fillMaxWidth
24+
import androidx.compose.foundation.layout.padding
25+
import androidx.compose.runtime.Composable
26+
import androidx.compose.runtime.getValue
27+
import androidx.compose.runtime.remember
28+
import androidx.compose.runtime.rememberCoroutineScope
29+
import androidx.compose.runtime.setValue
30+
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.res.dimensionResource
2232
import androidx.lifecycle.lifecycleScope
2333
import com.google.android.fhir.datacapture.R
34+
import com.google.android.fhir.datacapture.extensions.itemMedia
2435
import com.google.android.fhir.datacapture.extensions.sliderStepValue
2536
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
2637
import com.google.android.fhir.datacapture.validation.Invalid
@@ -29,31 +40,85 @@ import com.google.android.fhir.datacapture.validation.Valid
2940
import com.google.android.fhir.datacapture.validation.ValidationResult
3041
import com.google.android.fhir.datacapture.views.HeaderView
3142
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
43+
import com.google.android.fhir.datacapture.views.compose.ErrorText
44+
import com.google.android.fhir.datacapture.views.compose.Header
45+
import com.google.android.fhir.datacapture.views.compose.MediaItem
46+
import com.google.android.fhir.datacapture.views.compose.SliderItem
3247
import com.google.android.material.slider.Slider
48+
import kotlin.math.roundToInt
49+
import kotlinx.coroutines.Dispatchers
3350
import kotlinx.coroutines.launch
3451
import org.hl7.fhir.r4.model.IntegerType
3552
import org.hl7.fhir.r4.model.QuestionnaireResponse
3653
import org.hl7.fhir.r4.model.Type
3754

38-
internal object SliderViewHolderFactory :
39-
QuestionnaireItemAndroidViewHolderFactory(R.layout.slider_view) {
55+
internal object SliderViewHolderFactory : QuestionnaireItemComposeViewHolderFactory {
4056
override fun getQuestionnaireItemViewHolderDelegate() =
41-
object : QuestionnaireItemAndroidViewHolderDelegate {
57+
object : QuestionnaireItemComposeViewHolderDelegate {
4258
private lateinit var appContext: AppCompatActivity
4359
private lateinit var header: HeaderView
4460
private lateinit var slider: Slider
4561
private lateinit var error: TextView
46-
override lateinit var questionnaireViewItem: QuestionnaireViewItem
4762

48-
override fun init(itemView: View) {
63+
@Composable
64+
override fun Content(questionnaireViewItem: QuestionnaireViewItem) {
65+
val validationMessage =
66+
remember(questionnaireViewItem) {
67+
displayValidationResult(questionnaireViewItem.validationResult)
68+
}
69+
val readOnly =
70+
remember(questionnaireViewItem) { questionnaireViewItem.questionnaireItem.readOnly }
71+
val answer =
72+
remember(questionnaireViewItem) { questionnaireViewItem.answers.singleOrNull() }
73+
val minValue = remember(answer) { getMinValue(questionnaireViewItem.minAnswerValue) }
74+
val maxValue = remember(answer) { getMaxValue(questionnaireViewItem.maxAnswerValue) }
75+
76+
check(minValue < maxValue) { "minValue $minValue must be smaller than maxValue $maxValue" }
77+
val stepSize =
78+
remember(questionnaireViewItem) {
79+
questionnaireViewItem.questionnaireItem.sliderStepValue ?: SLIDER_DEFAULT_STEP_SIZE
80+
}
81+
val steps =
82+
remember(stepSize, minValue, maxValue) { (maxValue - minValue).div(stepSize).toInt() }
83+
val questionnaireViewItemAnswerValue =
84+
remember(answer) { answer?.valueIntegerType?.value?.toFloat() ?: minValue }
85+
val coroutineScope = rememberCoroutineScope { Dispatchers.Main }
86+
87+
Column(
88+
modifier =
89+
Modifier.fillMaxWidth()
90+
.padding(
91+
horizontal = dimensionResource(R.dimen.item_margin_horizontal),
92+
vertical = dimensionResource(R.dimen.item_margin_vertical),
93+
),
94+
) {
95+
Header(questionnaireViewItem, showRequiredOrOptionalText = true)
96+
questionnaireViewItem.questionnaireItem.itemMedia?.let { MediaItem(it) }
97+
SliderItem(
98+
startPosition = questionnaireViewItemAnswerValue,
99+
steps = steps,
100+
valueRange = minValue..maxValue,
101+
enabled = !readOnly,
102+
) {
103+
coroutineScope.launch {
104+
questionnaireViewItem.setAnswer(
105+
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
106+
.setValue(IntegerType(it.roundToInt())),
107+
)
108+
}
109+
}
110+
validationMessage?.let { ErrorText(it) }
111+
}
112+
}
113+
114+
fun init(itemView: View) {
49115
appContext = itemView.context.tryUnwrapContext()!!
50116
header = itemView.findViewById(R.id.header)
51117
slider = itemView.findViewById(R.id.slider)
52118
error = itemView.findViewById(R.id.error)
53119
}
54120

55-
override fun bind(questionnaireViewItem: QuestionnaireViewItem) {
56-
this.questionnaireViewItem = questionnaireViewItem
121+
fun bind(questionnaireViewItem: QuestionnaireViewItem) {
57122
header.bind(questionnaireViewItem, showRequiredOrOptionalText = true)
58123
val answer = questionnaireViewItem.answers.singleOrNull()
59124
val minValue = getMinValue(questionnaireViewItem.minAnswerValue)
@@ -82,19 +147,17 @@ internal object SliderViewHolderFactory :
82147
}
83148
}
84149

85-
displayValidationResult(questionnaireViewItem.validationResult)
150+
error.text = displayValidationResult(questionnaireViewItem.validationResult)
86151
}
87152

88-
private fun displayValidationResult(validationResult: ValidationResult) {
89-
error.text =
90-
when (validationResult) {
91-
is NotValidated,
92-
Valid, -> null
93-
is Invalid -> validationResult.getSingleStringValidationMessage()
94-
}
95-
}
153+
private fun displayValidationResult(validationResult: ValidationResult) =
154+
when (validationResult) {
155+
is NotValidated,
156+
Valid, -> null
157+
is Invalid -> validationResult.getSingleStringValidationMessage()
158+
}
96159

97-
override fun setReadOnly(isReadOnly: Boolean) {
160+
fun setReadOnly(isReadOnly: Boolean) {
98161
slider.isEnabled = !isReadOnly
99162
}
100163
}

0 commit comments

Comments
 (0)