Skip to content

Commit 5b96b5d

Browse files
committed
feat: Add disabled state to the Slider, also refactor the state management
1 parent 1379798 commit 5b96b5d

File tree

6 files changed

+169
-41
lines changed

6 files changed

+169
-41
lines changed

demo/src/main/java/io/monstarlab/mosaic/features/SliderDemo.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
77
import androidx.compose.foundation.layout.Row
88
import androidx.compose.foundation.layout.padding
99
import androidx.compose.foundation.layout.size
10+
import androidx.compose.foundation.shape.CircleShape
1011
import androidx.compose.material3.Scaffold
1112
import androidx.compose.material3.Text
1213
import androidx.compose.runtime.Composable
@@ -61,14 +62,32 @@ fun SliderDemo() = Scaffold(modifier = Modifier) {
6162

6263
Slider(
6364
state = rememberSliderState(value = 0.5f),
64-
colors = SliderColors(Color.Yellow),
65+
colors = SliderColors(Color.Magenta, Color.Red),
6566
) {
6667
Box(
6768
modifier = Modifier
6869
.size(32.dp)
6970
.background(Color.Black),
7071
)
7172
}
73+
74+
Slider(
75+
state = rememberSliderState(value = 0.5f),
76+
colors = SliderColors(Color.Black),
77+
enabled = true,
78+
) {
79+
Box(modifier = Modifier.background(Color.Red, shape = CircleShape).size(32.dp))
80+
}
81+
82+
var v by remember { mutableFloatStateOf(0.5f) }
83+
Slider(
84+
value = v,
85+
onValueChange = { v = it },
86+
colors = SliderColors(Color.Black),
87+
enabled = false,
88+
) {
89+
Box(modifier = Modifier.background(Color.Red, shape = CircleShape).size(32.dp))
90+
}
7291
}
7392
}
7493

@@ -96,7 +115,7 @@ fun MosaicSliderDemo(
96115
modifier = Modifier.weight(0.8f),
97116
value = value,
98117
onValueChange = { value = it },
99-
colors = SliderColors(Color.Black, disabled = Color.Red),
118+
colors = SliderColors(Color.Black, disabledRangeTrackColor = Color.Red),
100119
range = range,
101120
valueDistribution = valuesDistribution,
102121
disabledRange = disabledRange,

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import androidx.compose.ui.semantics.setProgress
1313

1414
internal fun Modifier.sliderDragModifier(
1515
state: SliderState,
16+
enabled: Boolean,
1617
interactionSource: MutableInteractionSource,
1718
isRtl: Boolean,
1819
): Modifier = this.draggable(
1920
state = state,
2021
orientation = Orientation.Horizontal,
21-
enabled = true,
22+
enabled = enabled,
2223
interactionSource = interactionSource,
2324
startDragImmediately = state.isDragging,
2425
reverseDirection = isRtl,
@@ -27,12 +28,18 @@ internal fun Modifier.sliderDragModifier(
2728

2829
internal fun Modifier.sliderTapModifier(
2930
state: SliderState,
31+
enabled: Boolean,
3032
interactionSource: MutableInteractionSource,
3133
): Modifier {
32-
return this.pointerInput(state, interactionSource) {
33-
detectTapGestures(
34-
onPress = { state.handlePress(it) },
35-
)
34+
return if (enabled) {
35+
this.pointerInput(state, interactionSource) {
36+
detectTapGestures(
37+
onPress = { state.handlePress(it) },
38+
onTap = { state.dispatchRawDelta(0f) },
39+
)
40+
}
41+
} else {
42+
this
3643
}
3744
}
3845

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

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.Box
77
import androidx.compose.foundation.layout.size
88
import androidx.compose.foundation.shape.CircleShape
99
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.LaunchedEffect
11+
import androidx.compose.runtime.mutableFloatStateOf
1012
import androidx.compose.runtime.remember
1113
import androidx.compose.ui.Modifier
1214
import androidx.compose.ui.graphics.Color
@@ -19,6 +21,7 @@ import androidx.compose.ui.unit.LayoutDirection
1921
*
2022
* @param value the current value of the slider
2123
* @param onValueChange a callback function invoked when the slider value changes
24+
* @param enabled - determines whether the user can interact with the slide or not
2225
* @param colors the colors used to customize the appearance of the slider
2326
* @param modifier the modifier to be applied to the slider
2427
* @param valueDistribution the strategy for distributing slider values
@@ -32,31 +35,48 @@ public fun Slider(
3235
onValueChange: (Float) -> Unit,
3336
colors: SliderColors,
3437
modifier: Modifier = Modifier,
38+
enabled: Boolean = true,
3539
valueDistribution: SliderValueDistribution = SliderValueDistribution.Linear,
3640
range: ClosedFloatingPointRange<Float> = 0f..1f,
3741
disabledRange: ClosedFloatingPointRange<Float> = EmptyRange,
3842
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
39-
thumb: @Composable (SliderState) -> Unit = { DefaultSliderThumb(colors = colors) },
43+
thumb: @Composable (
44+
SliderState,
45+
) -> Unit = { DefaultSliderThumb(colors = colors, enabled = enabled) },
4046
) {
4147
val state = rememberSliderState(value, range, valueDistribution, disabledRange)
42-
4348
state.onValueChange = onValueChange
4449
state.value = value
4550

51+
// Workaround for the initial value that might belong into disabled range
52+
// The initial value is remembered
53+
val initialValue = remember { mutableFloatStateOf(value) }
54+
55+
// In case initial value differs from the value inside the state
56+
// this means the value has been coerced into it
57+
// for this case the change of value must be explicitly notified to the receiver
58+
LaunchedEffect(initialValue) {
59+
if (initialValue.value != state.value) {
60+
onValueChange(state.value)
61+
}
62+
}
63+
4664
Slider(
4765
state = state,
4866
interactionSource = interactionSource,
4967
modifier = modifier,
5068
thumb = thumb,
5169
colors = colors,
70+
enabled = enabled,
5271
)
5372
}
5473

5574
/**
5675
* A composable function that creates a slider UI component.
5776
* @param state of the Slider where the latest slider value is stored
5877
* @param colors the colors used to customize the appearance of the slider
59-
* @param modifier the modifier to be applied to the slider
78+
* @param modifier the modifier to be applied to the slider,
79+
* @param enabled - determines whether the user can interact with the slide or not
6080
* @param interactionSource the interaction source used to handle user input interactions
6181
* @param thumb the composable function used to render the slider thumb
6282
*/
@@ -65,17 +85,18 @@ public fun Slider(
6585
state: SliderState,
6686
colors: SliderColors,
6787
modifier: Modifier = Modifier,
88+
enabled: Boolean = true,
6889
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
6990
thumb: @Composable (SliderState) -> Unit,
7091
) {
7192
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
72-
val tap = Modifier.sliderTapModifier(state, interactionSource)
73-
val drag = Modifier.sliderDragModifier(state, interactionSource, isRtl)
93+
val tap = Modifier.sliderTapModifier(state, enabled, interactionSource)
94+
val drag = Modifier.sliderDragModifier(state, enabled, interactionSource, isRtl)
7495

7596
SliderLayout(
7697
modifier = modifier
77-
.sliderSemantics(state, true)
78-
.focusable(true, interactionSource)
98+
.sliderSemantics(state, enabled)
99+
.focusable(enabled, interactionSource)
79100
.then(tap)
80101
.then(drag),
81102
thumb = thumb,
@@ -84,19 +105,20 @@ public fun Slider(
84105
progress = state.valueAsFraction,
85106
colors = colors,
86107
disabledRange = state.disabledRangeAsFractions,
108+
enabled = enabled,
87109
)
88110
},
89111
state = state,
90112
)
91113
}
92114

93115
@Composable
94-
internal fun DefaultSliderThumb(colors: SliderColors) {
116+
internal fun DefaultSliderThumb(enabled: Boolean, colors: SliderColors) {
95117
Box(
96118
modifier = Modifier
97119
.size(SliderDefaults.ThumbSize)
98120
.background(
99-
color = colors.active,
121+
color = colors.thumbColor(enabled),
100122
shape = CircleShape,
101123
),
102124
)
@@ -110,7 +132,18 @@ private fun PreviewSlider() {
110132
Slider(
111133
value = 0.5f,
112134
onValueChange = {},
113-
colors = SliderColors(Color.Yellow),
135+
colors = SliderColors(Color.Yellow, Color.Red),
136+
disabledRange = 0.8f..1f,
137+
)
138+
}
139+
140+
@Preview
141+
@Composable
142+
private fun PreviewDisabledSlider() {
143+
Slider(
144+
value = 0.5f,
145+
onValueChange = {},
146+
colors = SliderColors(Color.Yellow, Color.Red),
114147
disabledRange = 0.8f..1f,
115148
)
116149
}
Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,43 @@
11
package io.monstarlab.mosaic.slider
22

3+
import androidx.compose.runtime.Immutable
4+
import androidx.compose.runtime.Stable
35
import androidx.compose.ui.graphics.Color
46

5-
public data class SliderColors(
6-
val active: Color,
7-
val disabled: Color = active.copy(alpha = 0.2f),
8-
val inactive: Color = active.copy(alpha = 0.5f),
9-
)
7+
@Immutable
8+
public class SliderColors(
9+
public val activeTrackColor: Color,
10+
public val disabledRangeTrackColor: Color = activeTrackColor,
11+
public val inactiveTrackColor: Color = activeTrackColor.copy(alpha = 0.5f),
12+
public val disabledActiveTrackColor: Color = activeTrackColor.copy(alpha = 0.2f),
13+
public val disabledInactiveTrackColor: Color = activeTrackColor.copy(alpha = 0.2f),
14+
public val thumbColor: Color = activeTrackColor,
15+
public val disabledThumbColor: Color = disabledRangeTrackColor,
16+
) {
17+
@Stable
18+
public fun activeTrackColor(enabled: Boolean): Color {
19+
return if (enabled) {
20+
activeTrackColor
21+
} else {
22+
disabledActiveTrackColor
23+
}
24+
}
25+
26+
@Stable
27+
public fun thumbColor(enabled: Boolean): Color {
28+
return if (enabled) {
29+
thumbColor
30+
} else {
31+
disabledThumbColor
32+
}
33+
}
34+
35+
@Stable
36+
public fun inactiveTrackColor(enabled: Boolean): Color {
37+
return if (enabled) {
38+
inactiveTrackColor
39+
} else {
40+
disabledInactiveTrackColor
41+
}
42+
}
43+
}

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

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,39 +18,60 @@ import kotlinx.coroutines.coroutineScope
1818
* Responsible for managing internal properties such as offset value and drag / click behaviours
1919
*/
2020
public class SliderState(
21-
value: Float,
21+
initialValue: Float,
2222
public val range: ClosedFloatingPointRange<Float>,
2323
private val disabledRange: ClosedFloatingPointRange<Float>,
2424
private val valueDistribution: SliderValueDistribution,
2525
) : DraggableState {
2626

27+
/**
28+
* Optional callback for notifying the change of value
29+
* In case provided, the "value" state is stored outside in another component
30+
* And it is the responsibility of a client to manage the value state
31+
*/
2732
internal var onValueChange: ((Float) -> Unit)? = null
2833
internal var isDragging by mutableStateOf(false)
2934
private set
3035
private var totalWidth by mutableFloatStateOf(0f)
3136
private var thumbWidth by mutableFloatStateOf(0f)
3237

33-
private var rawOffset by mutableFloatStateOf(scaleToOffset(value))
34-
private val scrollMutex = MutatorMutex()
35-
36-
internal val valueAsFraction: Float
37-
get() {
38-
return calcFraction(0f, totalWidth, rawOffset)
39-
}
38+
/**
39+
* Internal state to hold the User Value (coerced and in range)
40+
* Serves as source of truth for the client
41+
*/
42+
private var valueState by mutableFloatStateOf(initialValue)
4043

41-
internal val disabledRangeAsFractions: ClosedFloatingPointRange<Float>
42-
get() = coerceRangeIntoFractions(disabledRange)
44+
/**
45+
* Internally stored offset, used simply for the drag & click gestures
46+
*/
47+
private var rawOffset by mutableFloatStateOf(scaleToOffset(initialValue))
48+
private val scrollMutex = MutatorMutex()
4349

4450
/**
4551
* Current value of the slider
4652
* If value of the slider is out of the [range] it will be coerced into it
53+
* If the value of the slider is inside the [disabledRange] It will be coerced int closes available range that is not disabled
54+
*
4755
*/
4856
public var value: Float
49-
get() = scaleToUserValue(rawOffset)
57+
get() = valueState.coerceIntoDisabledRange()
5058
set(value) {
51-
rawOffset = scaleToOffset(value)
59+
valueState = value.coerceIntoDisabledRange()
60+
}
61+
62+
/**
63+
* Internal value returned as fraction, used for displaying and specifying
64+
* the "real" position of the thumb
65+
*/
66+
internal val valueAsFraction: Float
67+
get() {
68+
val interpolated = valueDistribution.interpolate(value)
69+
return calcFraction(range.start, range.endInclusive, interpolated)
5270
}
5371

72+
internal val disabledRangeAsFractions: ClosedFloatingPointRange<Float>
73+
get() = coerceRangeIntoFractions(disabledRange)
74+
5475
private val dragScope: DragScope = object : DragScope {
5576
override fun dragBy(pixels: Float): Unit = dispatchRawDelta(pixels)
5677
}
@@ -81,10 +102,11 @@ public class SliderState(
81102
}
82103

83104
private fun handleValueUpdate(value: Float, offset: Float) {
105+
rawOffset = offset
84106
if (onValueChange != null) {
85107
onValueChange?.invoke(value)
86108
} else {
87-
rawOffset = offset
109+
valueState = value
88110
}
89111
}
90112

@@ -150,7 +172,7 @@ public fun rememberSliderState(
150172
): SliderState {
151173
return remember(range, valueDistribution, disabledRange) {
152174
SliderState(
153-
value = value,
175+
initialValue = value,
154176
range = range,
155177
disabledRange = disabledRange,
156178
valueDistribution = valueDistribution,

0 commit comments

Comments
 (0)