Skip to content

Commit 6a947a1

Browse files
committed
feat(Slider): Add Custom Value distribution and parabolic example
1 parent 394a906 commit 6a947a1

File tree

4 files changed

+127
-35
lines changed

4 files changed

+127
-35
lines changed

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

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package io.monstarlab.mosaic.features
22

33
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
45
import androidx.compose.foundation.layout.Box
56
import androidx.compose.foundation.layout.Column
6-
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.Row
78
import androidx.compose.foundation.layout.padding
89
import androidx.compose.foundation.layout.size
9-
import androidx.compose.foundation.shape.RoundedCornerShape
1010
import androidx.compose.material3.Scaffold
1111
import androidx.compose.material3.Text
1212
import androidx.compose.runtime.Composable
@@ -15,23 +15,27 @@ import androidx.compose.runtime.mutableFloatStateOf
1515
import androidx.compose.runtime.remember
1616
import androidx.compose.runtime.setValue
1717
import androidx.compose.ui.Modifier
18-
import androidx.compose.ui.draw.clip
1918
import androidx.compose.ui.graphics.Color
2019
import androidx.compose.ui.tooling.preview.Preview
2120
import androidx.compose.ui.unit.dp
21+
import io.monstarlab.mosaic.slider.ParabolicValueDistribution
2222
import io.monstarlab.mosaic.slider.Slider
2323
import io.monstarlab.mosaic.slider.SliderColors
24+
import io.monstarlab.mosaic.slider.SliderValueDistribution
25+
import kotlin.math.roundToInt
26+
import kotlin.math.sqrt
2427
import androidx.compose.material3.Slider as MaterialSlider
28+
2529
@Composable
2630
fun SliderDemo() = Scaffold(modifier = Modifier) {
2731
var materialSliderValue by remember { mutableFloatStateOf(50f) }
28-
var mosaicSliderValue by remember { mutableFloatStateOf(50f) }
2932

3033
Column(
3134
modifier = Modifier
3235
.padding(it)
3336
.padding(16.dp)
3437
.background(Color.LightGray),
38+
verticalArrangement = Arrangement.spacedBy(32.dp),
3539
) {
3640
MaterialSlider(
3741
value = materialSliderValue,
@@ -40,21 +44,61 @@ fun SliderDemo() = Scaffold(modifier = Modifier) {
4044
)
4145
Text(text = materialSliderValue.toString())
4246

43-
Spacer(modifier = Modifier.size(32.dp))
47+
MosaicSliderDemo(
48+
label = "Linear division strategy",
49+
range = 0f..1000f,
50+
valuesDistribution = SliderValueDistribution.Linear,
51+
)
52+
MosaicSliderDemo(
53+
label = "Parabolic",
54+
range = 0f..1000f,
55+
valuesDistribution = SliderValueDistribution.parabolic(
56+
a = (1000 - 100 * 0.1f) / (1000 * 1000),
57+
b = 0.1f,
58+
c = 1f
59+
),
60+
)
61+
}
62+
}
63+
64+
@Composable
65+
fun MosaicSliderDemo(
66+
label: String,
67+
range: ClosedFloatingPointRange<Float>,
68+
valuesDistribution: SliderValueDistribution,
69+
modifier: Modifier = Modifier,
70+
) {
71+
Column(modifier) {
72+
Text(text = label)
4473

45-
Slider(
46-
value = mosaicSliderValue,
47-
onValueChange = { mosaicSliderValue = it },
48-
colors = SliderColors(Color.Red),
49-
modifier = Modifier.clip(RoundedCornerShape(2.dp)),
50-
range = 0f..100f,
74+
var value by remember { mutableFloatStateOf(50f) }
75+
Row(
76+
horizontalArrangement = Arrangement.Start,
77+
modifier = Modifier,
5178
) {
52-
Box(modifier = Modifier.background(Color.Yellow).size(32.dp))
79+
Text(text = range.start.toString())
80+
Slider(
81+
value = value,
82+
onValueChange = { value = it },
83+
colors = SliderColors(Color.Black),
84+
range = range,
85+
valueDistribution = valuesDistribution,
86+
thumb = {
87+
Box(
88+
modifier = Modifier
89+
.size(32.dp)
90+
.background(Color.Yellow),
91+
) {
92+
Text(text = value.roundToInt().toString())
93+
}
94+
},
95+
)
5396
}
54-
Text(text = mosaicSliderValue.toString())
97+
Text(text = range.endInclusive.toString())
5598
}
5699
}
57100

101+
58102
@Preview
59103
@Composable
60104
private fun PreviewSliderDemo() {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ public fun Slider(
2121
onValueChange: (Float) -> Unit,
2222
colors: SliderColors,
2323
modifier: Modifier = Modifier,
24+
valueDistribution: SliderValueDistribution = SliderValueDistribution.Linear,
2425
range: ClosedFloatingPointRange<Float> = 0f..1f,
2526
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
2627
thumb: @Composable () -> Unit = { DefaultSliderThumb(colors = colors) },
2728
) {
28-
val state = rememberSliderState()
29+
val state = rememberSliderState(value, valueDistribution)
2930
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
3031

3132
state.value = value

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

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,58 @@ import androidx.compose.foundation.gestures.DragScope
66
import androidx.compose.foundation.gestures.DraggableState
77
import androidx.compose.runtime.Composable
88
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableFloatStateOf
910
import androidx.compose.runtime.mutableStateOf
1011
import androidx.compose.runtime.remember
1112
import androidx.compose.runtime.setValue
13+
import kotlinx.coroutines.coroutineScope
1214

1315
public class SliderState(
16+
value: Float,
1417
private val valueDistribution: SliderValueDistribution,
1518
) : DraggableState {
1619

17-
private var isDragging by mutableStateOf(false)
18-
private var rawOffset by mutableStateOf(0f)
20+
internal var range: ClosedFloatingPointRange<Float> = 0f..1f
21+
internal var onValueChange: ((Float) -> Unit)? = null
22+
internal var isDragging by mutableStateOf(false)
23+
private set
24+
25+
private var totalWidth by mutableFloatStateOf(0f)
26+
private var thumbWidth by mutableFloatStateOf(0f)
27+
28+
private var rawOffset by mutableFloatStateOf(scaleToOffset(value))
1929
private val scrollMutex = MutatorMutex()
20-
private var totalWidth = 0f
21-
private var thumbWidth = 0f
2230

23-
internal var onValueChange: (Float) -> Unit = {}
24-
internal var range: ClosedFloatingPointRange<Float> = 0f..1f
31+
internal val valueAsFraction: Float get() {
32+
return calcFraction(0f, totalWidth, rawOffset)
33+
}
2534

2635
public var value: Float
36+
get() = scaleToUserValue(rawOffset)
2737
set(value) {
2838
rawOffset = scaleToOffset(value)
2939
}
30-
get() {
31-
return valueAsFraction
32-
}
33-
34-
internal val valueAsFraction: Float get() {
35-
return calcFraction(0f, totalWidth, rawOffset)
36-
}
3740

3841
private val dragScope: DragScope = object : DragScope {
3942
override fun dragBy(pixels: Float): Unit = dispatchRawDelta(pixels)
4043
}
4144

4245
override fun dispatchRawDelta(delta: Float) {
43-
rawOffset += delta
44-
onValueChange(scaleToUserValue(rawOffset))
46+
val newRawOffset = rawOffset + delta
47+
val userValue = scaleToUserValue(newRawOffset)
48+
if (userValue != value) {
49+
if (onValueChange != null) {
50+
onValueChange?.invoke(userValue)
51+
} else {
52+
rawOffset = newRawOffset
53+
}
54+
}
4555
}
4656

47-
override suspend fun drag(dragPriority: MutatePriority, block: suspend DragScope.() -> Unit) {
57+
override suspend fun drag(
58+
dragPriority: MutatePriority,
59+
block: suspend DragScope.() -> Unit,
60+
): Unit = coroutineScope {
4861
isDragging = true
4962
scrollMutex.mutateWith(dragScope, dragPriority, block)
5063
isDragging = false
@@ -55,16 +68,25 @@ public class SliderState(
5568
this.thumbWidth = thumbWidth
5669
}
5770

58-
private fun scaleToUserValue(value: Float): Float {
59-
return scale(0f, totalWidth, value, range.start, range.endInclusive)
71+
private fun scaleToUserValue(offset: Float): Float {
72+
val rangeStart = valueDistribution.interpolate(range.start)
73+
val rangeEnd = valueDistribution.interpolate(range.endInclusive)
74+
val scaledUserValue = scale(0f, totalWidth, offset, rangeStart, rangeEnd)
75+
return valueDistribution.inverse(scaledUserValue)
6076
}
6177

6278
private fun scaleToOffset(value: Float): Float {
63-
return scale(range.start, range.endInclusive, value, 0f, totalWidth)
79+
val rangeStart = valueDistribution.interpolate(range.start)
80+
val rangeEnd = valueDistribution.interpolate(range.endInclusive)
81+
val interpolated = valueDistribution.interpolate(value)
82+
return scale(rangeStart, rangeEnd, interpolated, 0f, totalWidth)
6483
}
6584
}
6685

6786
@Composable
68-
public fun rememberSliderState(): SliderState {
69-
return remember { SliderState(SliderValueDistribution.Linear) }
87+
public fun rememberSliderState(
88+
value: Float,
89+
valueDistribution: SliderValueDistribution = SliderValueDistribution.Linear,
90+
): SliderState {
91+
return remember { SliderState(value, valueDistribution) }
7092
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package io.monstarlab.mosaic.slider
22

3+
import kotlin.math.sqrt
4+
35
public interface SliderValueDistribution {
46

57
public fun interpolate(value: Float): Float
68

79
public fun inverse(value: Float): Float
810

911
public companion object {
12+
13+
public fun parabolic(a: Float, b: Float, c: Float): SliderValueDistribution {
14+
return ParabolicValueDistribution(a, b, c)
15+
}
16+
1017
public val Linear: SliderValueDistribution = object : SliderValueDistribution {
1118
override fun interpolate(value: Float): Float {
1219
return value
@@ -18,3 +25,21 @@ public interface SliderValueDistribution {
1825
}
1926
}
2027
}
28+
29+
public class ParabolicValueDistribution(
30+
private val a: Float,
31+
private val b: Float,
32+
private val c: Float
33+
) : SliderValueDistribution {
34+
35+
override fun inverse(value: Float): Float {
36+
return a * (value * value) + b * value + c
37+
}
38+
39+
override fun interpolate(value: Float): Float {
40+
if (value == 0f) return 0f
41+
42+
val d = (b * b) - 4 * a * (c - value)
43+
return (-b + sqrt(d)) / (2 * a)
44+
}
45+
}

0 commit comments

Comments
 (0)