Skip to content

Commit 1379798

Browse files
committed
feat(Slider): Add slider semantics modifier for accesability
1 parent 95f3c0f commit 1379798

File tree

3 files changed

+91
-37
lines changed

3 files changed

+91
-37
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.monstarlab.mosaic.slider
2+
3+
import androidx.compose.foundation.gestures.Orientation
4+
import androidx.compose.foundation.gestures.detectTapGestures
5+
import androidx.compose.foundation.gestures.draggable
6+
import androidx.compose.foundation.interaction.MutableInteractionSource
7+
import androidx.compose.foundation.progressSemantics
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.input.pointer.pointerInput
10+
import androidx.compose.ui.semantics.disabled
11+
import androidx.compose.ui.semantics.semantics
12+
import androidx.compose.ui.semantics.setProgress
13+
14+
internal fun Modifier.sliderDragModifier(
15+
state: SliderState,
16+
interactionSource: MutableInteractionSource,
17+
isRtl: Boolean,
18+
): Modifier = this.draggable(
19+
state = state,
20+
orientation = Orientation.Horizontal,
21+
enabled = true,
22+
interactionSource = interactionSource,
23+
startDragImmediately = state.isDragging,
24+
reverseDirection = isRtl,
25+
onDragStopped = {},
26+
)
27+
28+
internal fun Modifier.sliderTapModifier(
29+
state: SliderState,
30+
interactionSource: MutableInteractionSource,
31+
): Modifier {
32+
return this.pointerInput(state, interactionSource) {
33+
detectTapGestures(
34+
onPress = { state.handlePress(it) },
35+
)
36+
}
37+
}
38+
39+
internal fun Modifier.sliderSemantics(state: SliderState, enabled: Boolean): Modifier {
40+
return semantics {
41+
if (!enabled) disabled()
42+
setProgress(
43+
action = { targetValue ->
44+
val coercedValue = state.coerceValue(targetValue)
45+
// This is to keep it consistent with AbsSeekbar.java: return false if no
46+
// change from current.
47+
if (coercedValue == state.value) {
48+
false
49+
} else {
50+
if (coercedValue != state.value) {
51+
if (state.onValueChange != null) {
52+
state.onValueChange?.let {
53+
it(coercedValue)
54+
}
55+
} else {
56+
state.value = coercedValue
57+
}
58+
}
59+
true
60+
}
61+
},
62+
)
63+
}.progressSemantics(
64+
value = state.value,
65+
valueRange = state.range.start..state.range.endInclusive,
66+
)
67+
}

slider/src/main/java/io/monstarlab/mosaic/slider/Slider.kt

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ package io.monstarlab.mosaic.slider
22

33
import androidx.compose.foundation.background
44
import androidx.compose.foundation.focusable
5-
import androidx.compose.foundation.gestures.Orientation
6-
import androidx.compose.foundation.gestures.detectTapGestures
7-
import androidx.compose.foundation.gestures.draggable
85
import androidx.compose.foundation.interaction.MutableInteractionSource
96
import androidx.compose.foundation.layout.Box
107
import androidx.compose.foundation.layout.size
@@ -13,7 +10,6 @@ import androidx.compose.runtime.Composable
1310
import androidx.compose.runtime.remember
1411
import androidx.compose.ui.Modifier
1512
import androidx.compose.ui.graphics.Color
16-
import androidx.compose.ui.input.pointer.pointerInput
1713
import androidx.compose.ui.platform.LocalLayoutDirection
1814
import androidx.compose.ui.tooling.preview.Preview
1915
import androidx.compose.ui.unit.LayoutDirection
@@ -73,25 +69,12 @@ public fun Slider(
7369
thumb: @Composable (SliderState) -> Unit,
7470
) {
7571
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
76-
77-
val tap = Modifier.pointerInput(state, interactionSource) {
78-
detectTapGestures(
79-
onPress = { state.handlePress(it) },
80-
)
81-
}
82-
83-
val drag = Modifier.draggable(
84-
state = state,
85-
orientation = Orientation.Horizontal,
86-
enabled = true,
87-
interactionSource = interactionSource,
88-
startDragImmediately = state.isDragging,
89-
reverseDirection = isRtl,
90-
onDragStopped = {},
91-
)
72+
val tap = Modifier.sliderTapModifier(state, interactionSource)
73+
val drag = Modifier.sliderDragModifier(state, interactionSource, isRtl)
9274

9375
SliderLayout(
9476
modifier = modifier
77+
.sliderSemantics(state, true)
9578
.focusable(true, interactionSource)
9679
.then(tap)
9780
.then(drag),

slider/src/main/java/io/monstarlab/mosaic/slider/SliderState.kt

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import kotlinx.coroutines.coroutineScope
1919
*/
2020
public class SliderState(
2121
value: Float,
22-
private val range: ClosedFloatingPointRange<Float>,
22+
public val range: ClosedFloatingPointRange<Float>,
2323
private val disabledRange: ClosedFloatingPointRange<Float>,
2424
private val valueDistribution: SliderValueDistribution,
2525
) : DraggableState {
@@ -39,7 +39,7 @@ public class SliderState(
3939
}
4040

4141
internal val disabledRangeAsFractions: ClosedFloatingPointRange<Float>
42-
get() = coerceRange(disabledRange)
42+
get() = coerceRangeIntoFractions(disabledRange)
4343

4444
/**
4545
* Current value of the slider
@@ -88,7 +88,13 @@ public class SliderState(
8888
}
8989
}
9090

91-
private fun coerceRange(
91+
internal fun coerceValue(value: Float): Float {
92+
return value
93+
.coerceIn(range)
94+
.coerceIntoDisabledRange()
95+
}
96+
97+
private fun coerceRangeIntoFractions(
9298
subrange: ClosedFloatingPointRange<Float>,
9399
): ClosedFloatingPointRange<Float> {
94100
if (subrange.isEmpty()) return subrange
@@ -108,23 +114,11 @@ public class SliderState(
108114
private fun scaleToUserValue(offset: Float): Float {
109115
val range = valueDistribution.interpolate(range)
110116
val scaledUserValue = scale(0f, totalWidth, offset, range.start, range.endInclusive)
111-
return valueDistribution.inverse(scaledUserValue)
112-
.coerceIn(this.range)
113-
.coerceIntoDisabledRange()
114-
}
115-
116-
private fun Float.coerceIntoDisabledRange(): Float {
117-
if (disabledRange.isEmpty()) return this
118-
// check if disabled range is on the left or right
119-
return if (disabledRange.start == range.start) {
120-
coerceAtLeast(disabledRange.endInclusive)
121-
} else {
122-
coerceAtMost(disabledRange.start)
123-
}
117+
return coerceValue(valueDistribution.inverse(scaledUserValue))
124118
}
125119

126120
private fun scaleToOffset(value: Float): Float {
127-
val coerced = value.coerceIn(range).coerceIntoDisabledRange()
121+
val coerced = coerceValue(value)
128122
val interpolatedRange = valueDistribution.interpolate(range)
129123
val interpolated = valueDistribution.interpolate(coerced)
130124
return scale(
@@ -135,6 +129,16 @@ public class SliderState(
135129
totalWidth,
136130
)
137131
}
132+
133+
private fun Float.coerceIntoDisabledRange(): Float {
134+
if (disabledRange.isEmpty()) return this
135+
// check if disabled range is on the left or right
136+
return if (disabledRange.start == range.start) {
137+
coerceAtLeast(disabledRange.endInclusive)
138+
} else {
139+
coerceAtMost(disabledRange.start)
140+
}
141+
}
138142
}
139143

140144
@Composable

0 commit comments

Comments
 (0)