Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import kotlin.math.roundToInt
object OffsetUtils {
fun calculateOffset(
trackSize: Int,
itemWidth: Float,
itemSize: Float,
value: Float,
isVertical: Boolean
): IntOffset {
val fraction = if (isVertical) 1 - value else value
val offset = (trackSize * fraction - itemWidth * fraction).roundToInt()
val offset = (trackSize * fraction - itemSize * fraction).roundToInt()
return if (isVertical) IntOffset(0, offset) else IntOffset(offset, 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class OffsetUtilsTest {
fun `calculateOffset returns zero offset when horizontal and value is 0`() {
val result = OffsetUtils.calculateOffset(
trackSize = 1000,
itemWidth = 100f,
itemSize = 100f,
value = 0f,
isVertical = false
)
Expand All @@ -21,7 +21,7 @@ class OffsetUtilsTest {
fun `calculateOffset returns max offset when horizontal and value is 1`() {
val result = OffsetUtils.calculateOffset(
trackSize = 1000,
itemWidth = 100f,
itemSize = 100f,
value = 1f,
isVertical = false
)
Expand All @@ -32,7 +32,7 @@ class OffsetUtilsTest {
fun `calculateOffset returns middle offset when horizontal and value is 0,5`() {
val result = OffsetUtils.calculateOffset(
trackSize = 1000,
itemWidth = 100f,
itemSize = 100f,
value = 0.5f,
isVertical = false
)
Expand All @@ -43,7 +43,7 @@ class OffsetUtilsTest {
fun `calculateOffset returns max offset when vertical and value is 0`() {
val result = OffsetUtils.calculateOffset(
trackSize = 1000,
itemWidth = 100f,
itemSize = 100f,
value = 0f,
isVertical = true
)
Expand All @@ -54,7 +54,7 @@ class OffsetUtilsTest {
fun `calculateOffset returns zero offset when vertical and value is 1`() {
val result = OffsetUtils.calculateOffset(
trackSize = 1000,
itemWidth = 100f,
itemSize = 100f,
value = 1f,
isVertical = true
)
Expand All @@ -65,7 +65,7 @@ class OffsetUtilsTest {
fun `calculateOffset returns middle offset when vertical and value is 0,5`() {
val result = OffsetUtils.calculateOffset(
trackSize = 1000,
itemWidth = 100f,
itemSize = 100f,
value = 0.5f,
isVertical = true
)
Expand All @@ -76,7 +76,7 @@ class OffsetUtilsTest {
fun `calculateOffset returns zero offset when itemWidth equals trackSize`() {
val result = OffsetUtils.calculateOffset(
trackSize = 100,
itemWidth = 100f,
itemSize = 100f,
value = 0.5f,
isVertical = false
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
package org.odk.collect.android.widgets.range

import android.view.MotionEvent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import org.odk.collect.androidshared.ui.OffsetUtils.calculateOffset
import kotlin.math.roundToInt
import androidx.compose.ui.tooling.preview.Preview

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HorizontalRangeSlider(
value: Float?,
Expand All @@ -40,112 +22,43 @@ fun HorizontalRangeSlider(
onValueChange: (Float) -> Unit,
onValueChangeFinished: () -> Unit
) {
val sliderContentDescription = stringResource(org.odk.collect.strings.R.string.horizontal_slider)

Column(horizontalAlignment = Alignment.CenterHorizontally) {
ValueLabel(valueLabel)

BoxWithConstraints {
Slider(
modifier = Modifier
.fillMaxWidth()
.semantics { contentDescription = sliderContentDescription }
.pointerInteropFilter { event ->
if (enabled && event.action == MotionEvent.ACTION_DOWN) {
onValueChanging(true)
if (value == null) {
onValueChange(0f)
}
}
false
},
value = value ?: 0f,
steps = steps,
onValueChange = onValueChange,
onValueChangeFinished = {
onValueChanging(false)
onValueChangeFinished()
},
thumb = {},
track = { Track(it, ticks) },
enabled = enabled
)

val thumbValue = value ?: placeholder
if (thumbValue != null) {
Box(
modifier = Modifier
.offset {
calculateOffset(
trackSize = constraints.maxWidth,
itemWidth = THUMB_WIDTH.dp.toPx(),
value = thumbValue,
isVertical = false
)
}
.pointerInteropFilter { false }
.align(Alignment.CenterStart)
) { Thumb(value = thumbValue) }
}
}
RangeSliderTrack(
orientation = Orientation.Horizontal,
value = value,
placeholder = placeholder,
ticks = ticks,
steps = steps,
enabled = enabled,
onValueChanging = onValueChanging,
onValueChange = onValueChange,
onValueChangeFinished = onValueChangeFinished
)

HorizontalEdgeLabels(startLabel, endLabel)
HorizontalStepLabels(labels)
RangeSliderEdgeLabels(Orientation.Horizontal, startLabel, endLabel)
RangeSliderStepLabels(Orientation.Horizontal, labels)
}
}

@Preview
@Composable
private fun HorizontalEdgeLabels(labelStart: String, labelEnd: String) {
val sliderStartLabelContentDescription = stringResource(org.odk.collect.strings.R.string.slider_start_label)
val sliderEndLabelContentDescription = stringResource(org.odk.collect.strings.R.string.slider_end_label)

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Label(
modifier = Modifier.semantics {
contentDescription = sliderStartLabelContentDescription
},
text = labelStart,
)
Label(
modifier = Modifier.semantics {
contentDescription = sliderEndLabelContentDescription
},
text = labelEnd,
private fun HorizontalRangeSliderPreview() {
Surface {
HorizontalRangeSlider(
value = 0.5f,
valueLabel = "5",
placeholder = null,
steps = 9,
ticks = 11,
enabled = true,
startLabel = "0",
endLabel = "10",
labels = listOf("very bad", "very good"),
onValueChanging = {},
onValueChange = {},
onValueChangeFinished = {}
)
}
}

@Composable
private fun HorizontalStepLabels(labels: List<String>) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val totalSteps = labels.size - 1
val labelWidth = maxWidth / 5 // Each label takes up a fifth of the track width. Confirmed to look good in most cases.

labels.forEachIndexed { index, label ->
if (label.isBlank()) return@forEachIndexed

val modifier = when (index) {
0 -> Modifier.align(Alignment.TopStart)
totalSteps -> Modifier.align(Alignment.TopEnd)
else -> Modifier.offset {
val fraction = index.toFloat() / totalSteps
val centerX = (constraints.maxWidth * fraction).roundToInt()
IntOffset(centerX - labelWidth.roundToPx() / 2, 0)
}
}

Label(
modifier = modifier.width(labelWidth),
text = label,
textAlign = when (index) {
0 -> TextAlign.Start
totalSteps -> TextAlign.End
else -> TextAlign.Center
}
)
}
}
}
Loading