Skip to content

Commit ec914e2

Browse files
authored
feat: slider distribution adjustments (#22)
1 parent a5bb1ca commit ec914e2

File tree

9 files changed

+106
-73
lines changed

9 files changed

+106
-73
lines changed

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import androidx.compose.ui.tooling.preview.Preview
3939
import androidx.compose.ui.unit.dp
4040
import io.monstarlab.mosaic.slider.Slider
4141
import io.monstarlab.mosaic.slider.SliderColors
42-
import io.monstarlab.mosaic.slider.distribution.CheckPointsValuesDistribution
4342
import io.monstarlab.mosaic.slider.distribution.SliderValuesDistribution
4443
import kotlin.math.roundToInt
4544
import androidx.compose.material3.Slider as MaterialSlider
@@ -84,13 +83,11 @@ fun MosaicSliderDemo() {
8483
}
8584

8685
val fragmentedDistribution: SliderValuesDistribution = remember {
87-
CheckPointsValuesDistribution(
88-
listOf(
89-
0f to 0f,
90-
0.2f to 500f,
91-
0.4f to 800f,
92-
1f to 1000f,
93-
),
86+
SliderValuesDistribution.checkpoints(
87+
0f to 0f,
88+
0.2f to 500f,
89+
0.4f to 800f,
90+
1f to 1000f,
9491
)
9592
}
9693

docs/slider.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ This allows you to control how the user interacts with the slider, the specific
142142

143143
![Distributions](./assets/example_distribution.gif)
144144

145-
### Linear Values Distribution
145+
#### Linear Values Distribution
146146
By default, Mosaic Slider will use `SliderValuesDistribution.Linear` which would arrange values in a linear fashion just like any other Slider
147147

148148

149-
### Parabolic Values Distribution
149+
#### Parabolic Values Distribution
150150
Parabolic Values Distribution allows you to arrange your values in parabolic fashion. For this, you would have to provide your `a`,`b` and `c` values for the `axˆ2 + bx+ c` equation.
151151

152152
!!! note
@@ -156,7 +156,7 @@ Parabolic Values Distribution allows you to arrange your values in parabolic fas
156156
val myDistribution = SliderValuesDistribution.parbolic(a, b, c)
157157
```
158158

159-
### Checkpoints Values Distribution
159+
#### Checkpoints Values Distribution
160160

161161
`CheckpointValuesDistribution` provides a more convinient way to customize distribution. It is based on the list of "checkoints" where each one of them is placed along the SliderTrack and comes with specific values.
162162

@@ -172,7 +172,7 @@ val distribution = SliderValuesDistribution.checkpoints(
172172
)
173173
```
174174

175-
### Make your own distribution
175+
#### Make your own distribution
176176
`SliderValuesDistribution` is a simple interface you can extend and build your own distribution.
177177

178178
```kotlin

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

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import androidx.compose.runtime.remember
1212
import androidx.compose.runtime.setValue
1313
import androidx.compose.ui.geometry.Offset
1414
import io.monstarlab.mosaic.slider.distribution.SliderValuesDistribution
15-
import io.monstarlab.mosaic.slider.math.fractionToValue
15+
import io.monstarlab.mosaic.slider.distribution.inverse
16+
import io.monstarlab.mosaic.slider.math.calcFraction
17+
import io.monstarlab.mosaic.slider.math.scale
1618
import io.monstarlab.mosaic.slider.math.valueToFraction
1719
import kotlinx.coroutines.coroutineScope
1820

@@ -70,8 +72,9 @@ public class SliderState(
7072
get() = if (totalWidth == 0f) {
7173
0f
7274
} else {
73-
val valueFraction = value.valueToFraction(range)
74-
valueDistribution.inverse(valueFraction).coerceIn(0f, 1f)
75+
val inverted = valueDistribution.inverse(value)
76+
val invertedRange = valueDistribution.inverse(range)
77+
inverted.valueToFraction(invertedRange)
7578
}
7679

7780
internal val disabledRangeAsFractions: ClosedFloatingPointRange<Float>
@@ -82,7 +85,7 @@ public class SliderState(
8285
}
8386

8487
override fun dispatchRawDelta(delta: Float) {
85-
val newRawOffset = rawOffset + delta
88+
val newRawOffset = (rawOffset + delta).coerceIn(0f, totalWidth)
8689
val userValue = scaleToUserValue(newRawOffset)
8790
handleValueUpdate(userValue, newRawOffset)
8891
}
@@ -119,20 +122,27 @@ public class SliderState(
119122
* Scales offset in to the value that user should see
120123
*/
121124
private fun scaleToUserValue(offset: Float): Float {
122-
val coercedValue = (offset / totalWidth).coerceIn(0f..1f)
123-
val value = valueDistribution.interpolate(coercedValue)
124-
.fractionToValue(range)
125-
return coerceUserValue(value)
125+
println("Range: $range")
126+
val invertedRange = valueDistribution.inverse(range)
127+
println("Inverted range: $invertedRange")
128+
val value = scale(0f, totalWidth, offset, invertedRange.start, invertedRange.endInclusive)
129+
return coerceUserValue(valueDistribution.interpolate(value))
126130
}
127131

128132
/**
129133
* Converts value of the user into the raw offset on the track
130134
*/
131135
private fun scaleToOffset(value: Float): Float {
132-
val valueAsFraction = coerceUserValue(value).valueToFraction(range)
133-
return valueDistribution
134-
.inverse(valueAsFraction)
135-
.fractionToValue(0f, totalWidth)
136+
val coerced = coerceUserValue(value)
137+
val invertedRange = valueDistribution.inverse(range)
138+
val invertedValue = valueDistribution.inverse(coerced)
139+
return scale(
140+
invertedRange.start,
141+
invertedRange.endInclusive,
142+
invertedValue,
143+
0f,
144+
totalWidth,
145+
)
136146
}
137147

138148
internal fun coerceUserValue(value: Float): Float {
@@ -157,10 +167,18 @@ public class SliderState(
157167
private fun coerceRangeIntoFractions(
158168
subrange: ClosedFloatingPointRange<Float>,
159169
): ClosedFloatingPointRange<Float> {
160-
if (subrange.isEmpty()) return subrange
161-
val start = valueDistribution.inverse(subrange.start.valueToFraction(range))
162-
val end = valueDistribution.inverse(subrange.endInclusive.valueToFraction(range))
163-
return start..end
170+
val inverseRange = valueDistribution.inverse(range)
171+
val inverseSubrange = valueDistribution.inverse(subrange)
172+
173+
return calcFraction(
174+
inverseRange.start,
175+
inverseRange.endInclusive,
176+
inverseSubrange.start,
177+
)..calcFraction(
178+
inverseRange.start,
179+
inverseRange.endInclusive,
180+
inverseSubrange.endInclusive,
181+
)
164182
}
165183
}
166184

slider/src/main/java/io/monstarlab/mosaic/slider/distribution/CheckPointsValuesDistribution.kt

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package io.monstarlab.mosaic.slider.distribution
33
import io.monstarlab.mosaic.slider.math.LinearEquation
44
import io.monstarlab.mosaic.slider.math.Point
55
import io.monstarlab.mosaic.slider.math.RangedLinearEquation
6-
import io.monstarlab.mosaic.slider.math.valueToFraction
76

87
/**
98
* Represents a distribution strategy for slider values based on a list of check points.
@@ -24,31 +23,28 @@ public class CheckPointsValuesDistribution(
2423
private var equations: List<RangedLinearEquation>
2524

2625
init {
27-
require(valuesMap.isNotEmpty()) {
26+
val max = requireNotNull(valuesMap.maxByOrNull { it.first }?.second) {
2827
"Values map can't be empty"
2928
}
3029

31-
val offsetRange = valuesMap.minOf { it.first }..valuesMap.maxOf { it.first }
32-
val valueRange = valuesMap.minOf { it.second }..valuesMap.maxOf { it.second }
33-
3430
equations = valuesMap.sortedBy { it.first }
3531
.zipWithNext()
3632
.checkIncreasingValues() // check if values are always increasing
3733
.map {
38-
val x1Fraction = it.first.first.valueToFraction(offsetRange)
39-
val x2Fraction = it.second.first.valueToFraction(offsetRange)
40-
val y1Fraction = it.first.second.valueToFraction(valueRange)
41-
val y2Fraction = it.second.second.valueToFraction(valueRange)
34+
val x1 = it.first.first * max
35+
val x2 = it.second.first * max
36+
val y1 = it.first.second
37+
val y2 = it.second.second
4238
val equation = LinearEquation.fromTwoPoints(
43-
x1 = x1Fraction,
44-
x2 = x2Fraction,
45-
y1 = y1Fraction,
46-
y2 = y2Fraction,
39+
x1 = x1,
40+
x2 = x2,
41+
y1 = y1,
42+
y2 = y2,
4743
)
4844
RangedLinearEquation(
4945
equation = equation,
50-
offsetRange = x1Fraction..x2Fraction,
51-
valueRange = y1Fraction..y2Fraction,
46+
offsetRange = x1..x2,
47+
valueRange = y1..y2,
5248
)
5349
}
5450
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.monstarlab.mosaic.slider.distribution
2+
3+
internal fun SliderValuesDistribution.inverse(
4+
range: ClosedFloatingPointRange<Float>,
5+
): ClosedFloatingPointRange<Float> {
6+
if (range.isEmpty()) return range
7+
println("inverse ${range.start } ${range.endInclusive}")
8+
return inverse(range.start)..inverse(range.endInclusive)
9+
}
10+
11+
internal fun SliderValuesDistribution.interpolate(
12+
range: ClosedFloatingPointRange<Float>,
13+
): ClosedFloatingPointRange<Float> {
14+
if (range.isEmpty()) return range
15+
return interpolate(range.start)..interpolate(range.endInclusive)
16+
}

slider/src/main/java/io/monstarlab/mosaic/slider/distribution/SliderValuesDistribution.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package io.monstarlab.mosaic.slider.distribution
22

3-
import androidx.annotation.FloatRange
4-
53
/**
64
* Determines how the values will be distributed across the slider
75
* Usually the values are distributed in a linear fashion, this interfaces allows
@@ -12,18 +10,18 @@ public interface SliderValuesDistribution {
1210

1311
/**
1412
* Interpolates a value based on the distribution strategy.
15-
* @param value the normalized input value to interpolate, it must be between 0 and 1
16-
* @return the normalized interpolated value based on the distribution strategy, between 0 and 1
13+
* @param value the normalized input value to interpolate
14+
* @return the interpolated value based on the distribution strategy
1715
*/
18-
public fun interpolate(@FloatRange(0.0, 1.0) value: Float): Float
16+
public fun interpolate(value: Float): Float
1917

2018
/**
2119
* Inversely interpolates a value from the output range to the input range based on the distribution strategy.
2220
*
23-
* @param value the normalized value to inverse interpolate, must be between 0 and 1
24-
* @return the normalized inverse interpolated value based on the distribution strategy, between 0 and 1
21+
* @param value to inverse interpolate
22+
* @return inverse interpolated value based on the distribution strategy
2523
*/
26-
public fun inverse(@FloatRange(0.0, 1.0) value: Float): Float
24+
public fun inverse(value: Float): Float
2725

2826
public companion object {
2927

slider/src/main/java/io/monstarlab/mosaic/slider/math/LinearEquation.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package io.monstarlab.mosaic.slider.math
22

33
internal data class LinearEquation(
4-
private val m: Float,
5-
private val c: Float,
4+
private val m: Double,
5+
private val c: Double,
66
) {
7-
internal fun valueFromOffset(offset: Float): Float = m * offset + c
7+
internal fun valueFromOffset(offset: Float): Float {
8+
return (m * offset + c).toFloat()
9+
}
810

9-
internal fun offsetFromValue(value: Float): Float = (value - c) / m
11+
internal fun offsetFromValue(value: Float): Float {
12+
return ((value - c) / m).toFloat()
13+
}
1014

1115
internal companion object {
1216
internal fun fromTwoPoints(x1: Float, y1: Float, x2: Float, y2: Float): LinearEquation {
1317
require(x2 != x1) { "can't calc equation from points with similar x value" }
14-
val slope = (y2 - y1) / (x2 - x1)
18+
val slope = (y2.toDouble() - y1) / (x2 - x1)
1519
val c = y2 - slope * x2
1620
return LinearEquation(slope, c)
1721
}

slider/src/main/java/io/monstarlab/mosaic/slider/math/MathUtils.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.monstarlab.mosaic.slider.math
22

33
import androidx.compose.ui.util.lerp
4+
import kotlin.math.pow
5+
import kotlin.math.roundToInt
46

57
internal fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) =
68
lerp(a2, b2, calcFraction(a1, b1, x1))
@@ -17,5 +19,7 @@ internal fun Float.valueToFraction(range: ClosedFloatingPointRange<Float>) =
1719
internal fun Float.fractionToValue(rangeStart: Float, rangeEnd: Float): Float =
1820
scale(0f, 1f, coerceIn(0f, 1f), rangeStart, rangeEnd)
1921

20-
internal fun Float.fractionToValue(range: ClosedFloatingPointRange<Float>): Float =
21-
fractionToValue(range.start, range.endInclusive)
22+
internal fun Float.roundFractionToDigits(digits: Int): Float {
23+
val factor = 10.0.pow(digits)
24+
return (this * factor).roundToInt() / factor.toFloat()
25+
}

slider/src/test/java/io/monstarlab/mosaic/slider/CheckPointsValueDistributionTest.kt

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ class CheckPointsValueDistributionTest {
1717
checkPointsValueDistribution = CheckPointsValuesDistribution(
1818
listOf(
1919
0f to 0f,
20-
25f to 25f,
21-
50f to 75f,
22-
100f to 100f,
20+
0.25f to 25f,
21+
0.50f to 75f,
22+
1f to 100f,
2323
),
2424
)
2525
}
@@ -45,12 +45,12 @@ class CheckPointsValueDistributionTest {
4545
@Test
4646
fun `create from pairs and interpolate`() {
4747
val points = listOf(
48-
0.1f to 0.1f,
49-
0.2f to 0.2f,
50-
0.25f to 0.25f,
51-
0.4f to 0.55f,
52-
0.6f to 0.8f,
53-
0.75f to 0.875f,
48+
10f to 10f,
49+
20f to 20f,
50+
25f to 25f,
51+
40f to 55f,
52+
60f to 80f,
53+
70.5f to 85.25f,
5454
)
5555
points.forEach {
5656
assertEquals(it.second, checkPointsValueDistribution.interpolate(it.first), accuracy)
@@ -60,12 +60,12 @@ class CheckPointsValueDistributionTest {
6060
@Test
6161
fun `create from pairs and inverse`() {
6262
val points = listOf(
63-
0.1f to 0.1f,
64-
0.2f to 0.2f,
65-
0.25f to 0.25f,
66-
0.4f to 0.55f,
67-
0.6f to 0.8f,
68-
0.75f to 0.875f,
63+
10f to 10f,
64+
20f to 20f,
65+
25f to 25f,
66+
40f to 55f,
67+
60f to 80f,
68+
70.5f to 85.25f,
6969
)
7070
points.forEach {
7171
assertEquals(it.first, checkPointsValueDistribution.inverse(it.second), accuracy)

0 commit comments

Comments
 (0)