Skip to content

Commit 629417e

Browse files
authored
Merge pull request #19 from monstar-lab-oss/feat/slider_distribution
Feat/slider distribution
2 parents 3bad9b2 + c848695 commit 629417e

File tree

15 files changed

+307
-144
lines changed

15 files changed

+307
-144
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
/.idea/workspace.xml
99
/.idea/navEditor.xml
1010
/.idea/assetWizardSettings.xml
11+
/.idea/misc.xml
12+
/.idea/deploymentTargetDropDown.xml
13+
/.idea/deploymentTargetSelector.xml
1114
.DS_Store
1215
/build
1316
/captures

.idea/deploymentTargetSelector.xml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ 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.SliderValueDistribution
42+
import io.monstarlab.mosaic.slider.distribution.CheckPointsValueDistribution
43+
import io.monstarlab.mosaic.slider.distribution.SliderValueDistribution
4344
import kotlin.math.roundToInt
4445
import androidx.compose.material3.Slider as MaterialSlider
4546

@@ -66,7 +67,6 @@ fun MosaicSliderDemo() {
6667
var enabled by remember { mutableStateOf(true) }
6768
var isCustom by remember { mutableStateOf(false) }
6869
var linearDistribution by remember { mutableStateOf(false) }
69-
7070
var sliderValue by remember { mutableFloatStateOf(500f) }
7171

7272
MaterialSlider(
@@ -83,8 +83,15 @@ fun MosaicSliderDemo() {
8383
Modifier
8484
}
8585

86-
val parabolic: SliderValueDistribution = remember {
87-
SliderValueDistribution.parabolic(a = 0.005f)
86+
val fragmentedDistribution: SliderValueDistribution = remember {
87+
CheckPointsValueDistribution(
88+
listOf(
89+
0f to 0f,
90+
0.2f to 500f,
91+
0.4f to 800f,
92+
1f to 1000f,
93+
),
94+
)
8895
}
8996

9097
Slider(
@@ -94,10 +101,11 @@ fun MosaicSliderDemo() {
94101
enabled = enabled,
95102
colors = colors,
96103
range = 0f..1000f,
104+
disabledRange = 50f..300f,
97105
valueDistribution = if (linearDistribution) {
98106
SliderValueDistribution.Linear
99107
} else {
100-
parabolic
108+
fragmentedDistribution
101109
},
102110
thumb = {
103111
if (isCustom) {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@ internal fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) =
77

88
internal fun calcFraction(a: Float, b: Float, pos: Float) =
99
(if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f)
10+
11+
internal fun Float.valueToFraction(rangeStart: Float, rangeEnd: Float) =
12+
calcFraction(rangeStart, rangeEnd, this)
13+
14+
internal fun Float.valueToFraction(range: ClosedFloatingPointRange<Float>) =
15+
valueToFraction(range.start, range.endInclusive)
16+
17+
internal fun Float.fractionToValue(rangeStart: Float, rangeEnd: Float): Float =
18+
scale(0f, 1f, this.coerceIn(0f, 1f), rangeStart, rangeEnd)
19+
20+
internal fun Float.fractionToValue(range: ClosedFloatingPointRange<Float>): Float =
21+
scale(0f, 1f, this, range.start, range.endInclusive)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ internal fun Modifier.sliderSemantics(state: SliderState, enabled: Boolean): Mod
4646
if (!enabled) disabled()
4747
setProgress(
4848
action = { targetValue ->
49-
val coercedValue = state.coerceValue(targetValue)
49+
val coercedValue = state.coerceUserValue(targetValue)
5050
// This is to keep it consistent with AbsSeekbar.java: return false if no
5151
// change from current.
5252
if (coercedValue == state.value) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color
1515
import androidx.compose.ui.platform.LocalLayoutDirection
1616
import androidx.compose.ui.tooling.preview.Preview
1717
import androidx.compose.ui.unit.LayoutDirection
18+
import io.monstarlab.mosaic.slider.distribution.SliderValueDistribution
1819

1920
/**
2021
* A composable function that creates a slider UI component.
@@ -99,7 +100,7 @@ public fun Slider(
99100
thumb = thumb,
100101
track = {
101102
SliderTrack(
102-
progress = state.valueAsFraction,
103+
progress = state.offsetAsFraction,
103104
colors = colors,
104105
disabledRange = state.disabledRangeAsFractions,
105106
enabled = enabled,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public fun SliderLayout(
4646
state.updateDimensions(sliderWidth.toFloat(), thumbPlaceable.width.toFloat())
4747

4848
val trackOffsetX = thumbPlaceable.width / 2
49-
val thumbOffsetX = ((trackPlaceable.width) * state.valueAsFraction).roundToInt()
49+
val thumbOffsetX = ((trackPlaceable.width) * state.offsetAsFraction).roundToInt()
5050
val trackOffsetY = (sliderHeight - trackPlaceable.height) / 2
5151
val thumbOffsetY = (sliderHeight - thumbPlaceable.height) / 2
5252

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

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.runtime.mutableStateOf
1111
import androidx.compose.runtime.remember
1212
import androidx.compose.runtime.setValue
1313
import androidx.compose.ui.geometry.Offset
14+
import io.monstarlab.mosaic.slider.distribution.SliderValueDistribution
1415
import kotlinx.coroutines.coroutineScope
1516

1617
/**
@@ -56,18 +57,19 @@ public class SliderState(
5657
public var value: Float
5758
get() = valueState
5859
set(value) {
59-
valueState = coerceValue(value)
60+
valueState = coerceUserValue(value)
6061
}
6162

6263
/**
6364
* Internal value returned as fraction, used for displaying and specifying
6465
* the "real" position of the thumb
6566
*/
66-
internal val valueAsFraction: Float
67-
get() {
68-
val inverted = valueDistribution.inverse(value)
69-
val invertedRange = valueDistribution.inverse(range)
70-
return calcFraction(invertedRange.start, invertedRange.endInclusive, inverted)
67+
internal val offsetAsFraction: Float
68+
get() = if (totalWidth == 0f) {
69+
0f
70+
} else {
71+
val valueFraction = value.valueToFraction(range)
72+
valueDistribution.inverse(valueFraction).coerceIn(0f, 1f)
7173
}
7274

7375
internal val disabledRangeAsFractions: ClosedFloatingPointRange<Float>
@@ -115,35 +117,33 @@ public class SliderState(
115117
* Scales offset in to the value that user should see
116118
*/
117119
private fun scaleToUserValue(offset: Float): Float {
118-
val invertedRange = valueDistribution.inverse(range)
119-
val value = scale(0f, totalWidth, offset, invertedRange.start, invertedRange.endInclusive)
120-
return coerceValue(valueDistribution.interpolate(value))
120+
val coercedValue = (offset / totalWidth).coerceIn(0f..1f)
121+
val value = valueDistribution.interpolate(coercedValue)
122+
.fractionToValue(range)
123+
return coerceUserValue(value)
121124
}
122125

123126
/**
124127
* Converts value of the user into the raw offset on the track
125128
*/
126129
private fun scaleToOffset(value: Float): Float {
127-
val coerced = coerceValue(value)
128-
val invertedRange = valueDistribution.inverse(range)
129-
val invertedValue = valueDistribution.inverse(coerced)
130-
return scale(
131-
invertedRange.start,
132-
invertedRange.endInclusive,
133-
invertedValue,
134-
0f,
135-
totalWidth,
136-
)
130+
val valueAsFraction = coerceUserValue(value).valueToFraction(range)
131+
return valueDistribution
132+
.inverse(valueAsFraction)
133+
.fractionToValue(0f, totalWidth)
137134
}
138135

139-
internal fun coerceValue(value: Float): Float {
136+
internal fun coerceUserValue(value: Float): Float {
140137
return value
141138
.coerceIn(range)
142139
.coerceIntoDisabledRange()
143140
}
144141

145142
private fun Float.coerceIntoDisabledRange(): Float {
146-
if (disabledRange.isEmpty()) return this
143+
if (disabledRange.isEmpty() || !disabledRange.contains(this)) {
144+
return this
145+
}
146+
147147
// check if disabled range is on the left or right
148148
return if (disabledRange.start == range.start) {
149149
coerceAtLeast(disabledRange.endInclusive)
@@ -156,17 +156,9 @@ public class SliderState(
156156
subrange: ClosedFloatingPointRange<Float>,
157157
): ClosedFloatingPointRange<Float> {
158158
if (subrange.isEmpty()) return subrange
159-
val interpolatedRange = valueDistribution.interpolate(range)
160-
val interpolatedSubrange = valueDistribution.interpolate(subrange)
161-
return calcFraction(
162-
interpolatedRange.start,
163-
interpolatedRange.endInclusive,
164-
interpolatedSubrange.start,
165-
)..calcFraction(
166-
interpolatedRange.start,
167-
interpolatedRange.endInclusive,
168-
interpolatedSubrange.endInclusive,
169-
)
159+
val start = valueDistribution.inverse(subrange.start.valueToFraction(range))
160+
val end = valueDistribution.inverse(subrange.endInclusive.valueToFraction(range))
161+
return start..end
170162
}
171163
}
172164

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

Lines changed: 0 additions & 101 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.monstarlab.mosaic.slider.distribution
2+
3+
import io.monstarlab.mosaic.slider.valueToFraction
4+
5+
public class CheckPointsValueDistribution(
6+
valuesMap: List<Pair<Float, Float>>,
7+
) :
8+
SliderValueDistribution {
9+
10+
private var equations: List<RangedLinearEquation>
11+
12+
init {
13+
require(valuesMap.isNotEmpty()) {
14+
"Values map can't be empty"
15+
}
16+
17+
val offsetRange = valuesMap.minOf { it.first }..valuesMap.maxOf { it.first }
18+
val valueRange = valuesMap.minOf { it.second }..valuesMap.maxOf { it.second }
19+
val zipped = valuesMap.sortedBy { it.first }
20+
.zipWithNext()
21+
// make sure all values are increasing, otherwise throw an exception
22+
zipped.firstOrNull { it.first.second >= it.second.second }
23+
?.let {
24+
throw DecreasingValueException(it.first)
25+
}
26+
equations = zipped.map {
27+
val x1Fraction = it.first.first.valueToFraction(offsetRange)
28+
val x2Fraction = it.second.first.valueToFraction(offsetRange)
29+
val y1Fraction = it.first.second.valueToFraction(valueRange)
30+
val y2Fraction = it.second.second.valueToFraction(valueRange)
31+
val equation = LinearEquation.fromTwoPoints(
32+
x1 = x1Fraction,
33+
x2 = x2Fraction,
34+
y1 = y1Fraction,
35+
y2 = y2Fraction,
36+
)
37+
RangedLinearEquation(
38+
equation = equation,
39+
offsetRange = x1Fraction..x2Fraction,
40+
valueRange = y1Fraction..y2Fraction,
41+
)
42+
}
43+
}
44+
45+
override fun interpolate(value: Float): Float {
46+
val equation = equations.firstOrNull { it.offsetRange.contains(value) }?.equation
47+
checkNotNull(equation) { "No equation found for value $value during interpolate" }
48+
return equation.valueFromOffset(value)
49+
}
50+
51+
override fun inverse(value: Float): Float {
52+
val equation = equations.firstOrNull { it.valueRange.contains(value) }?.equation
53+
checkNotNull(equation) { "No equation found for value $value during inverse" }
54+
return equation.offsetFromValue(value)
55+
}
56+
57+
public class DecreasingValueException(progressValuePair: Pair<Float, Float>) :
58+
IllegalStateException(
59+
"Values must be always increasing with increasing progress," +
60+
" item at progress ${progressValuePair.first} with value " +
61+
"${progressValuePair.second} is breaking this rule ",
62+
)
63+
}

0 commit comments

Comments
 (0)