1 /*
<lambda>null2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.material
18 
19 import androidx.annotation.IntRange
20 import androidx.compose.animation.core.Animatable
21 import androidx.compose.animation.core.TweenSpec
22 import androidx.compose.foundation.Canvas
23 import androidx.compose.foundation.MutatePriority
24 import androidx.compose.foundation.MutatorMutex
25 import androidx.compose.foundation.background
26 import androidx.compose.foundation.focusable
27 import androidx.compose.foundation.gestures.DragScope
28 import androidx.compose.foundation.gestures.DraggableState
29 import androidx.compose.foundation.gestures.GestureCancellationException
30 import androidx.compose.foundation.gestures.Orientation
31 import androidx.compose.foundation.gestures.awaitEachGesture
32 import androidx.compose.foundation.gestures.awaitFirstDown
33 import androidx.compose.foundation.gestures.detectTapGestures
34 import androidx.compose.foundation.gestures.draggable
35 import androidx.compose.foundation.gestures.horizontalDrag
36 import androidx.compose.foundation.hoverable
37 import androidx.compose.foundation.indication
38 import androidx.compose.foundation.interaction.DragInteraction
39 import androidx.compose.foundation.interaction.Interaction
40 import androidx.compose.foundation.interaction.MutableInteractionSource
41 import androidx.compose.foundation.interaction.PressInteraction
42 import androidx.compose.foundation.layout.Box
43 import androidx.compose.foundation.layout.BoxScope
44 import androidx.compose.foundation.layout.BoxWithConstraints
45 import androidx.compose.foundation.layout.Spacer
46 import androidx.compose.foundation.layout.fillMaxSize
47 import androidx.compose.foundation.layout.heightIn
48 import androidx.compose.foundation.layout.padding
49 import androidx.compose.foundation.layout.requiredSizeIn
50 import androidx.compose.foundation.layout.size
51 import androidx.compose.foundation.layout.widthIn
52 import androidx.compose.foundation.progressSemantics
53 import androidx.compose.foundation.shape.CircleShape
54 import androidx.compose.runtime.Composable
55 import androidx.compose.runtime.Immutable
56 import androidx.compose.runtime.LaunchedEffect
57 import androidx.compose.runtime.MutableState
58 import androidx.compose.runtime.SideEffect
59 import androidx.compose.runtime.Stable
60 import androidx.compose.runtime.State
61 import androidx.compose.runtime.getValue
62 import androidx.compose.runtime.mutableFloatStateOf
63 import androidx.compose.runtime.mutableStateListOf
64 import androidx.compose.runtime.mutableStateOf
65 import androidx.compose.runtime.remember
66 import androidx.compose.runtime.rememberCoroutineScope
67 import androidx.compose.runtime.rememberUpdatedState
68 import androidx.compose.runtime.setValue
69 import androidx.compose.ui.Alignment
70 import androidx.compose.ui.Modifier
71 import androidx.compose.ui.composed
72 import androidx.compose.ui.draw.shadow
73 import androidx.compose.ui.geometry.Offset
74 import androidx.compose.ui.geometry.lerp
75 import androidx.compose.ui.graphics.Color
76 import androidx.compose.ui.graphics.PointMode
77 import androidx.compose.ui.graphics.StrokeCap
78 import androidx.compose.ui.graphics.compositeOver
79 import androidx.compose.ui.input.key.Key
80 import androidx.compose.ui.input.key.KeyEventType
81 import androidx.compose.ui.input.key.key
82 import androidx.compose.ui.input.key.onKeyEvent
83 import androidx.compose.ui.input.key.type
84 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
85 import androidx.compose.ui.input.pointer.PointerId
86 import androidx.compose.ui.input.pointer.PointerInputChange
87 import androidx.compose.ui.input.pointer.PointerType
88 import androidx.compose.ui.input.pointer.pointerInput
89 import androidx.compose.ui.input.pointer.positionChange
90 import androidx.compose.ui.platform.LocalDensity
91 import androidx.compose.ui.platform.LocalLayoutDirection
92 import androidx.compose.ui.platform.debugInspectorInfo
93 import androidx.compose.ui.semantics.contentDescription
94 import androidx.compose.ui.semantics.disabled
95 import androidx.compose.ui.semantics.semantics
96 import androidx.compose.ui.semantics.setProgress
97 import androidx.compose.ui.unit.Dp
98 import androidx.compose.ui.unit.LayoutDirection
99 import androidx.compose.ui.unit.dp
100 import androidx.compose.ui.util.fastCoerceIn
101 import androidx.compose.ui.util.fastMap
102 import androidx.compose.ui.util.fastMinByOrNull
103 import androidx.compose.ui.util.lerp
104 import kotlin.math.abs
105 import kotlin.math.floor
106 import kotlin.math.max
107 import kotlin.math.min
108 import kotlinx.coroutines.CancellationException
109 import kotlinx.coroutines.CoroutineScope
110 import kotlinx.coroutines.coroutineScope
111 import kotlinx.coroutines.launch
112 
113 /**
114  * [Material Design slider](https://material.io/components/sliders)
115  *
116  * Sliders allow users to make selections from a range of values.
117  *
118  * Sliders reflect a range of values along a bar, from which users may select a single value. They
119  * are ideal for adjusting settings such as volume, brightness, or applying image filters.
120  *
121  * ![Sliders
122  * image](https://developer.android.com/images/reference/androidx/compose/material/sliders.png)
123  *
124  * Use continuous sliders to allow users to make meaningful selections that don’t require a specific
125  * value:
126  *
127  * @sample androidx.compose.material.samples.SliderSample
128  *
129  * You can allow the user to choose only between predefined set of values by specifying the amount
130  * of steps between min and max values:
131  *
132  * @sample androidx.compose.material.samples.StepsSliderSample
133  * @param value current value of the Slider. If outside of [valueRange] provided, value will be
134  *   coerced to this range.
135  * @param onValueChange lambda in which value should be updated
136  * @param modifier modifiers for the Slider layout
137  * @param enabled whether or not component is enabled and can be interacted with or not
138  * @param valueRange range of values that Slider value can take. Passed [value] will be coerced to
139  *   this range
140  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
141  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
142  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
143  *   continuously and allow any value from the range. Must not be negative.
144  * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
145  *   shouldn't be used to update the slider value (use [onValueChange] for that), but rather to know
146  *   when the user has completed selecting a new value by ending a drag or a click.
147  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
148  *   emitting [Interaction]s for this slider. You can use this to change the slider's appearance or
149  *   preview the slider in different states. Note that if `null` is provided, interactions will
150  *   still happen internally.
151  * @param colors [SliderColors] that will be used to determine the color of the Slider parts in
152  *   different state. See [SliderDefaults.colors] to customize.
153  */
154 @Composable
155 fun Slider(
156     value: Float,
157     onValueChange: (Float) -> Unit,
158     modifier: Modifier = Modifier,
159     enabled: Boolean = true,
160     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
161     @IntRange(from = 0) steps: Int = 0,
162     onValueChangeFinished: (() -> Unit)? = null,
163     interactionSource: MutableInteractionSource? = null,
164     colors: SliderColors = SliderDefaults.colors()
165 ) {
166     @Suppress("NAME_SHADOWING")
167     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
168     require(steps >= 0) { "steps should be >= 0" }
169     val onValueChangeState = rememberUpdatedState(onValueChange)
170     val onValueChangeFinishedState = rememberUpdatedState(onValueChangeFinished)
171     val tickFractions = remember(steps) { stepsToTickFractions(steps) }
172 
173     BoxWithConstraints(
174         modifier
175             .minimumInteractiveComponentSize()
176             .requiredSizeIn(minWidth = ThumbRadius * 2, minHeight = ThumbRadius * 2)
177             .sliderSemantics(
178                 value,
179                 enabled,
180                 onValueChange,
181                 onValueChangeFinished,
182                 valueRange,
183                 steps
184             )
185             .focusable(enabled, interactionSource)
186             .slideOnKeyEvents(
187                 enabled,
188                 steps,
189                 valueRange,
190                 value,
191                 LocalLayoutDirection.current == LayoutDirection.Rtl,
192                 onValueChangeState,
193                 onValueChangeFinishedState
194             )
195     ) {
196         val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
197         val widthPx = constraints.maxWidth.toFloat()
198         val maxPx: Float
199         val minPx: Float
200 
201         with(LocalDensity.current) {
202             maxPx = max(widthPx - ThumbRadius.toPx(), 0f)
203             minPx = min(ThumbRadius.toPx(), maxPx)
204         }
205 
206         fun scaleToUserValue(offset: Float) =
207             scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
208 
209         fun scaleToOffset(userValue: Float) =
210             scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
211 
212         val scope = rememberCoroutineScope()
213         val rawOffset = remember { mutableFloatStateOf(scaleToOffset(value)) }
214         val pressOffset = remember { mutableFloatStateOf(0f) }
215 
216         val draggableState =
217             remember(minPx, maxPx, valueRange) {
218                 SliderDraggableState {
219                     rawOffset.floatValue = (rawOffset.floatValue + it + pressOffset.floatValue)
220                     pressOffset.floatValue = 0f
221                     val offsetInTrack = rawOffset.floatValue.coerceIn(minPx, maxPx)
222                     onValueChangeState.value.invoke(scaleToUserValue(offsetInTrack))
223                 }
224             }
225 
226         CorrectValueSideEffect(::scaleToOffset, valueRange, minPx..maxPx, rawOffset, value)
227 
228         val gestureEndAction =
229             rememberUpdatedState<(Float) -> Unit> { velocity: Float ->
230                 val current = rawOffset.floatValue
231                 val target = snapValueToTick(current, tickFractions, minPx, maxPx)
232                 if (current != target) {
233                     scope.launch {
234                         animateToTarget(draggableState, current, target, velocity)
235                         onValueChangeFinished?.invoke()
236                     }
237                 } else if (!draggableState.isDragging) {
238                     // check ifDragging in case the change is still in progress (touch -> drag case)
239                     onValueChangeFinished?.invoke()
240                 }
241             }
242         val press =
243             Modifier.sliderTapModifier(
244                 draggableState,
245                 interactionSource,
246                 widthPx,
247                 isRtl,
248                 rawOffset,
249                 gestureEndAction,
250                 pressOffset,
251                 enabled
252             )
253 
254         val drag =
255             Modifier.draggable(
256                 orientation = Orientation.Horizontal,
257                 reverseDirection = isRtl,
258                 enabled = enabled,
259                 interactionSource = interactionSource,
260                 onDragStopped = { velocity -> gestureEndAction.value.invoke(velocity) },
261                 startDragImmediately = draggableState.isDragging,
262                 state = draggableState
263             )
264 
265         val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive)
266         val fraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced)
267         SliderImpl(
268             enabled,
269             fraction,
270             tickFractions,
271             colors,
272             maxPx - minPx,
273             interactionSource,
274             modifier = press.then(drag)
275         )
276     }
277 }
278 
Modifiernull279 private fun Modifier.slideOnKeyEvents(
280     enabled: Boolean,
281     steps: Int,
282     valueRange: ClosedFloatingPointRange<Float>,
283     value: Float,
284     isRtl: Boolean,
285     onValueChangeState: State<(Float) -> Unit>,
286     onValueChangeFinishedState: State<(() -> Unit)?>
287 ): Modifier {
288     require(steps >= 0) { "steps should be >= 0" }
289     return this.onKeyEvent {
290         if (!enabled) return@onKeyEvent false
291         when (it.type) {
292             KeyEventType.KeyDown -> {
293                 val rangeLength = abs(valueRange.endInclusive - valueRange.start)
294                 // When steps == 0, it means that a user is not limited by a step length (delta)
295                 // when using touch or mouse. But it is not possible to adjust the value
296                 // continuously when using keyboard buttons - the delta has to be discrete.
297                 // In this case, 1% of the valueRange seems to make sense.
298                 val actualSteps = if (steps > 0) steps + 1 else 100
299                 val delta = rangeLength / actualSteps
300                 when (it.key) {
301                     Key.DirectionUp -> {
302                         onValueChangeState.value((value + delta).coerceIn(valueRange))
303                         true
304                     }
305                     Key.DirectionDown -> {
306                         onValueChangeState.value((value - delta).coerceIn(valueRange))
307                         true
308                     }
309                     Key.DirectionRight -> {
310                         val sign = if (isRtl) -1 else 1
311                         onValueChangeState.value((value + sign * delta).coerceIn(valueRange))
312                         true
313                     }
314                     Key.DirectionLeft -> {
315                         val sign = if (isRtl) -1 else 1
316                         onValueChangeState.value((value - sign * delta).coerceIn(valueRange))
317                         true
318                     }
319                     Key.MoveHome -> {
320                         onValueChangeState.value(valueRange.start)
321                         true
322                     }
323                     Key.MoveEnd -> {
324                         onValueChangeState.value(valueRange.endInclusive)
325                         true
326                     }
327                     Key.PageUp -> {
328                         val page = (actualSteps / 10).coerceIn(1, 10)
329                         onValueChangeState.value((value - page * delta).coerceIn(valueRange))
330                         true
331                     }
332                     Key.PageDown -> {
333                         val page = (actualSteps / 10).coerceIn(1, 10)
334                         onValueChangeState.value((value + page * delta).coerceIn(valueRange))
335                         true
336                     }
337                     else -> false
338                 }
339             }
340             KeyEventType.KeyUp -> {
341                 when (it.key) {
342                     Key.DirectionUp,
343                     Key.DirectionDown,
344                     Key.DirectionRight,
345                     Key.DirectionLeft,
346                     Key.MoveHome,
347                     Key.MoveEnd,
348                     Key.PageUp,
349                     Key.PageDown -> {
350                         onValueChangeFinishedState.value?.invoke()
351                         true
352                     }
353                     else -> false
354                 }
355             }
356             else -> false
357         }
358     }
359 }
360 
361 /**
362  * [Material Design slider](https://material.io/components/sliders)
363  *
364  * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
365  *
366  * The two values are still bounded by the value range but they also cannot cross each other.
367  *
368  * Use continuous Range Sliders to allow users to make meaningful selections that don’t require a
369  * specific values:
370  *
371  * @sample androidx.compose.material.samples.RangeSliderSample
372  *
373  * You can allow the user to choose only between predefined set of values by specifying the amount
374  * of steps between min and max values:
375  *
376  * @sample androidx.compose.material.samples.StepRangeSliderSample
377  * @param value current values of the RangeSlider. If either value is outside of [valueRange]
378  *   provided, it will be coerced to this range.
379  * @param onValueChange lambda in which values should be updated
380  * @param modifier modifiers for the Range Slider layout
381  * @param enabled whether or not component is enabled and can we interacted with or not
382  * @param valueRange range of values that Range Slider values can take. Passed [value] will be
383  *   coerced to this range
384  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
385  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
386  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
387  *   continuously and allow any value from the range. Must not be negative.
388  * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
389  *   shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
390  *   to know when the user has completed selecting a new value by ending a drag or a click.
391  * @param colors [SliderColors] that will be used to determine the color of the Range Slider parts
392  *   in different state. See [SliderDefaults.colors] to customize.
393  */
394 @Composable
395 @ExperimentalMaterialApi
RangeSlidernull396 fun RangeSlider(
397     value: ClosedFloatingPointRange<Float>,
398     onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
399     modifier: Modifier = Modifier,
400     enabled: Boolean = true,
401     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
402     @IntRange(from = 0) steps: Int = 0,
403     onValueChangeFinished: (() -> Unit)? = null,
404     colors: SliderColors = SliderDefaults.colors()
405 ) {
406     val startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
407     val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
408 
409     require(steps >= 0) { "steps should be >= 0" }
410     val onValueChangeState = rememberUpdatedState(onValueChange)
411     val tickFractions = remember(steps) { stepsToTickFractions(steps) }
412 
413     BoxWithConstraints(
414         modifier =
415             modifier
416                 .minimumInteractiveComponentSize()
417                 .requiredSizeIn(minWidth = ThumbRadius * 4, minHeight = ThumbRadius * 2)
418     ) {
419         val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
420         val widthPx = constraints.maxWidth.toFloat()
421         val maxPx: Float
422         val minPx: Float
423 
424         with(LocalDensity.current) {
425             maxPx = widthPx - ThumbRadius.toPx()
426             minPx = ThumbRadius.toPx()
427         }
428 
429         fun scaleToUserValue(offset: ClosedFloatingPointRange<Float>) =
430             scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
431 
432         fun scaleToOffset(userValue: Float) =
433             scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
434 
435         val rawOffsetStart = remember { mutableFloatStateOf(scaleToOffset(value.start)) }
436         val rawOffsetEnd = remember { mutableFloatStateOf(scaleToOffset(value.endInclusive)) }
437 
438         CorrectValueSideEffect(
439             ::scaleToOffset,
440             valueRange,
441             minPx..maxPx,
442             rawOffsetStart,
443             value.start
444         )
445         CorrectValueSideEffect(
446             ::scaleToOffset,
447             valueRange,
448             minPx..maxPx,
449             rawOffsetEnd,
450             value.endInclusive
451         )
452 
453         val scope = rememberCoroutineScope()
454         val gestureEndAction =
455             rememberUpdatedState<(Boolean) -> Unit> { isStart ->
456                 val current = (if (isStart) rawOffsetStart else rawOffsetEnd).floatValue
457                 // target is a closest anchor to the `current`, if exists
458                 val target = snapValueToTick(current, tickFractions, minPx, maxPx)
459                 if (current == target) {
460                     onValueChangeFinished?.invoke()
461                     return@rememberUpdatedState
462                 }
463 
464                 scope.launch {
465                     Animatable(initialValue = current).animateTo(
466                         target,
467                         SliderToTickAnimation,
468                         0f
469                     ) {
470                         (if (isStart) rawOffsetStart else rawOffsetEnd).floatValue = this.value
471                         onValueChangeState.value.invoke(
472                             scaleToUserValue(rawOffsetStart.floatValue..rawOffsetEnd.floatValue)
473                         )
474                     }
475 
476                     onValueChangeFinished?.invoke()
477                 }
478             }
479 
480         val onDrag =
481             rememberUpdatedState<(Boolean, Float) -> Unit> { isStart, offset ->
482                 val offsetRange =
483                     if (isStart) {
484                         rawOffsetStart.floatValue = (rawOffsetStart.floatValue + offset)
485                         rawOffsetEnd.floatValue = scaleToOffset(value.endInclusive)
486                         val offsetEnd = rawOffsetEnd.floatValue
487                         val offsetStart = rawOffsetStart.floatValue.coerceIn(minPx, offsetEnd)
488                         offsetStart..offsetEnd
489                     } else {
490                         rawOffsetEnd.floatValue = (rawOffsetEnd.floatValue + offset)
491                         rawOffsetStart.floatValue = scaleToOffset(value.start)
492                         val offsetStart = rawOffsetStart.floatValue
493                         val offsetEnd = rawOffsetEnd.floatValue.coerceIn(offsetStart, maxPx)
494                         offsetStart..offsetEnd
495                     }
496 
497                 onValueChangeState.value.invoke(scaleToUserValue(offsetRange))
498             }
499 
500         val pressDrag =
501             Modifier.rangeSliderPressDragModifier(
502                 startInteractionSource,
503                 endInteractionSource,
504                 rawOffsetStart,
505                 rawOffsetEnd,
506                 enabled,
507                 isRtl,
508                 widthPx,
509                 valueRange,
510                 gestureEndAction,
511                 onDrag,
512             )
513 
514         // The positions of the thumbs are dependant on each other.
515         val coercedStart = value.start.coerceIn(valueRange.start, value.endInclusive)
516         val coercedEnd = value.endInclusive.coerceIn(value.start, valueRange.endInclusive)
517         val fractionStart = calcFraction(valueRange.start, valueRange.endInclusive, coercedStart)
518         val fractionEnd = calcFraction(valueRange.start, valueRange.endInclusive, coercedEnd)
519         val startSteps = floor(steps * fractionEnd).toInt()
520         val endSteps = floor(steps * (1f - fractionStart)).toInt()
521 
522         val startThumbSemantics =
523             Modifier.sliderSemantics(
524                 coercedStart,
525                 enabled,
526                 { value -> onValueChangeState.value.invoke(value..coercedEnd) },
527                 onValueChangeFinished,
528                 valueRange.start..coercedEnd,
529                 startSteps
530             )
531         val endThumbSemantics =
532             Modifier.sliderSemantics(
533                 coercedEnd,
534                 enabled,
535                 { value -> onValueChangeState.value.invoke(coercedStart..value) },
536                 onValueChangeFinished,
537                 coercedStart..valueRange.endInclusive,
538                 endSteps
539             )
540 
541         RangeSliderImpl(
542             enabled,
543             fractionStart,
544             fractionEnd,
545             tickFractions,
546             colors,
547             maxPx - minPx,
548             startInteractionSource,
549             endInteractionSource,
550             modifier = pressDrag,
551             startThumbSemantics,
552             endThumbSemantics
553         )
554     }
555 }
556 
557 /** Object to hold defaults used by [Slider] */
558 object SliderDefaults {
559 
560     /**
561      * Creates a [SliderColors] that represents the different colors used in parts of the [Slider]
562      * in different states.
563      *
564      * For the name references below the words "active" and "inactive" are used. Active part of the
565      * slider is filled with progress, so if slider's progress is 30% out of 100%, left (or right in
566      * RTL) 30% of the track will be active, the rest is not active.
567      *
568      * @param thumbColor thumb color when enabled
569      * @param disabledThumbColor thumb colors when disabled
570      * @param activeTrackColor color of the track in the part that is "active", meaning that the
571      *   thumb is ahead of it
572      * @param inactiveTrackColor color of the track in the part that is "inactive", meaning that the
573      *   thumb is before it
574      * @param disabledActiveTrackColor color of the track in the "active" part when the Slider is
575      *   disabled
576      * @param disabledInactiveTrackColor color of the track in the "inactive" part when the Slider
577      *   is disabled
578      * @param activeTickColor colors to be used to draw tick marks on the active track, if `steps`
579      *   is specified
580      * @param inactiveTickColor colors to be used to draw tick marks on the inactive track, if
581      *   `steps` are specified on the Slider is specified
582      * @param disabledActiveTickColor colors to be used to draw tick marks on the active track when
583      *   Slider is disabled and when `steps` are specified on it
584      * @param disabledInactiveTickColor colors to be used to draw tick marks on the inactive part of
585      *   the track when Slider is disabled and when `steps` are specified on it
586      */
587     @Composable
colorsnull588     fun colors(
589         thumbColor: Color = MaterialTheme.colors.primary,
590         disabledThumbColor: Color =
591             MaterialTheme.colors.onSurface
592                 .copy(alpha = ContentAlpha.disabled)
593                 .compositeOver(MaterialTheme.colors.surface),
594         activeTrackColor: Color = MaterialTheme.colors.primary,
595         inactiveTrackColor: Color = activeTrackColor.copy(alpha = InactiveTrackAlpha),
596         disabledActiveTrackColor: Color =
597             MaterialTheme.colors.onSurface.copy(alpha = DisabledActiveTrackAlpha),
598         disabledInactiveTrackColor: Color =
599             disabledActiveTrackColor.copy(alpha = DisabledInactiveTrackAlpha),
600         activeTickColor: Color = contentColorFor(activeTrackColor).copy(alpha = TickAlpha),
601         inactiveTickColor: Color = activeTrackColor.copy(alpha = TickAlpha),
602         disabledActiveTickColor: Color = activeTickColor.copy(alpha = DisabledTickAlpha),
603         disabledInactiveTickColor: Color =
604             disabledInactiveTrackColor.copy(alpha = DisabledTickAlpha)
605     ): SliderColors =
606         DefaultSliderColors(
607             thumbColor = thumbColor,
608             disabledThumbColor = disabledThumbColor,
609             activeTrackColor = activeTrackColor,
610             inactiveTrackColor = inactiveTrackColor,
611             disabledActiveTrackColor = disabledActiveTrackColor,
612             disabledInactiveTrackColor = disabledInactiveTrackColor,
613             activeTickColor = activeTickColor,
614             inactiveTickColor = inactiveTickColor,
615             disabledActiveTickColor = disabledActiveTickColor,
616             disabledInactiveTickColor = disabledInactiveTickColor
617         )
618 
619     /** Default alpha of the inactive part of the track */
620     const val InactiveTrackAlpha = 0.24f
621 
622     /** Default alpha for the track when it is disabled but active */
623     const val DisabledInactiveTrackAlpha = 0.12f
624 
625     /** Default alpha for the track when it is disabled and inactive */
626     const val DisabledActiveTrackAlpha = 0.32f
627 
628     /** Default alpha of the ticks that are drawn on top of the track */
629     const val TickAlpha = 0.54f
630 
631     /** Default alpha for tick marks when they are disabled */
632     const val DisabledTickAlpha = 0.12f
633 }
634 
635 /**
636  * Represents the colors used by a [Slider] and its parts in different states
637  *
638  * See [SliderDefaults.colors] for the default implementation that follows Material specifications.
639  */
640 @Stable
641 interface SliderColors {
642 
643     /**
644      * Represents the color used for the sliders's thumb, depending on [enabled].
645      *
646      * @param enabled whether the [Slider] is enabled or not
647      */
648     @Composable fun thumbColor(enabled: Boolean): State<Color>
649 
650     /**
651      * Represents the color used for the sliders's track, depending on [enabled] and [active].
652      *
653      * Active part is filled with progress, so if sliders progress is 30% out of 100%, left (or
654      * right in RTL) 30% of the track will be active, the rest is not active.
655      *
656      * @param enabled whether the [Slider] is enabled or not
657      * @param active whether the part of the track is active of not
658      */
659     @Composable fun trackColor(enabled: Boolean, active: Boolean): State<Color>
660 
661     /**
662      * Represents the color used for the sliders's tick which is the dot separating steps, if they
663      * are set on the slider, depending on [enabled] and [active].
664      *
665      * Active tick is the tick that is in the part of the track filled with progress, so if sliders
666      * progress is 30% out of 100%, left (or right in RTL) 30% of the track and the ticks in this
667      * 30% will be active, the rest is not active.
668      *
669      * @param enabled whether the [Slider] is enabled or not
670      * @param active whether the part of the track this tick is in is active of not
671      */
672     @Composable fun tickColor(enabled: Boolean, active: Boolean): State<Color>
673 }
674 
675 @Composable
SliderImplnull676 private fun SliderImpl(
677     enabled: Boolean,
678     positionFraction: Float,
679     tickFractions: List<Float>,
680     colors: SliderColors,
681     width: Float,
682     interactionSource: MutableInteractionSource,
683     modifier: Modifier
684 ) {
685     Box(modifier.then(DefaultSliderConstraints)) {
686         val trackStrokeWidth: Float
687         val thumbPx: Float
688         val widthDp: Dp
689         with(LocalDensity.current) {
690             trackStrokeWidth = TrackHeight.toPx()
691             thumbPx = ThumbRadius.toPx()
692             widthDp = width.toDp()
693         }
694 
695         val thumbSize = ThumbRadius * 2
696         val offset = widthDp * positionFraction
697 
698         Track(
699             Modifier.fillMaxSize(),
700             colors,
701             enabled,
702             0f,
703             positionFraction,
704             tickFractions,
705             thumbPx,
706             trackStrokeWidth
707         )
708         SliderThumb(Modifier, offset, interactionSource, colors, enabled, thumbSize)
709     }
710 }
711 
712 @Composable
RangeSliderImplnull713 private fun RangeSliderImpl(
714     enabled: Boolean,
715     positionFractionStart: Float,
716     positionFractionEnd: Float,
717     tickFractions: List<Float>,
718     colors: SliderColors,
719     width: Float,
720     startInteractionSource: MutableInteractionSource,
721     endInteractionSource: MutableInteractionSource,
722     modifier: Modifier,
723     startThumbSemantics: Modifier,
724     endThumbSemantics: Modifier
725 ) {
726 
727     val startContentDescription = getString(Strings.SliderRangeStart)
728     val endContentDescription = getString(Strings.SliderRangeEnd)
729     Box(modifier.then(DefaultSliderConstraints)) {
730         val trackStrokeWidth: Float
731         val thumbPx: Float
732         val widthDp: Dp
733         with(LocalDensity.current) {
734             trackStrokeWidth = TrackHeight.toPx()
735             thumbPx = ThumbRadius.toPx()
736             widthDp = width.toDp()
737         }
738 
739         val thumbSize = ThumbRadius * 2
740         val offsetStart = widthDp * positionFractionStart
741         val offsetEnd = widthDp * positionFractionEnd
742         Track(
743             Modifier.align(Alignment.CenterStart).fillMaxSize(),
744             colors,
745             enabled,
746             positionFractionStart,
747             positionFractionEnd,
748             tickFractions,
749             thumbPx,
750             trackStrokeWidth
751         )
752 
753         SliderThumb(
754             Modifier.semantics(mergeDescendants = true) {
755                     contentDescription = startContentDescription
756                 }
757                 .focusable(true, startInteractionSource)
758                 .then(startThumbSemantics),
759             offsetStart,
760             startInteractionSource,
761             colors,
762             enabled,
763             thumbSize
764         )
765         SliderThumb(
766             Modifier.semantics(mergeDescendants = true) {
767                     contentDescription = endContentDescription
768                 }
769                 .focusable(true, endInteractionSource)
770                 .then(endThumbSemantics),
771             offsetEnd,
772             endInteractionSource,
773             colors,
774             enabled,
775             thumbSize
776         )
777     }
778 }
779 
780 @Composable
SliderThumbnull781 private fun BoxScope.SliderThumb(
782     modifier: Modifier,
783     offset: Dp,
784     interactionSource: MutableInteractionSource,
785     colors: SliderColors,
786     enabled: Boolean,
787     thumbSize: Dp
788 ) {
789     Box(Modifier.padding(start = offset).align(Alignment.CenterStart)) {
790         val interactions = remember { mutableStateListOf<Interaction>() }
791         LaunchedEffect(interactionSource) {
792             interactionSource.interactions.collect { interaction ->
793                 when (interaction) {
794                     is PressInteraction.Press -> interactions.add(interaction)
795                     is PressInteraction.Release -> interactions.remove(interaction.press)
796                     is PressInteraction.Cancel -> interactions.remove(interaction.press)
797                     is DragInteraction.Start -> interactions.add(interaction)
798                     is DragInteraction.Stop -> interactions.remove(interaction.start)
799                     is DragInteraction.Cancel -> interactions.remove(interaction.start)
800                 }
801             }
802         }
803 
804         val elevation =
805             if (interactions.isNotEmpty()) {
806                 ThumbPressedElevation
807             } else {
808                 ThumbDefaultElevation
809             }
810         Spacer(
811             modifier
812                 .size(thumbSize, thumbSize)
813                 .indication(
814                     interactionSource = interactionSource,
815                     indication = ripple(bounded = false, radius = ThumbRippleRadius)
816                 )
817                 .hoverable(interactionSource = interactionSource)
818                 .shadow(if (enabled) elevation else 0.dp, CircleShape, clip = false)
819                 .background(colors.thumbColor(enabled).value, CircleShape)
820         )
821     }
822 }
823 
824 @Composable
Tracknull825 private fun Track(
826     modifier: Modifier,
827     colors: SliderColors,
828     enabled: Boolean,
829     positionFractionStart: Float,
830     positionFractionEnd: Float,
831     tickFractions: List<Float>,
832     thumbPx: Float,
833     trackStrokeWidth: Float
834 ) {
835     val inactiveTrackColor = colors.trackColor(enabled, active = false)
836     val activeTrackColor = colors.trackColor(enabled, active = true)
837     val inactiveTickColor = colors.tickColor(enabled, active = false)
838     val activeTickColor = colors.tickColor(enabled, active = true)
839     Canvas(modifier) {
840         val isRtl = layoutDirection == LayoutDirection.Rtl
841         val sliderLeft = Offset(thumbPx, center.y)
842         val sliderRight = Offset(size.width - thumbPx, center.y)
843         val sliderStart = if (isRtl) sliderRight else sliderLeft
844         val sliderEnd = if (isRtl) sliderLeft else sliderRight
845         drawLine(
846             inactiveTrackColor.value,
847             sliderStart,
848             sliderEnd,
849             trackStrokeWidth,
850             StrokeCap.Round
851         )
852         val sliderValueEnd =
853             Offset(sliderStart.x + (sliderEnd.x - sliderStart.x) * positionFractionEnd, center.y)
854 
855         val sliderValueStart =
856             Offset(sliderStart.x + (sliderEnd.x - sliderStart.x) * positionFractionStart, center.y)
857 
858         drawLine(
859             activeTrackColor.value,
860             sliderValueStart,
861             sliderValueEnd,
862             trackStrokeWidth,
863             StrokeCap.Round
864         )
865         @Suppress("ListIterator")
866         tickFractions
867             .groupBy { it > positionFractionEnd || it < positionFractionStart }
868             .forEach { (outsideFraction, list) ->
869                 drawPoints(
870                     list.fastMap { Offset(lerp(sliderStart, sliderEnd, it).x, center.y) },
871                     PointMode.Points,
872                     (if (outsideFraction) inactiveTickColor else activeTickColor).value,
873                     trackStrokeWidth,
874                     StrokeCap.Round
875                 )
876             }
877     }
878 }
879 
snapValueToTicknull880 private fun snapValueToTick(
881     current: Float,
882     tickFractions: List<Float>,
883     minPx: Float,
884     maxPx: Float
885 ): Float {
886     // target is a closest anchor to the `current`, if exists
887     return tickFractions
888         .fastMinByOrNull { abs(lerp(minPx, maxPx, it) - current) }
889         ?.run { lerp(minPx, maxPx, this) } ?: current
890 }
891 
awaitSlopnull892 private suspend fun AwaitPointerEventScope.awaitSlop(
893     id: PointerId,
894     type: PointerType
895 ): Pair<PointerInputChange, Float>? {
896     var initialDelta = 0f
897     val postPointerSlop = { pointerInput: PointerInputChange, offset: Float ->
898         pointerInput.consume()
899         initialDelta = offset
900     }
901     val afterSlopResult = awaitHorizontalPointerSlopOrCancellation(id, type, postPointerSlop)
902     return if (afterSlopResult != null) afterSlopResult to initialDelta else null
903 }
904 
stepsToTickFractionsnull905 private fun stepsToTickFractions(steps: Int): List<Float> {
906     return if (steps == 0) emptyList() else List(steps + 2) { it.toFloat() / (steps + 1) }
907 }
908 
909 // Scale x1 from a1..b1 range to a2..b2 range
scalenull910 private fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) =
911     lerp(a2, b2, calcFraction(a1, b1, x1))
912 
913 // Scale x.start, x.endInclusive from a1..b1 range to a2..b2 range
914 private fun scale(a1: Float, b1: Float, x: ClosedFloatingPointRange<Float>, a2: Float, b2: Float) =
915     scale(a1, b1, x.start, a2, b2)..scale(a1, b1, x.endInclusive, a2, b2)
916 
917 // Calculate the 0..1 fraction that `pos` value represents between `a` and `b`
918 private fun calcFraction(a: Float, b: Float, pos: Float) =
919     (if (b - a == 0f) 0f else (pos - a) / (b - a)).fastCoerceIn(0f, 1f)
920 
921 @Composable
922 private fun CorrectValueSideEffect(
923     scaleToOffset: (Float) -> Float,
924     valueRange: ClosedFloatingPointRange<Float>,
925     trackRange: ClosedFloatingPointRange<Float>,
926     valueState: MutableState<Float>,
927     value: Float
928 ) {
929     SideEffect {
930         val error = (valueRange.endInclusive - valueRange.start) / 1000
931         val newOffset = scaleToOffset(value)
932         if (abs(newOffset - valueState.value) > error) {
933             if (valueState.value in trackRange) {
934                 valueState.value = newOffset
935             }
936         }
937     }
938 }
939 
sliderSemanticsnull940 private fun Modifier.sliderSemantics(
941     value: Float,
942     enabled: Boolean,
943     onValueChange: (Float) -> Unit,
944     onValueChangeFinished: (() -> Unit)? = null,
945     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
946     steps: Int = 0
947 ): Modifier {
948     val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive)
949     return semantics {
950             if (!enabled) disabled()
951             setProgress(
952                 action = { targetValue ->
953                     var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive)
954                     val originalVal = newValue
955                     val resolvedValue =
956                         if (steps > 0) {
957                             var distance: Float = newValue
958                             for (i in 0..steps + 1) {
959                                 val stepValue =
960                                     lerp(
961                                         valueRange.start,
962                                         valueRange.endInclusive,
963                                         i.toFloat() / (steps + 1)
964                                     )
965                                 if (abs(stepValue - originalVal) <= distance) {
966                                     distance = abs(stepValue - originalVal)
967                                     newValue = stepValue
968                                 }
969                             }
970                             newValue
971                         } else {
972                             newValue
973                         }
974                     // This is to keep it consistent with AbsSeekbar.java: return false if no
975                     // change from current.
976                     if (resolvedValue == coerced) {
977                         false
978                     } else {
979                         onValueChange(resolvedValue)
980                         onValueChangeFinished?.invoke()
981                         true
982                     }
983                 }
984             )
985         }
986         .progressSemantics(value, valueRange, steps)
987 }
988 
sliderTapModifiernull989 private fun Modifier.sliderTapModifier(
990     draggableState: DraggableState,
991     interactionSource: MutableInteractionSource,
992     maxPx: Float,
993     isRtl: Boolean,
994     rawOffset: State<Float>,
995     gestureEndAction: State<(Float) -> Unit>,
996     pressOffset: MutableState<Float>,
997     enabled: Boolean
998 ) =
999     composed(
1000         factory = {
1001             if (enabled) {
1002                 val scope = rememberCoroutineScope()
1003                 pointerInput(draggableState, interactionSource, maxPx, isRtl) {
1004                     detectTapGestures(
1005                         onPress = { pos ->
1006                             val to = if (isRtl) maxPx - pos.x else pos.x
1007                             pressOffset.value = to - rawOffset.value
1008                             try {
1009                                 awaitRelease()
1010                             } catch (_: GestureCancellationException) {
1011                                 pressOffset.value = 0f
1012                             }
1013                         },
1014                         onTap = {
1015                             scope.launch {
1016                                 draggableState.drag(MutatePriority.UserInput) {
1017                                     // just trigger animation, press offset will be applied
1018                                     dragBy(0f)
1019                                 }
1020                                 gestureEndAction.value.invoke(0f)
1021                             }
1022                         }
1023                     )
1024                 }
1025             } else {
1026                 this
1027             }
1028         },
1029         inspectorInfo =
<lambda>null1030             debugInspectorInfo {
1031                 name = "sliderTapModifier"
1032                 properties["draggableState"] = draggableState
1033                 properties["interactionSource"] = interactionSource
1034                 properties["maxPx"] = maxPx
1035                 properties["isRtl"] = isRtl
1036                 properties["rawOffset"] = rawOffset
1037                 properties["gestureEndAction"] = gestureEndAction
1038                 properties["pressOffset"] = pressOffset
1039                 properties["enabled"] = enabled
1040             }
1041     )
1042 
animateToTargetnull1043 private suspend fun animateToTarget(
1044     draggableState: DraggableState,
1045     current: Float,
1046     target: Float,
1047     velocity: Float
1048 ) {
1049     draggableState.drag {
1050         var latestValue = current
1051         Animatable(initialValue = current).animateTo(target, SliderToTickAnimation, velocity) {
1052             dragBy(this.value - latestValue)
1053             latestValue = this.value
1054         }
1055     }
1056 }
1057 
rangeSliderPressDragModifiernull1058 private fun Modifier.rangeSliderPressDragModifier(
1059     startInteractionSource: MutableInteractionSource,
1060     endInteractionSource: MutableInteractionSource,
1061     rawOffsetStart: State<Float>,
1062     rawOffsetEnd: State<Float>,
1063     enabled: Boolean,
1064     isRtl: Boolean,
1065     maxPx: Float,
1066     valueRange: ClosedFloatingPointRange<Float>,
1067     gestureEndAction: State<(Boolean) -> Unit>,
1068     onDrag: State<(Boolean, Float) -> Unit>,
1069 ): Modifier =
1070     if (enabled) {
1071         pointerInput(startInteractionSource, endInteractionSource, maxPx, isRtl, valueRange) {
1072             val rangeSliderLogic =
1073                 RangeSliderLogic(
1074                     startInteractionSource,
1075                     endInteractionSource,
1076                     rawOffsetStart,
1077                     rawOffsetEnd,
1078                     onDrag
1079                 )
1080             coroutineScope {
1081                 awaitEachGesture {
1082                     val event = awaitFirstDown(requireUnconsumed = false)
1083                     val interaction = DragInteraction.Start()
1084                     var posX = if (isRtl) maxPx - event.position.x else event.position.x
1085                     val compare = rangeSliderLogic.compareOffsets(posX)
1086                     var draggingStart =
1087                         if (compare != 0) {
1088                             compare < 0
1089                         } else {
1090                             rawOffsetStart.value > posX
1091                         }
1092 
1093                     awaitSlop(event.id, event.type)?.let {
1094                         val slop = viewConfiguration.pointerSlop(event.type)
1095                         val shouldUpdateCapturedThumb =
1096                             abs(rawOffsetEnd.value - posX) < slop &&
1097                                 abs(rawOffsetStart.value - posX) < slop
1098                         if (shouldUpdateCapturedThumb) {
1099                             val dir = it.second
1100                             draggingStart = if (isRtl) dir >= 0f else dir < 0f
1101                             posX += it.first.positionChange().x
1102                         }
1103                     }
1104 
1105                     rangeSliderLogic.captureThumb(
1106                         draggingStart,
1107                         posX,
1108                         interaction,
1109                         this@coroutineScope
1110                     )
1111 
1112                     val finishInteraction =
1113                         try {
1114                             val success =
1115                                 horizontalDrag(pointerId = event.id) {
1116                                     val deltaX = it.positionChange().x
1117                                     onDrag.value.invoke(
1118                                         draggingStart,
1119                                         if (isRtl) -deltaX else deltaX
1120                                     )
1121                                 }
1122                             if (success) {
1123                                 DragInteraction.Stop(interaction)
1124                             } else {
1125                                 DragInteraction.Cancel(interaction)
1126                             }
1127                         } catch (e: CancellationException) {
1128                             DragInteraction.Cancel(interaction)
1129                         }
1130 
1131                     gestureEndAction.value.invoke(draggingStart)
1132                     launch {
1133                         rangeSliderLogic.activeInteraction(draggingStart).emit(finishInteraction)
1134                     }
1135                 }
1136             }
1137         }
1138     } else {
1139         this
1140     }
1141 
1142 private class RangeSliderLogic(
1143     val startInteractionSource: MutableInteractionSource,
1144     val endInteractionSource: MutableInteractionSource,
1145     val rawOffsetStart: State<Float>,
1146     val rawOffsetEnd: State<Float>,
1147     val onDrag: State<(Boolean, Float) -> Unit>,
1148 ) {
activeInteractionnull1149     fun activeInteraction(draggingStart: Boolean): MutableInteractionSource =
1150         if (draggingStart) startInteractionSource else endInteractionSource
1151 
1152     fun compareOffsets(eventX: Float): Int {
1153         val diffStart = abs(rawOffsetStart.value - eventX)
1154         val diffEnd = abs(rawOffsetEnd.value - eventX)
1155         return diffStart.compareTo(diffEnd)
1156     }
1157 
captureThumbnull1158     fun captureThumb(
1159         draggingStart: Boolean,
1160         posX: Float,
1161         interaction: Interaction,
1162         scope: CoroutineScope
1163     ) {
1164         onDrag.value.invoke(
1165             draggingStart,
1166             posX - if (draggingStart) rawOffsetStart.value else rawOffsetEnd.value
1167         )
1168         scope.launch { activeInteraction(draggingStart).emit(interaction) }
1169     }
1170 }
1171 
1172 @Immutable
1173 private class DefaultSliderColors(
1174     private val thumbColor: Color,
1175     private val disabledThumbColor: Color,
1176     private val activeTrackColor: Color,
1177     private val inactiveTrackColor: Color,
1178     private val disabledActiveTrackColor: Color,
1179     private val disabledInactiveTrackColor: Color,
1180     private val activeTickColor: Color,
1181     private val inactiveTickColor: Color,
1182     private val disabledActiveTickColor: Color,
1183     private val disabledInactiveTickColor: Color
1184 ) : SliderColors {
1185 
1186     @Composable
thumbColornull1187     override fun thumbColor(enabled: Boolean): State<Color> {
1188         return rememberUpdatedState(if (enabled) thumbColor else disabledThumbColor)
1189     }
1190 
1191     @Composable
trackColornull1192     override fun trackColor(enabled: Boolean, active: Boolean): State<Color> {
1193         return rememberUpdatedState(
1194             if (enabled) {
1195                 if (active) activeTrackColor else inactiveTrackColor
1196             } else {
1197                 if (active) disabledActiveTrackColor else disabledInactiveTrackColor
1198             }
1199         )
1200     }
1201 
1202     @Composable
tickColornull1203     override fun tickColor(enabled: Boolean, active: Boolean): State<Color> {
1204         return rememberUpdatedState(
1205             if (enabled) {
1206                 if (active) activeTickColor else inactiveTickColor
1207             } else {
1208                 if (active) disabledActiveTickColor else disabledInactiveTickColor
1209             }
1210         )
1211     }
1212 
equalsnull1213     override fun equals(other: Any?): Boolean {
1214         if (this === other) return true
1215         if (other == null || this::class != other::class) return false
1216 
1217         other as DefaultSliderColors
1218 
1219         if (thumbColor != other.thumbColor) return false
1220         if (disabledThumbColor != other.disabledThumbColor) return false
1221         if (activeTrackColor != other.activeTrackColor) return false
1222         if (inactiveTrackColor != other.inactiveTrackColor) return false
1223         if (disabledActiveTrackColor != other.disabledActiveTrackColor) return false
1224         if (disabledInactiveTrackColor != other.disabledInactiveTrackColor) return false
1225         if (activeTickColor != other.activeTickColor) return false
1226         if (inactiveTickColor != other.inactiveTickColor) return false
1227         if (disabledActiveTickColor != other.disabledActiveTickColor) return false
1228         if (disabledInactiveTickColor != other.disabledInactiveTickColor) return false
1229 
1230         return true
1231     }
1232 
hashCodenull1233     override fun hashCode(): Int {
1234         var result = thumbColor.hashCode()
1235         result = 31 * result + disabledThumbColor.hashCode()
1236         result = 31 * result + activeTrackColor.hashCode()
1237         result = 31 * result + inactiveTrackColor.hashCode()
1238         result = 31 * result + disabledActiveTrackColor.hashCode()
1239         result = 31 * result + disabledInactiveTrackColor.hashCode()
1240         result = 31 * result + activeTickColor.hashCode()
1241         result = 31 * result + inactiveTickColor.hashCode()
1242         result = 31 * result + disabledActiveTickColor.hashCode()
1243         result = 31 * result + disabledInactiveTickColor.hashCode()
1244         return result
1245     }
1246 }
1247 
1248 // Internal to be referred to in tests
1249 internal val ThumbRadius = 10.dp
1250 private val ThumbRippleRadius = 24.dp
1251 private val ThumbDefaultElevation = 1.dp
1252 private val ThumbPressedElevation = 6.dp
1253 
1254 // Internal to be referred to in tests
1255 internal val TrackHeight = 4.dp
1256 private val SliderHeight = 48.dp
1257 private val SliderMinWidth = 144.dp // TODO: clarify min width
1258 private val DefaultSliderConstraints =
1259     Modifier.widthIn(min = SliderMinWidth).heightIn(max = SliderHeight)
1260 
1261 private val SliderToTickAnimation = TweenSpec<Float>(durationMillis = 100)
1262 
1263 private class SliderDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
1264 
1265     var isDragging by mutableStateOf(false)
1266         private set
1267 
1268     private val dragScope: DragScope =
1269         object : DragScope {
dragBynull1270             override fun dragBy(pixels: Float): Unit = onDelta(pixels)
1271         }
1272 
1273     private val scrollMutex = MutatorMutex()
1274 
1275     override suspend fun drag(
1276         dragPriority: MutatePriority,
1277         block: suspend DragScope.() -> Unit
1278     ): Unit = coroutineScope {
1279         isDragging = true
1280         scrollMutex.mutateWith(dragScope, dragPriority, block)
1281         isDragging = false
1282     }
1283 
dispatchRawDeltanull1284     override fun dispatchRawDelta(delta: Float) {
1285         return onDelta(delta)
1286     }
1287 }
1288