1 /*
<lambda>null2 * Copyright 2022 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.material3
18
19 import androidx.annotation.IntRange
20 import androidx.compose.foundation.Canvas
21 import androidx.compose.foundation.MutatePriority
22 import androidx.compose.foundation.MutatorMutex
23 import androidx.compose.foundation.background
24 import androidx.compose.foundation.focusable
25 import androidx.compose.foundation.gestures.DragScope
26 import androidx.compose.foundation.gestures.DraggableState
27 import androidx.compose.foundation.gestures.Orientation
28 import androidx.compose.foundation.gestures.Orientation.Horizontal
29 import androidx.compose.foundation.gestures.Orientation.Vertical
30 import androidx.compose.foundation.gestures.awaitEachGesture
31 import androidx.compose.foundation.gestures.awaitFirstDown
32 import androidx.compose.foundation.gestures.detectTapGestures
33 import androidx.compose.foundation.gestures.draggable
34 import androidx.compose.foundation.gestures.horizontalDrag
35 import androidx.compose.foundation.hoverable
36 import androidx.compose.foundation.interaction.DragInteraction
37 import androidx.compose.foundation.interaction.Interaction
38 import androidx.compose.foundation.interaction.MutableInteractionSource
39 import androidx.compose.foundation.interaction.PressInteraction
40 import androidx.compose.foundation.layout.Box
41 import androidx.compose.foundation.layout.Spacer
42 import androidx.compose.foundation.layout.fillMaxHeight
43 import androidx.compose.foundation.layout.fillMaxWidth
44 import androidx.compose.foundation.layout.height
45 import androidx.compose.foundation.layout.requiredSizeIn
46 import androidx.compose.foundation.layout.size
47 import androidx.compose.foundation.layout.width
48 import androidx.compose.foundation.layout.wrapContentHeight
49 import androidx.compose.foundation.layout.wrapContentWidth
50 import androidx.compose.foundation.progressSemantics
51 import androidx.compose.material3.RangeSliderState.Companion.Saver
52 import androidx.compose.material3.SliderState.Companion.Saver
53 import androidx.compose.material3.internal.IncreaseHorizontalSemanticsBounds
54 import androidx.compose.material3.internal.Strings
55 import androidx.compose.material3.internal.awaitHorizontalPointerSlopOrCancellation
56 import androidx.compose.material3.internal.getString
57 import androidx.compose.material3.internal.pointerSlop
58 import androidx.compose.material3.tokens.SliderTokens
59 import androidx.compose.runtime.Composable
60 import androidx.compose.runtime.Immutable
61 import androidx.compose.runtime.LaunchedEffect
62 import androidx.compose.runtime.Stable
63 import androidx.compose.runtime.getValue
64 import androidx.compose.runtime.mutableFloatStateOf
65 import androidx.compose.runtime.mutableIntStateOf
66 import androidx.compose.runtime.mutableStateListOf
67 import androidx.compose.runtime.mutableStateOf
68 import androidx.compose.runtime.remember
69 import androidx.compose.runtime.saveable.Saver
70 import androidx.compose.runtime.saveable.listSaver
71 import androidx.compose.runtime.saveable.rememberSaveable
72 import androidx.compose.runtime.setValue
73 import androidx.compose.ui.Modifier
74 import androidx.compose.ui.draw.rotate
75 import androidx.compose.ui.draw.scale
76 import androidx.compose.ui.geometry.CornerRadius
77 import androidx.compose.ui.geometry.Offset
78 import androidx.compose.ui.geometry.Rect
79 import androidx.compose.ui.geometry.RoundRect
80 import androidx.compose.ui.geometry.Size
81 import androidx.compose.ui.geometry.lerp
82 import androidx.compose.ui.graphics.Color
83 import androidx.compose.ui.graphics.Path
84 import androidx.compose.ui.graphics.PointMode
85 import androidx.compose.ui.graphics.StrokeCap
86 import androidx.compose.ui.graphics.compositeOver
87 import androidx.compose.ui.graphics.drawscope.DrawScope
88 import androidx.compose.ui.graphics.takeOrElse
89 import androidx.compose.ui.input.key.Key
90 import androidx.compose.ui.input.key.KeyEventType
91 import androidx.compose.ui.input.key.key
92 import androidx.compose.ui.input.key.onKeyEvent
93 import androidx.compose.ui.input.key.type
94 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
95 import androidx.compose.ui.input.pointer.PointerId
96 import androidx.compose.ui.input.pointer.PointerInputChange
97 import androidx.compose.ui.input.pointer.PointerType
98 import androidx.compose.ui.input.pointer.pointerInput
99 import androidx.compose.ui.input.pointer.positionChange
100 import androidx.compose.ui.layout.AlignmentLine
101 import androidx.compose.ui.layout.Layout
102 import androidx.compose.ui.layout.VerticalAlignmentLine
103 import androidx.compose.ui.layout.layout
104 import androidx.compose.ui.layout.layoutId
105 import androidx.compose.ui.layout.onSizeChanged
106 import androidx.compose.ui.platform.LocalLayoutDirection
107 import androidx.compose.ui.semantics.contentDescription
108 import androidx.compose.ui.semantics.disabled
109 import androidx.compose.ui.semantics.semantics
110 import androidx.compose.ui.semantics.setProgress
111 import androidx.compose.ui.unit.Dp
112 import androidx.compose.ui.unit.DpSize
113 import androidx.compose.ui.unit.LayoutDirection
114 import androidx.compose.ui.unit.dp
115 import androidx.compose.ui.unit.offset
116 import androidx.compose.ui.util.fastFirst
117 import androidx.compose.ui.util.fastMap
118 import androidx.compose.ui.util.lerp
119 import androidx.compose.ui.util.packFloats
120 import androidx.compose.ui.util.unpackFloat1
121 import androidx.compose.ui.util.unpackFloat2
122 import kotlin.jvm.JvmInline
123 import kotlin.jvm.JvmName
124 import kotlin.math.abs
125 import kotlin.math.floor
126 import kotlin.math.max
127 import kotlin.math.min
128 import kotlin.math.roundToInt
129 import kotlinx.coroutines.CancellationException
130 import kotlinx.coroutines.CoroutineScope
131 import kotlinx.coroutines.coroutineScope
132 import kotlinx.coroutines.launch
133
134 /**
135 * [Material Design slider](https://m3.material.io/components/sliders/overview)
136 *
137 * Sliders allow users to make selections from a range of values.
138 *
139 * It uses [SliderDefaults.Thumb] and [SliderDefaults.Track] as the thumb and track.
140 *
141 * Sliders reflect a range of values along a horizontal bar, from which users may select a single
142 * value. They are ideal for adjusting settings such as volume, brightness, or applying image
143 * filters.
144 *
145 * 
147 *
148 * Use continuous sliders to allow users to make meaningful selections that don’t require a specific
149 * value:
150 *
151 * @sample androidx.compose.material3.samples.SliderSample
152 *
153 * You can allow the user to choose only between predefined set of values by specifying the amount
154 * of steps between min and max values:
155 *
156 * @sample androidx.compose.material3.samples.StepsSliderSample
157 * @param value current value of the slider. If outside of [valueRange] provided, value will be
158 * coerced to this range.
159 * @param onValueChange callback in which value should be updated
160 * @param modifier the [Modifier] to be applied to this slider
161 * @param enabled controls the enabled state of this slider. When `false`, this component will not
162 * respond to user input, and it will appear visually disabled and disabled to accessibility
163 * services.
164 * @param valueRange range of values that this slider can take. The passed [value] will be coerced
165 * to this range.
166 * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
167 * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
168 * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
169 * continuously and allow any value from the range. Must not be negative.
170 * @param onValueChangeFinished called when value change has ended. This should not be used to
171 * update the slider value (use [onValueChange] instead), but rather to know when the user has
172 * completed selecting a new value by ending a drag or a click.
173 * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
174 * different states. See [SliderDefaults.colors].
175 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
176 * for this slider. You can create and pass in your own `remember`ed instance to observe
177 * [Interaction]s and customize the appearance / behavior of this slider in different states.
178 */
179 @OptIn(ExperimentalMaterial3Api::class)
180 @Composable
181 fun Slider(
182 value: Float,
183 onValueChange: (Float) -> Unit,
184 modifier: Modifier = Modifier,
185 enabled: Boolean = true,
186 valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
187 @IntRange(from = 0) steps: Int = 0,
188 onValueChangeFinished: (() -> Unit)? = null,
189 colors: SliderColors = SliderDefaults.colors(),
190 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
191 ) {
192 Slider(
193 value = value,
194 onValueChange = onValueChange,
195 modifier = modifier,
196 enabled = enabled,
197 onValueChangeFinished = onValueChangeFinished,
198 colors = colors,
199 interactionSource = interactionSource,
200 steps = steps,
<lambda>null201 thumb = {
202 SliderDefaults.Thumb(
203 interactionSource = interactionSource,
204 colors = colors,
205 enabled = enabled
206 )
207 },
sliderStatenull208 track = { sliderState ->
209 SliderDefaults.Track(colors = colors, enabled = enabled, sliderState = sliderState)
210 },
211 valueRange = valueRange
212 )
213 }
214
215 /**
216 * [Material Design slider](https://m3.material.io/components/sliders/overview)
217 *
218 * Sliders allow users to make selections from a range of values.
219 *
220 * Sliders reflect a range of values along a horizontal bar, from which users may select a single
221 * value. They are ideal for adjusting settings such as volume, brightness, or applying image
222 * filters.
223 *
224 * 
226 *
227 * Use continuous sliders to allow users to make meaningful selections that don’t require a specific
228 * value:
229 *
230 * @sample androidx.compose.material3.samples.SliderSample
231 *
232 * You can allow the user to choose only between predefined set of values by specifying the amount
233 * of steps between min and max values:
234 *
235 * @sample androidx.compose.material3.samples.StepsSliderSample
236 *
237 * Slider using a custom thumb:
238 *
239 * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
240 *
241 * Slider using custom track and thumb:
242 *
243 * @sample androidx.compose.material3.samples.SliderWithCustomTrackAndThumbSample
244 *
245 * Slider using track icons:
246 *
247 * @sample androidx.compose.material3.samples.SliderWithTrackIconsSample
248 * @param value current value of the slider. If outside of [valueRange] provided, value will be
249 * coerced to this range.
250 * @param onValueChange callback in which value should be updated
251 * @param modifier the [Modifier] to be applied to this slider
252 * @param enabled controls the enabled state of this slider. When `false`, this component will not
253 * respond to user input, and it will appear visually disabled and disabled to accessibility
254 * services.
255 * @param onValueChangeFinished called when value change has ended. This should not be used to
256 * update the slider value (use [onValueChange] instead), but rather to know when the user has
257 * completed selecting a new value by ending a drag or a click.
258 * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
259 * different states. See [SliderDefaults.colors].
260 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
261 * for this slider. You can create and pass in your own `remember`ed instance to observe
262 * [Interaction]s and customize the appearance / behavior of this slider in different states.
263 * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
264 * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
265 * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
266 * continuously and allow any value from the range. Must not be negative.
267 * @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The
268 * lambda receives a [SliderState] which is used to obtain the current active track.
269 * @param track the track to be displayed on the slider, it is placed underneath the thumb. The
270 * lambda receives a [SliderState] which is used to obtain the current active track.
271 * @param valueRange range of values that this slider can take. The passed [value] will be coerced
272 * to this range.
273 */
274 @Composable
275 @ExperimentalMaterial3Api
Slidernull276 fun Slider(
277 value: Float,
278 onValueChange: (Float) -> Unit,
279 modifier: Modifier = Modifier,
280 enabled: Boolean = true,
281 onValueChangeFinished: (() -> Unit)? = null,
282 colors: SliderColors = SliderDefaults.colors(),
283 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
284 @IntRange(from = 0) steps: Int = 0,
285 thumb: @Composable (SliderState) -> Unit = {
286 SliderDefaults.Thumb(
287 interactionSource = interactionSource,
288 colors = colors,
289 enabled = enabled
290 )
291 },
292 track: @Composable (SliderState) -> Unit = { sliderState ->
293 SliderDefaults.Track(colors = colors, enabled = enabled, sliderState = sliderState)
294 },
295 valueRange: ClosedFloatingPointRange<Float> = 0f..1f
296 ) {
297 val state =
<lambda>null298 remember(steps, valueRange) { SliderState(value, steps, onValueChangeFinished, valueRange) }
299 state.onValueChangeFinished = onValueChangeFinished
300 state.onValueChange = onValueChange
301 state.value = value
302
303 Slider(
304 state = state,
305 modifier = modifier,
306 enabled = enabled,
307 interactionSource = interactionSource,
308 thumb = thumb,
309 track = track
310 )
311 }
312
313 /**
314 * [Material Design slider](https://m3.material.io/components/sliders/overview)
315 *
316 * Sliders allow users to make selections from a range of values.
317 *
318 * Sliders reflect a range of values along a horizontal bar, from which users may select a single
319 * value. They are ideal for adjusting settings such as volume, brightness, or applying image
320 * filters.
321 *
322 * 
324 *
325 * Use continuous sliders to allow users to make meaningful selections that don’t require a specific
326 * value:
327 *
328 * @sample androidx.compose.material3.samples.SliderSample
329 *
330 * You can allow the user to choose only between predefined set of values by specifying the amount
331 * of steps between min and max values:
332 *
333 * @sample androidx.compose.material3.samples.StepsSliderSample
334 *
335 * Slider using a custom thumb:
336 *
337 * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
338 *
339 * Slider using custom track and thumb:
340 *
341 * @sample androidx.compose.material3.samples.SliderWithCustomTrackAndThumbSample
342 *
343 * Slider using track icons:
344 *
345 * @sample androidx.compose.material3.samples.SliderWithTrackIconsSample
346 * @param state [SliderState] which contains the slider's current value.
347 * @param modifier the [Modifier] to be applied to this slider
348 * @param enabled controls the enabled state of this slider. When `false`, this component will not
349 * respond to user input, and it will appear visually disabled and disabled to accessibility
350 * services.
351 * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
352 * different states. See [SliderDefaults.colors].
353 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
354 * for this slider. You can create and pass in your own `remember`ed instance to observe
355 * [Interaction]s and customize the appearance / behavior of this slider in different states.
356 * @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The
357 * lambda receives a [SliderState] which is used to obtain the current active track.
358 * @param track the track to be displayed on the slider, it is placed underneath the thumb. The
359 * lambda receives a [SliderState] which is used to obtain the current active track.
360 */
361 @Composable
362 @ExperimentalMaterial3Api
Slidernull363 fun Slider(
364 state: SliderState,
365 modifier: Modifier = Modifier,
366 enabled: Boolean = true,
367 colors: SliderColors = SliderDefaults.colors(),
368 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
369 thumb: @Composable (SliderState) -> Unit = {
370 SliderDefaults.Thumb(
371 interactionSource = interactionSource,
372 colors = colors,
373 enabled = enabled
374 )
375 },
376 track: @Composable (SliderState) -> Unit = { sliderState ->
377 SliderDefaults.Track(colors = colors, enabled = enabled, sliderState = sliderState)
378 }
379 ) {
<lambda>null380 require(state.steps >= 0) { "steps should be >= 0" }
381
382 SliderImpl(
383 state = state,
384 modifier = modifier,
385 enabled = enabled,
386 interactionSource = interactionSource,
387 thumb = thumb,
388 track = track
389 )
390 }
391
392 /**
393 * [Material Design slider](https://m3.material.io/components/sliders/overview)
394 *
395 * Vertical Sliders allow users to make selections from a range of values.
396 *
397 * Vertical Sliders reflect a range of values along a vertical bar, from which users may select a
398 * single value. They are ideal for adjusting settings such as volume, brightness, or applying image
399 * filters.
400 *
401 * 
403 *
404 * Vertical Slider:
405 *
406 * @sample androidx.compose.material3.samples.VerticalSliderSample
407 * @param state [SliderState] which contains the slider's current value.
408 * @param modifier the [Modifier] to be applied to this slider
409 * @param enabled controls the enabled state of this slider. When `false`, this component will not
410 * respond to user input, and it will appear visually disabled and disabled to accessibility
411 * services.
412 * @param reverseDirection controls the direction of this slider. Default is top to bottom.
413 * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
414 * different states. See [SliderDefaults.colors].
415 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
416 * for this slider. You can create and pass in your own `remember`ed instance to observe
417 * [Interaction]s and customize the appearance / behavior of this slider in different states.
418 * @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The
419 * lambda receives a [SliderState] which is used to obtain the current active track.
420 * @param track the track to be displayed on the slider, it is placed underneath the thumb. The
421 * lambda receives a [SliderState] which is used to obtain the current active track.
422 */
423 @OptIn(ExperimentalMaterial3Api::class)
424 @ExperimentalMaterial3ExpressiveApi
425 @Composable
VerticalSlidernull426 fun VerticalSlider(
427 state: SliderState,
428 modifier: Modifier = Modifier,
429 enabled: Boolean = true,
430 reverseDirection: Boolean = false,
431 colors: SliderColors = SliderDefaults.colors(),
432 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
433 thumb: @Composable (SliderState) -> Unit = { sliderState ->
434 SliderDefaults.Thumb(
435 interactionSource = interactionSource,
436 sliderState = sliderState,
437 colors = colors,
438 enabled = enabled,
439 thumbSize = VerticalThumbSize
440 )
441 },
442 track: @Composable (SliderState) -> Unit = { sliderState ->
443 SliderDefaults.Track(
444 colors = colors,
445 enabled = enabled,
446 sliderState = sliderState,
447 trackCornerSize = Dp.Unspecified
448 )
449 }
450 ) {
<lambda>null451 require(state.steps >= 0) { "steps should be >= 0" }
452
453 state.orientation = Vertical
454 state.reverseVerticalDirection = reverseDirection
455
456 SliderImpl(
457 state = state,
458 modifier = modifier,
459 enabled = enabled,
460 interactionSource = interactionSource,
461 thumb = thumb,
462 track = track
463 )
464 }
465
466 /**
467 * [Material Design range slider](https://m3.material.io/components/sliders/overview)
468 *
469 * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
470 *
471 * The two values are still bounded by the value range but they also cannot cross each other.
472 *
473 * Use continuous Range Sliders to allow users to make meaningful selections that don’t require a
474 * specific values:
475 *
476 * @sample androidx.compose.material3.samples.RangeSliderSample
477 *
478 * You can allow the user to choose only between predefined set of values by specifying the amount
479 * of steps between min and max values:
480 *
481 * @sample androidx.compose.material3.samples.StepRangeSliderSample
482 * @param value current values of the RangeSlider. If either value is outside of [valueRange]
483 * provided, it will be coerced to this range.
484 * @param onValueChange lambda in which values should be updated
485 * @param modifier modifiers for the Range Slider layout
486 * @param enabled whether or not component is enabled and can we interacted with or not
487 * @param valueRange range of values that Range Slider values can take. Passed [value] will be
488 * coerced to this range
489 * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
490 * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
491 * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
492 * continuously and allow any value from the range. Must not be negative.
493 * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
494 * shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
495 * to know when the user has completed selecting a new value by ending a drag or a click.
496 * @param colors [SliderColors] that will be used to determine the color of the Range Slider parts
497 * in different state. See [SliderDefaults.colors] to customize.
498 */
499 @OptIn(ExperimentalMaterial3Api::class)
500 @Composable
RangeSlidernull501 fun RangeSlider(
502 value: ClosedFloatingPointRange<Float>,
503 onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
504 modifier: Modifier = Modifier,
505 enabled: Boolean = true,
506 valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
507 @IntRange(from = 0) steps: Int = 0,
508 onValueChangeFinished: (() -> Unit)? = null,
509 colors: SliderColors = SliderDefaults.colors()
510 ) {
511 val startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
512 val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
513
514 RangeSlider(
515 value = value,
516 onValueChange = onValueChange,
517 modifier = modifier,
518 enabled = enabled,
519 valueRange = valueRange,
520 steps = steps,
521 onValueChangeFinished = onValueChangeFinished,
522 startInteractionSource = startInteractionSource,
523 endInteractionSource = endInteractionSource,
524 startThumb = {
525 SliderDefaults.Thumb(
526 interactionSource = startInteractionSource,
527 colors = colors,
528 enabled = enabled
529 )
530 },
531 endThumb = {
532 SliderDefaults.Thumb(
533 interactionSource = endInteractionSource,
534 colors = colors,
535 enabled = enabled
536 )
537 },
538 track = { rangeSliderState ->
539 SliderDefaults.Track(
540 colors = colors,
541 enabled = enabled,
542 rangeSliderState = rangeSliderState
543 )
544 }
545 )
546 }
547
548 /**
549 * [Material Design range slider](https://m3.material.io/components/sliders/overview)
550 *
551 * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
552 *
553 * The two values are still bounded by the value range but they also cannot cross each other.
554 *
555 * It uses the provided startThumb for the slider's start thumb and endThumb for the slider's end
556 * thumb. It also uses the provided track for the slider's track. If nothing is passed for these
557 * parameters, it will use [SliderDefaults.Thumb] and [SliderDefaults.Track] for the thumbs and
558 * track.
559 *
560 * Use continuous Range Sliders to allow users to make meaningful selections that don’t require a
561 * specific values:
562 *
563 * @sample androidx.compose.material3.samples.RangeSliderSample
564 *
565 * You can allow the user to choose only between predefined set of values by specifying the amount
566 * of steps between min and max values:
567 *
568 * @sample androidx.compose.material3.samples.StepRangeSliderSample
569 *
570 * A custom start/end thumb and track can be provided:
571 *
572 * @sample androidx.compose.material3.samples.RangeSliderWithCustomComponents
573 * @param value current values of the RangeSlider. If either value is outside of [valueRange]
574 * provided, it will be coerced to this range.
575 * @param onValueChange lambda in which values should be updated
576 * @param modifier modifiers for the Range Slider layout
577 * @param enabled whether or not component is enabled and can we interacted with or not
578 * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
579 * shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
580 * to know when the user has completed selecting a new value by ending a drag or a click.
581 * @param colors [SliderColors] that will be used to determine the color of the Range Slider parts
582 * in different state. See [SliderDefaults.colors] to customize.
583 * @param startInteractionSource the [MutableInteractionSource] representing the stream of
584 * [Interaction]s for the start thumb. You can create and pass in your own `remember`ed instance
585 * to observe.
586 * @param endInteractionSource the [MutableInteractionSource] representing the stream of
587 * [Interaction]s for the end thumb. You can create and pass in your own `remember`ed instance to
588 * observe.
589 * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
590 * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
591 * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
592 * continuously and allow any value from the range. Must not be negative.
593 * @param startThumb the start thumb to be displayed on the Range Slider. The lambda receives a
594 * [RangeSliderState] which is used to obtain the current active track.
595 * @param endThumb the end thumb to be displayed on the Range Slider. The lambda receives a
596 * [RangeSliderState] which is used to obtain the current active track.
597 * @param track the track to be displayed on the range slider, it is placed underneath the thumb.
598 * The lambda receives a [RangeSliderState] which is used to obtain the current active track.
599 * @param valueRange range of values that Range Slider values can take. Passed [value] will be
600 * coerced to this range.
601 */
602 @Composable
603 @ExperimentalMaterial3Api
RangeSlidernull604 fun RangeSlider(
605 value: ClosedFloatingPointRange<Float>,
606 onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
607 modifier: Modifier = Modifier,
608 enabled: Boolean = true,
609 valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
610 onValueChangeFinished: (() -> Unit)? = null,
611 colors: SliderColors = SliderDefaults.colors(),
612 startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
613 endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
614 startThumb: @Composable (RangeSliderState) -> Unit = {
615 SliderDefaults.Thumb(
616 interactionSource = startInteractionSource,
617 colors = colors,
618 enabled = enabled
619 )
620 },
621 endThumb: @Composable (RangeSliderState) -> Unit = {
622 SliderDefaults.Thumb(
623 interactionSource = endInteractionSource,
624 colors = colors,
625 enabled = enabled
626 )
627 },
628 track: @Composable (RangeSliderState) -> Unit = { rangeSliderState ->
629 SliderDefaults.Track(
630 colors = colors,
631 enabled = enabled,
632 rangeSliderState = rangeSliderState
633 )
634 },
635 @IntRange(from = 0) steps: Int = 0
636 ) {
637 val state =
<lambda>null638 remember(steps, valueRange) {
639 RangeSliderState(
640 value.start,
641 value.endInclusive,
642 steps,
643 onValueChangeFinished,
644 valueRange
645 )
646 }
647
648 state.onValueChangeFinished = onValueChangeFinished
<lambda>null649 state.onValueChange = { onValueChange(it.start..it.endInclusive) }
650 state.activeRangeStart = value.start
651 state.activeRangeEnd = value.endInclusive
652
653 RangeSlider(
654 modifier = modifier,
655 state = state,
656 enabled = enabled,
657 startInteractionSource = startInteractionSource,
658 endInteractionSource = endInteractionSource,
659 startThumb = startThumb,
660 endThumb = endThumb,
661 track = track
662 )
663 }
664
665 /**
666 * [Material Design range slider](https://m3.material.io/components/sliders/overview)
667 *
668 * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
669 *
670 * The two values are still bounded by the value range but they also cannot cross each other.
671 *
672 * It uses the provided startThumb for the slider's start thumb and endThumb for the slider's end
673 * thumb. It also uses the provided track for the slider's track. If nothing is passed for these
674 * parameters, it will use [SliderDefaults.Thumb] and [SliderDefaults.Track] for the thumbs and
675 * track.
676 *
677 * Use continuous Range Sliders to allow users to make meaningful selections that don’t require a
678 * specific values:
679 *
680 * @sample androidx.compose.material3.samples.RangeSliderSample
681 *
682 * You can allow the user to choose only between predefined set of values by specifying the amount
683 * of steps between min and max values:
684 *
685 * @sample androidx.compose.material3.samples.StepRangeSliderSample
686 *
687 * A custom start/end thumb and track can be provided:
688 *
689 * @sample androidx.compose.material3.samples.RangeSliderWithCustomComponents
690 * @param state [RangeSliderState] which contains the current values of the RangeSlider.
691 * @param modifier modifiers for the Range Slider layout
692 * @param enabled whether or not component is enabled and can we interacted with or not
693 * @param colors [SliderColors] that will be used to determine the color of the Range Slider parts
694 * in different state. See [SliderDefaults.colors] to customize.
695 * @param startInteractionSource the [MutableInteractionSource] representing the stream of
696 * [Interaction]s for the start thumb. You can create and pass in your own `remember`ed instance
697 * to observe.
698 * @param endInteractionSource the [MutableInteractionSource] representing the stream of
699 * [Interaction]s for the end thumb. You can create and pass in your own `remember`ed instance to
700 * observe.
701 * @param startThumb the start thumb to be displayed on the Range Slider. The lambda receives a
702 * [RangeSliderState] which is used to obtain the current active track.
703 * @param endThumb the end thumb to be displayed on the Range Slider. The lambda receives a
704 * [RangeSliderState] which is used to obtain the current active track.
705 * @param track the track to be displayed on the range slider, it is placed underneath the thumb.
706 * The lambda receives a [RangeSliderState] which is used to obtain the current active track.
707 */
708 @Composable
709 @ExperimentalMaterial3Api
RangeSlidernull710 fun RangeSlider(
711 state: RangeSliderState,
712 modifier: Modifier = Modifier,
713 enabled: Boolean = true,
714 colors: SliderColors = SliderDefaults.colors(),
715 startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
716 endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
717 startThumb: @Composable (RangeSliderState) -> Unit = {
718 SliderDefaults.Thumb(
719 interactionSource = startInteractionSource,
720 colors = colors,
721 enabled = enabled
722 )
723 },
724 endThumb: @Composable (RangeSliderState) -> Unit = {
725 SliderDefaults.Thumb(
726 interactionSource = endInteractionSource,
727 colors = colors,
728 enabled = enabled
729 )
730 },
731 track: @Composable (RangeSliderState) -> Unit = { rangeSliderState ->
732 SliderDefaults.Track(
733 colors = colors,
734 enabled = enabled,
735 rangeSliderState = rangeSliderState
736 )
737 }
738 ) {
<lambda>null739 require(state.steps >= 0) { "steps should be >= 0" }
740
741 RangeSliderImpl(
742 modifier = modifier,
743 state = state,
744 enabled = enabled,
745 startInteractionSource = startInteractionSource,
746 endInteractionSource = endInteractionSource,
747 startThumb = startThumb,
748 endThumb = endThumb,
749 track = track
750 )
751 }
752
753 @OptIn(ExperimentalMaterial3Api::class)
754 @Composable
SliderImplnull755 private fun SliderImpl(
756 modifier: Modifier,
757 state: SliderState,
758 enabled: Boolean,
759 interactionSource: MutableInteractionSource,
760 thumb: @Composable (SliderState) -> Unit,
761 track: @Composable (SliderState) -> Unit
762 ) {
763 state.isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
764 val reverseDirection =
765 (state.orientation == Horizontal && state.isRtl) ||
766 (state.orientation == Vertical && state.reverseVerticalDirection)
767 val press = Modifier.sliderTapModifier(state, interactionSource, enabled)
768 val drag =
769 Modifier.draggable(
770 orientation = state.orientation,
771 reverseDirection = reverseDirection,
772 enabled = enabled,
773 interactionSource = interactionSource,
774 onDragStopped = { state.gestureEndAction() },
775 startDragImmediately = state.isDragging,
776 state = state
777 )
778 val thumbModifier =
779 if (state.orientation == Vertical) {
780 Modifier.layoutId(SliderComponents.THUMB).wrapContentHeight()
781 } else {
782 Modifier.layoutId(SliderComponents.THUMB).wrapContentWidth()
783 }
784
785 Layout(
786 {
787 Box(
788 modifier =
789 thumbModifier.onSizeChanged {
790 state.thumbWidth = it.width
791 state.thumbHeight = it.height
792 }
793 ) {
794 thumb(state)
795 }
796 Box(modifier = Modifier.layoutId(SliderComponents.TRACK)) { track(state) }
797 },
798 modifier =
799 modifier
800 .minimumInteractiveComponentSize()
801 .requiredSizeIn(
802 minWidth = if (state.orientation == Vertical) TrackHeight else ThumbWidth,
803 minHeight = if (state.orientation == Vertical) ThumbWidth else TrackHeight,
804 )
805 .sliderSemantics(state, enabled)
806 .focusable(enabled, interactionSource)
807 .slideOnKeyEvents(
808 enabled,
809 state.steps,
810 state.valueRange,
811 state.value,
812 reverseDirection,
813 state.onValueChange,
814 state.onValueChangeFinished
815 )
816 .then(press)
817 .then(drag)
818 ) { measurables, constraints ->
819 val thumbPlaceable =
820 measurables.fastFirst { it.layoutId == SliderComponents.THUMB }.measure(constraints)
821
822 val trackMeasurable = measurables.fastFirst { it.layoutId == SliderComponents.TRACK }
823 val trackPlaceable =
824 if (state.orientation == Vertical) {
825 trackMeasurable.measure(
826 constraints.offset(vertical = -thumbPlaceable.height).copy(minWidth = 0)
827 )
828 } else {
829 trackMeasurable.measure(
830 constraints.offset(horizontal = -thumbPlaceable.width).copy(minHeight = 0)
831 )
832 }
833
834 val sliderWidth: Int
835 val sliderHeight: Int
836 val trackOffsetX: Int
837 val trackOffsetY: Int
838 val thumbOffsetX: Int
839 var thumbOffsetY: Int
840 val valueAsFraction = state.coercedValueAsFraction
841 val isOnFirstOrLastStep =
842 valueAsFraction == state.tickFractions.firstOrNull() ||
843 valueAsFraction == state.tickFractions.lastOrNull()
844 var trackCornerSize =
845 trackPlaceable[CornerSizeAlignmentLine].let {
846 if (it != AlignmentLine.Unspecified) it else 0
847 }
848
849 if (layoutDirection == LayoutDirection.Rtl && trackCornerSize != 0) {
850 trackCornerSize = trackPlaceable.width - trackCornerSize
851 }
852 if (state.orientation == Vertical) {
853 sliderWidth = max(trackPlaceable.width, thumbPlaceable.width)
854 sliderHeight = thumbPlaceable.height + trackPlaceable.height
855 trackOffsetX = (sliderWidth - trackPlaceable.width) / 2
856 trackOffsetY = thumbPlaceable.height / 2
857 thumbOffsetX = (sliderWidth - thumbPlaceable.width) / 2
858 thumbOffsetY =
859 if (state.steps > 0 && !isOnFirstOrLastStep) {
860 ((trackPlaceable.height - trackCornerSize * 2) * valueAsFraction).roundToInt() +
861 trackCornerSize
862 } else {
863 (trackPlaceable.height * valueAsFraction).roundToInt()
864 }
865 if (state.reverseVerticalDirection) {
866 thumbOffsetY = trackPlaceable.height - thumbOffsetY
867 }
868 } else {
869 sliderWidth = thumbPlaceable.width + trackPlaceable.width
870 sliderHeight = max(trackPlaceable.height, thumbPlaceable.height)
871 trackOffsetX = thumbPlaceable.width / 2
872 trackOffsetY = (sliderHeight - trackPlaceable.height) / 2
873 thumbOffsetX =
874 if (state.steps > 0 && !isOnFirstOrLastStep) {
875 ((trackPlaceable.width - trackCornerSize * 2) * valueAsFraction).roundToInt() +
876 trackCornerSize
877 } else {
878 (trackPlaceable.width * valueAsFraction).roundToInt()
879 }
880 thumbOffsetY = (sliderHeight - thumbPlaceable.height) / 2
881 }
882
883 state.updateDimensions(newTotalWidth = sliderWidth, newTotalHeight = sliderHeight)
884
885 layout(sliderWidth, sliderHeight) {
886 trackPlaceable.placeRelative(trackOffsetX, trackOffsetY)
887 thumbPlaceable.placeRelative(thumbOffsetX, thumbOffsetY)
888 }
889 }
890 }
891
Modifiernull892 private fun Modifier.slideOnKeyEvents(
893 enabled: Boolean,
894 steps: Int,
895 valueRange: ClosedFloatingPointRange<Float>,
896 value: Float,
897 reverseDirection: Boolean,
898 onValueChangeState: ((Float) -> Unit)?,
899 onValueChangeFinishedState: (() -> Unit)?
900 ): Modifier {
901 require(steps >= 0) { "steps should be >= 0" }
902 return this.onKeyEvent {
903 if (!enabled) return@onKeyEvent false
904 if (onValueChangeState == null) return@onKeyEvent false
905 when (it.type) {
906 KeyEventType.KeyDown -> {
907 val rangeLength = abs(valueRange.endInclusive - valueRange.start)
908 // When steps == 0, it means that a user is not limited by a step length (delta)
909 // when using touch or mouse. But it is not possible to adjust the value
910 // continuously when using keyboard buttons - the delta has to be discrete.
911 // In this case, 1% of the valueRange seems to make sense.
912 val actualSteps = if (steps > 0) steps + 1 else 100
913 val delta = rangeLength / actualSteps
914 val sign = if (reverseDirection) -1 else 1
915 when (it.key) {
916 Key.DirectionUp -> {
917 onValueChangeState((value + sign * delta).coerceIn(valueRange))
918 true
919 }
920 Key.DirectionDown -> {
921 onValueChangeState((value - sign * delta).coerceIn(valueRange))
922 true
923 }
924 Key.DirectionRight -> {
925 onValueChangeState((value + sign * delta).coerceIn(valueRange))
926 true
927 }
928 Key.DirectionLeft -> {
929 onValueChangeState((value - sign * delta).coerceIn(valueRange))
930 true
931 }
932 Key.MoveHome -> {
933 onValueChangeState(valueRange.start)
934 true
935 }
936 Key.MoveEnd -> {
937 onValueChangeState(valueRange.endInclusive)
938 true
939 }
940 Key.PageUp -> {
941 val page = (actualSteps / 10).coerceIn(1, 10)
942 onValueChangeState((value - page * delta).coerceIn(valueRange))
943 true
944 }
945 Key.PageDown -> {
946 val page = (actualSteps / 10).coerceIn(1, 10)
947 onValueChangeState((value + page * delta).coerceIn(valueRange))
948 true
949 }
950 else -> false
951 }
952 }
953 KeyEventType.KeyUp -> {
954 when (it.key) {
955 Key.DirectionUp,
956 Key.DirectionDown,
957 Key.DirectionRight,
958 Key.DirectionLeft,
959 Key.MoveHome,
960 Key.MoveEnd,
961 Key.PageUp,
962 Key.PageDown -> {
963 onValueChangeFinishedState?.invoke()
964 true
965 }
966 else -> false
967 }
968 }
969 else -> false
970 }
971 }
972 }
973
974 @OptIn(ExperimentalMaterial3Api::class)
975 @Composable
RangeSliderImplnull976 private fun RangeSliderImpl(
977 modifier: Modifier,
978 state: RangeSliderState,
979 enabled: Boolean,
980 startInteractionSource: MutableInteractionSource,
981 endInteractionSource: MutableInteractionSource,
982 startThumb: @Composable ((RangeSliderState) -> Unit),
983 endThumb: @Composable ((RangeSliderState) -> Unit),
984 track: @Composable ((RangeSliderState) -> Unit)
985 ) {
986 state.isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
987
988 val pressDrag =
989 Modifier.rangeSliderPressDragModifier(
990 state,
991 startInteractionSource,
992 endInteractionSource,
993 enabled
994 )
995
996 val startContentDescription = getString(Strings.SliderRangeStart)
997 val endContentDescription = getString(Strings.SliderRangeEnd)
998
999 Layout(
1000 {
1001 Box(
1002 modifier =
1003 Modifier.layoutId(RangeSliderComponents.STARTTHUMB)
1004 .wrapContentWidth()
1005 .onSizeChanged {
1006 state.startThumbWidth = it.width.toFloat()
1007 state.startThumbHeight = it.height.toFloat()
1008 }
1009 .rangeSliderStartThumbSemantics(state, enabled)
1010 .semantics(mergeDescendants = true) {
1011 contentDescription = startContentDescription
1012 }
1013 .focusable(enabled, startInteractionSource)
1014 ) {
1015 startThumb(state)
1016 }
1017 Box(
1018 modifier =
1019 Modifier.layoutId(RangeSliderComponents.ENDTHUMB)
1020 .wrapContentWidth()
1021 .onSizeChanged {
1022 state.endThumbWidth = it.width.toFloat()
1023 state.endThumbHeight = it.height.toFloat()
1024 }
1025 .rangeSliderEndThumbSemantics(state, enabled)
1026 .semantics(mergeDescendants = true) {
1027 contentDescription = endContentDescription
1028 }
1029 .focusable(enabled, endInteractionSource)
1030 ) {
1031 endThumb(state)
1032 }
1033 Box(modifier = Modifier.layoutId(RangeSliderComponents.TRACK)) { track(state) }
1034 },
1035 modifier =
1036 modifier
1037 .minimumInteractiveComponentSize()
1038 .requiredSizeIn(minWidth = ThumbWidth, minHeight = TrackHeight)
1039 .then(pressDrag)
1040 ) { measurables, constraints ->
1041 val startThumbPlaceable =
1042 measurables
1043 .fastFirst { it.layoutId == RangeSliderComponents.STARTTHUMB }
1044 .measure(constraints)
1045
1046 val endThumbPlaceable =
1047 measurables
1048 .fastFirst { it.layoutId == RangeSliderComponents.ENDTHUMB }
1049 .measure(constraints)
1050
1051 val trackPlaceable =
1052 measurables
1053 .fastFirst { it.layoutId == RangeSliderComponents.TRACK }
1054 .measure(
1055 constraints
1056 .offset(
1057 horizontal = -(startThumbPlaceable.width + endThumbPlaceable.width) / 2
1058 )
1059 .copy(minHeight = 0)
1060 )
1061
1062 val sliderWidth =
1063 trackPlaceable.width + (startThumbPlaceable.width + endThumbPlaceable.width) / 2
1064 val sliderHeight =
1065 maxOf(trackPlaceable.height, startThumbPlaceable.height, endThumbPlaceable.height)
1066
1067 state.totalWidth = sliderWidth
1068
1069 state.updateMinMaxPx()
1070
1071 val startValueAsFraction = state.coercedActiveRangeStartAsFraction
1072 val isStartOnFirstOrLastStep =
1073 startValueAsFraction == state.tickFractions.firstOrNull() ||
1074 startValueAsFraction == state.tickFractions.lastOrNull()
1075 val endValueAsFraction = state.coercedActiveRangeEndAsFraction
1076 val isEndOnFirstOrLastStep =
1077 endValueAsFraction == state.tickFractions.firstOrNull() ||
1078 endValueAsFraction == state.tickFractions.lastOrNull()
1079 val trackOffsetX = startThumbPlaceable.width / 2
1080 var trackCornerSize =
1081 trackPlaceable[CornerSizeAlignmentLine].let {
1082 if (it != AlignmentLine.Unspecified) it else 0
1083 }
1084
1085 if (layoutDirection == LayoutDirection.Rtl && trackCornerSize != 0) {
1086 trackCornerSize = trackPlaceable.width - trackCornerSize
1087 }
1088
1089 val startThumbOffsetX =
1090 if (state.steps > 0 && !isStartOnFirstOrLastStep) {
1091 ((trackPlaceable.width - trackCornerSize * 2) * startValueAsFraction).roundToInt() +
1092 trackCornerSize
1093 } else {
1094 (trackPlaceable.width * startValueAsFraction).roundToInt()
1095 }
1096 // When start thumb and end thumb have different widths,
1097 // we need to add a correction for the centering of the slider.
1098 val endCorrection = (startThumbPlaceable.width - endThumbPlaceable.width) / 2
1099 val endThumbOffsetX =
1100 if (state.steps > 0 && !isEndOnFirstOrLastStep) {
1101 ((trackPlaceable.width - trackCornerSize * 2) * endValueAsFraction + endCorrection)
1102 .roundToInt() + trackCornerSize
1103 } else {
1104 (trackPlaceable.width * endValueAsFraction + endCorrection).roundToInt()
1105 }
1106 val trackOffsetY = (sliderHeight - trackPlaceable.height) / 2
1107 val startThumbOffsetY = (sliderHeight - startThumbPlaceable.height) / 2
1108 val endThumbOffsetY = (sliderHeight - endThumbPlaceable.height) / 2
1109
1110 layout(sliderWidth, sliderHeight) {
1111 trackPlaceable.placeRelative(trackOffsetX, trackOffsetY)
1112 startThumbPlaceable.placeRelative(startThumbOffsetX, startThumbOffsetY)
1113 endThumbPlaceable.placeRelative(endThumbOffsetX, endThumbOffsetY)
1114 }
1115 }
1116 }
1117
1118 /** Object to hold defaults used by [Slider] */
1119 @Stable
1120 object SliderDefaults {
1121
1122 /**
1123 * Creates a [SliderColors] that represents the different colors used in parts of the [Slider]
1124 * in different states.
1125 */
colorsnull1126 @Composable fun colors() = MaterialTheme.colorScheme.defaultSliderColors
1127
1128 /**
1129 * Creates a [SliderColors] that represents the different colors used in parts of the [Slider]
1130 * in different states.
1131 *
1132 * For the name references below the words "active" and "inactive" are used. Active part of the
1133 * slider is filled with progress, so if slider's progress is 30% out of 100%, left (or right in
1134 * RTL) 30% of the track will be active, while the rest is inactive.
1135 *
1136 * @param thumbColor thumb color when enabled
1137 * @param activeTrackColor color of the track in the part that is "active", meaning that the
1138 * thumb is ahead of it
1139 * @param activeTickColor colors to be used to draw tick marks on the active track, if `steps`
1140 * is specified
1141 * @param inactiveTrackColor color of the track in the part that is "inactive", meaning that the
1142 * thumb is before it
1143 * @param inactiveTickColor colors to be used to draw tick marks on the inactive track, if
1144 * `steps` are specified on the Slider is specified
1145 * @param disabledThumbColor thumb colors when disabled
1146 * @param disabledActiveTrackColor color of the track in the "active" part when the Slider is
1147 * disabled
1148 * @param disabledActiveTickColor colors to be used to draw tick marks on the active track when
1149 * Slider is disabled and when `steps` are specified on it
1150 * @param disabledInactiveTrackColor color of the track in the "inactive" part when the Slider
1151 * is disabled
1152 * @param disabledInactiveTickColor colors to be used to draw tick marks on the inactive part of
1153 * the track when Slider is disabled and when `steps` are specified on it
1154 */
1155 @Composable
1156 fun colors(
1157 thumbColor: Color = Color.Unspecified,
1158 activeTrackColor: Color = Color.Unspecified,
1159 activeTickColor: Color = Color.Unspecified,
1160 inactiveTrackColor: Color = Color.Unspecified,
1161 inactiveTickColor: Color = Color.Unspecified,
1162 disabledThumbColor: Color = Color.Unspecified,
1163 disabledActiveTrackColor: Color = Color.Unspecified,
1164 disabledActiveTickColor: Color = Color.Unspecified,
1165 disabledInactiveTrackColor: Color = Color.Unspecified,
1166 disabledInactiveTickColor: Color = Color.Unspecified
1167 ): SliderColors =
1168 MaterialTheme.colorScheme.defaultSliderColors.copy(
1169 thumbColor = thumbColor,
1170 activeTrackColor = activeTrackColor,
1171 activeTickColor = activeTickColor,
1172 inactiveTrackColor = inactiveTrackColor,
1173 inactiveTickColor = inactiveTickColor,
1174 disabledThumbColor = disabledThumbColor,
1175 disabledActiveTrackColor = disabledActiveTrackColor,
1176 disabledActiveTickColor = disabledActiveTickColor,
1177 disabledInactiveTrackColor = disabledInactiveTrackColor,
1178 disabledInactiveTickColor = disabledInactiveTickColor
1179 )
1180
1181 internal val ColorScheme.defaultSliderColors: SliderColors
1182 get() {
1183 return defaultSliderColorsCached
1184 ?: SliderColors(
1185 thumbColor = fromToken(SliderTokens.HandleColor),
1186 activeTrackColor = fromToken(SliderTokens.ActiveTrackColor),
1187 activeTickColor = fromToken(SliderTokens.InactiveTrackColor),
1188 inactiveTrackColor = fromToken(SliderTokens.InactiveTrackColor),
1189 inactiveTickColor = fromToken(SliderTokens.ActiveTrackColor),
1190 disabledThumbColor =
1191 fromToken(SliderTokens.DisabledHandleColor)
1192 .copy(alpha = SliderTokens.DisabledHandleOpacity)
1193 .compositeOver(surface),
1194 disabledActiveTrackColor =
1195 fromToken(SliderTokens.DisabledActiveTrackColor)
1196 .copy(alpha = SliderTokens.DisabledActiveTrackOpacity),
1197 disabledActiveTickColor =
1198 fromToken(SliderTokens.DisabledInactiveTrackColor)
1199 .copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),
1200 disabledInactiveTrackColor =
1201 fromToken(SliderTokens.DisabledInactiveTrackColor)
1202 .copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),
1203 disabledInactiveTickColor =
1204 fromToken(SliderTokens.DisabledActiveTrackColor)
1205 .copy(alpha = SliderTokens.DisabledActiveTrackOpacity)
1206 )
1207 .also { defaultSliderColorsCached = it }
1208 }
1209
1210 /**
1211 * The Default thumb for [Slider] and [RangeSlider]
1212 *
1213 * @param interactionSource the [MutableInteractionSource] representing the stream of
1214 * [Interaction]s for this thumb. You can create and pass in your own `remember`ed instance to
1215 * observe
1216 * @param modifier the [Modifier] to be applied to the thumb.
1217 * @param colors [SliderColors] that will be used to resolve the colors used for this thumb in
1218 * different states. See [SliderDefaults.colors].
1219 * @param enabled controls the enabled state of this slider. When `false`, this component will
1220 * not respond to user input, and it will appear visually disabled and disabled to
1221 * accessibility services.
1222 * @param thumbSize the size of the thumb.
1223 */
1224 @Composable
Thumbnull1225 fun Thumb(
1226 interactionSource: MutableInteractionSource,
1227 modifier: Modifier = Modifier,
1228 colors: SliderColors = colors(),
1229 enabled: Boolean = true,
1230 thumbSize: DpSize = ThumbSize
1231 ) {
1232 val interactions = remember { mutableStateListOf<Interaction>() }
1233 LaunchedEffect(interactionSource) {
1234 interactionSource.interactions.collect { interaction ->
1235 when (interaction) {
1236 is PressInteraction.Press -> interactions.add(interaction)
1237 is PressInteraction.Release -> interactions.remove(interaction.press)
1238 is PressInteraction.Cancel -> interactions.remove(interaction.press)
1239 is DragInteraction.Start -> interactions.add(interaction)
1240 is DragInteraction.Stop -> interactions.remove(interaction.start)
1241 is DragInteraction.Cancel -> interactions.remove(interaction.start)
1242 }
1243 }
1244 }
1245
1246 val size =
1247 if (interactions.isNotEmpty()) {
1248 thumbSize.copy(width = thumbSize.width / 2)
1249 } else {
1250 thumbSize
1251 }
1252 Spacer(
1253 modifier
1254 .size(size)
1255 .hoverable(interactionSource = interactionSource)
1256 .background(colors.thumbColor(enabled), SliderTokens.HandleShape.value)
1257 )
1258 }
1259
1260 /**
1261 * The Default thumb for [Slider], [VerticalSlider] and [RangeSlider]
1262 *
1263 * @param interactionSource the [MutableInteractionSource] representing the stream of
1264 * [Interaction]s for this thumb. You can create and pass in your own `remember`ed instance to
1265 * observe
1266 * @param sliderState [SliderState] which is used to obtain the current active track.
1267 * @param modifier the [Modifier] to be applied to the thumb.
1268 * @param colors [SliderColors] that will be used to resolve the colors used for this thumb in
1269 * different states. See [SliderDefaults.colors].
1270 * @param enabled controls the enabled state of this slider. When `false`, this component will
1271 * not respond to user input, and it will appear visually disabled and disabled to
1272 * accessibility services.
1273 * @param thumbSize the size of the thumb.
1274 */
1275 @OptIn(ExperimentalMaterial3Api::class)
1276 @ExperimentalMaterial3ExpressiveApi
1277 @Composable
Thumbnull1278 fun Thumb(
1279 interactionSource: MutableInteractionSource,
1280 sliderState: SliderState,
1281 modifier: Modifier = Modifier,
1282 colors: SliderColors = colors(),
1283 enabled: Boolean = true,
1284 thumbSize: DpSize = ThumbSize
1285 ) {
1286 val interactions = remember { mutableStateListOf<Interaction>() }
1287 LaunchedEffect(interactionSource) {
1288 interactionSource.interactions.collect { interaction ->
1289 when (interaction) {
1290 is PressInteraction.Press -> interactions.add(interaction)
1291 is PressInteraction.Release -> interactions.remove(interaction.press)
1292 is PressInteraction.Cancel -> interactions.remove(interaction.press)
1293 is DragInteraction.Start -> interactions.add(interaction)
1294 is DragInteraction.Stop -> interactions.remove(interaction.start)
1295 is DragInteraction.Cancel -> interactions.remove(interaction.start)
1296 }
1297 }
1298 }
1299
1300 val size =
1301 if (interactions.isNotEmpty()) {
1302 if (sliderState.orientation == Vertical) {
1303 thumbSize.copy(height = thumbSize.height / 2)
1304 } else {
1305 thumbSize.copy(width = thumbSize.width / 2)
1306 }
1307 } else {
1308 thumbSize
1309 }
1310 Spacer(
1311 modifier
1312 .size(size)
1313 .hoverable(interactionSource = interactionSource)
1314 .background(colors.thumbColor(enabled), SliderTokens.HandleShape.value)
1315 )
1316 }
1317
1318 /**
1319 * The Default track for [Slider] and [RangeSlider]
1320 *
1321 * @param sliderPositions [SliderPositions] which is used to obtain the current active track and
1322 * the tick positions if the slider is discrete.
1323 * @param modifier the [Modifier] to be applied to the track.
1324 * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1325 * different states. See [SliderDefaults.colors].
1326 * @param enabled controls the enabled state of this slider. When `false`, this component will
1327 * not respond to user input, and it will appear visually disabled and disabled to
1328 * accessibility services.
1329 */
1330 @Suppress("DEPRECATION")
1331 @Composable
1332 @Deprecated("Use version that supports slider state")
Tracknull1333 fun Track(
1334 sliderPositions: SliderPositions,
1335 modifier: Modifier = Modifier,
1336 colors: SliderColors = colors(),
1337 enabled: Boolean = true,
1338 ) {
1339 val inactiveTrackColor = colors.trackColor(enabled, active = false)
1340 val activeTrackColor = colors.trackColor(enabled, active = true)
1341 val inactiveTickColor = colors.tickColor(enabled, active = false)
1342 val activeTickColor = colors.tickColor(enabled, active = true)
1343 Canvas(modifier.fillMaxWidth().height(TrackHeight)) {
1344 val isRtl = layoutDirection == LayoutDirection.Rtl
1345 val sliderLeft = Offset(0f, center.y)
1346 val sliderRight = Offset(size.width, center.y)
1347 val sliderStart = if (isRtl) sliderRight else sliderLeft
1348 val sliderEnd = if (isRtl) sliderLeft else sliderRight
1349 val tickSize = TickSize.toPx()
1350 val trackStrokeWidth = TrackHeight.toPx()
1351 drawLine(inactiveTrackColor, sliderStart, sliderEnd, trackStrokeWidth, StrokeCap.Round)
1352 val sliderValueEnd =
1353 Offset(
1354 sliderStart.x +
1355 (sliderEnd.x - sliderStart.x) * sliderPositions.activeRange.endInclusive,
1356 center.y
1357 )
1358
1359 val sliderValueStart =
1360 Offset(
1361 sliderStart.x +
1362 (sliderEnd.x - sliderStart.x) * sliderPositions.activeRange.start,
1363 center.y
1364 )
1365
1366 drawLine(
1367 activeTrackColor,
1368 sliderValueStart,
1369 sliderValueEnd,
1370 trackStrokeWidth,
1371 StrokeCap.Round
1372 )
1373 sliderPositions.tickFractions
1374 .groupBy {
1375 it > sliderPositions.activeRange.endInclusive ||
1376 it < sliderPositions.activeRange.start
1377 }
1378 .forEach { (outsideFraction, list) ->
1379 drawPoints(
1380 list.fastMap { Offset(lerp(sliderStart, sliderEnd, it).x, center.y) },
1381 PointMode.Points,
1382 (if (outsideFraction) inactiveTickColor else activeTickColor),
1383 tickSize,
1384 StrokeCap.Round
1385 )
1386 }
1387 }
1388 }
1389
1390 /**
1391 * The Default track for [Slider]
1392 *
1393 * @param sliderState [SliderState] which is used to obtain the current active track.
1394 * @param modifier the [Modifier] to be applied to the track.
1395 * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1396 * different states. See [SliderDefaults.colors].
1397 * @param enabled controls the enabled state of this slider. When `false`, this component will
1398 * not respond to user input, and it will appear visually disabled and disabled to
1399 * accessibility services.
1400 */
1401 @Deprecated(
1402 message =
1403 "Use the overload that takes `drawStopIndicator`, `drawTick`, " +
1404 "`thumbTrackGapSize` and `trackInsideCornerSize`, see `LegacySliderSample` " +
1405 "on how to restore the previous behavior",
1406 replaceWith =
1407 ReplaceWith(
1408 "Track(sliderState, modifier, enabled, colors, drawStopIndicator, " +
1409 "drawTick, thumbTrackGapSize, trackInsideCornerSize)"
1410 ),
1411 level = DeprecationLevel.HIDDEN
1412 )
1413 @Composable
1414 @ExperimentalMaterial3Api
Tracknull1415 fun Track(
1416 sliderState: SliderState,
1417 modifier: Modifier = Modifier,
1418 colors: SliderColors = colors(),
1419 enabled: Boolean = true
1420 ) {
1421 Track(
1422 sliderState,
1423 modifier,
1424 enabled,
1425 colors,
1426 thumbTrackGapSize = ThumbTrackGapSize,
1427 trackInsideCornerSize = TrackInsideCornerSize
1428 )
1429 }
1430
1431 /**
1432 * The Default track for [Slider]
1433 *
1434 * @param sliderState [SliderState] which is used to obtain the current active track.
1435 * @param modifier the [Modifier] to be applied to the track.
1436 * @param enabled controls the enabled state of this slider. When `false`, this component will
1437 * not respond to user input, and it will appear visually disabled and disabled to
1438 * accessibility services.
1439 * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1440 * different states. See [SliderDefaults.colors].
1441 * @param drawStopIndicator lambda that will be called to draw the stop indicator at the end of
1442 * the track.
1443 * @param drawTick lambda that will be called to draw the ticks if steps are greater than 0.
1444 * @param thumbTrackGapSize size of the gap between the thumb and the track.
1445 * @param trackInsideCornerSize size of the corners towards the thumb when a gap is set.
1446 */
1447 @ExperimentalMaterial3Api
1448 @Composable
Tracknull1449 fun Track(
1450 sliderState: SliderState,
1451 modifier: Modifier = Modifier,
1452 enabled: Boolean = true,
1453 colors: SliderColors = colors(),
1454 drawStopIndicator: (DrawScope.(Offset) -> Unit)? = {
1455 drawStopIndicator(
1456 offset = it,
1457 color = colors.trackColor(enabled, active = true),
1458 size = TrackStopIndicatorSize
1459 )
1460 },
offsetnull1461 drawTick: DrawScope.(Offset, Color) -> Unit = { offset, color ->
1462 drawStopIndicator(offset = offset, color = color, size = TickSize)
1463 },
1464 thumbTrackGapSize: Dp = ThumbTrackGapSize,
1465 trackInsideCornerSize: Dp = TrackInsideCornerSize
1466 ) {
1467 TrackImpl(
1468 sliderState = sliderState,
1469 trackCornerSize = Dp.Unspecified,
1470 modifier = modifier,
1471 enabled = enabled,
1472 colors = colors,
1473 drawStopIndicator = drawStopIndicator,
1474 drawTick = drawTick,
1475 thumbTrackGapSize = thumbTrackGapSize,
1476 trackInsideCornerSize = trackInsideCornerSize,
1477 enableCornerShrinking = false
1478 )
1479 }
1480
1481 /**
1482 * The Default track for [Slider] and [VerticalSlider]
1483 *
1484 * This track has a different corner treatment where the corner size decreases as the thumb gets
1485 * closer.
1486 *
1487 * @param sliderState [SliderState] which is used to obtain the current active track.
1488 * @param trackCornerSize size of the external corners.
1489 * @param modifier the [Modifier] to be applied to the track.
1490 * @param enabled controls the enabled state of this slider. When `false`, this component will
1491 * not respond to user input, and it will appear visually disabled and disabled to
1492 * accessibility services.
1493 * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1494 * different states. See [SliderDefaults.colors].
1495 * @param drawStopIndicator lambda that will be called to draw the stop indicator at the end of
1496 * the track.
1497 * @param drawTick lambda that will be called to draw the ticks if steps are greater than 0.
1498 * @param thumbTrackGapSize size of the gap between the thumb and the track.
1499 * @param trackInsideCornerSize size of the corners towards the thumb when a gap is set.
1500 */
1501 @OptIn(ExperimentalMaterial3Api::class)
1502 @ExperimentalMaterial3ExpressiveApi
1503 @Composable
Tracknull1504 fun Track(
1505 sliderState: SliderState,
1506 trackCornerSize: Dp,
1507 modifier: Modifier = Modifier,
1508 enabled: Boolean = true,
1509 colors: SliderColors = colors(),
1510 drawStopIndicator: (DrawScope.(Offset) -> Unit)? = {
1511 drawStopIndicator(
1512 offset = it,
1513 color = colors.trackColor(enabled, active = true),
1514 size = TrackStopIndicatorSize
1515 )
1516 },
offsetnull1517 drawTick: DrawScope.(Offset, Color) -> Unit = { offset, color ->
1518 drawStopIndicator(offset = offset, color = color, size = TickSize)
1519 },
1520 thumbTrackGapSize: Dp = ThumbTrackGapSize,
1521 trackInsideCornerSize: Dp = TrackInsideCornerSize
1522 ) {
1523 TrackImpl(
1524 sliderState = sliderState,
1525 trackCornerSize = trackCornerSize,
1526 modifier = modifier,
1527 enabled = enabled,
1528 colors = colors,
1529 drawStopIndicator = drawStopIndicator,
1530 drawTick = drawTick,
1531 thumbTrackGapSize = thumbTrackGapSize,
1532 trackInsideCornerSize = trackInsideCornerSize,
1533 enableCornerShrinking = true
1534 )
1535 }
1536
1537 @OptIn(ExperimentalMaterial3Api::class)
1538 @Composable
TrackImplnull1539 private fun TrackImpl(
1540 sliderState: SliderState,
1541 trackCornerSize: Dp,
1542 modifier: Modifier,
1543 enabled: Boolean,
1544 colors: SliderColors,
1545 drawStopIndicator: (DrawScope.(Offset) -> Unit)?,
1546 drawTick: DrawScope.(Offset, Color) -> Unit,
1547 thumbTrackGapSize: Dp,
1548 trackInsideCornerSize: Dp,
1549 enableCornerShrinking: Boolean
1550 ) {
1551 val inactiveTrackColor = colors.trackColor(enabled = enabled, active = false)
1552 val activeTrackColor = colors.trackColor(enabled = enabled, active = true)
1553 val inactiveTickColor = colors.tickColor(enabled = enabled, active = false)
1554 val activeTickColor = colors.tickColor(enabled = enabled, active = true)
1555 var cornerSize by remember { mutableIntStateOf(0) }
1556 Canvas(
1557 if (sliderState.orientation == Vertical) {
1558 modifier.width(TrackHeight).fillMaxHeight().let {
1559 if (sliderState.reverseVerticalDirection) it.scale(1f, -1f) else it
1560 }
1561 } else {
1562 modifier.fillMaxWidth().height(TrackHeight).let {
1563 if (sliderState.isRtl) it.scale(-1f, 1f) else it
1564 }
1565 }
1566 .then(
1567 Modifier.layout { measurable, constraints ->
1568 val placeable = measurable.measure(constraints)
1569 cornerSize =
1570 if (trackCornerSize == Dp.Unspecified) {
1571 if (sliderState.orientation == Vertical) {
1572 placeable.width / 2
1573 } else {
1574 placeable.height / 2
1575 }
1576 } else {
1577 trackCornerSize.roundToPx()
1578 }
1579 layout(
1580 width = placeable.width,
1581 height = placeable.height,
1582 alignmentLines = mapOf(CornerSizeAlignmentLine to cornerSize)
1583 ) {
1584 placeable.place(0, 0)
1585 }
1586 }
1587 )
1588 ) {
1589 drawTrack(
1590 tickFractions = sliderState.tickFractions,
1591 activeRangeStart = 0f,
1592 activeRangeEnd = sliderState.coercedValueAsFraction,
1593 inactiveTrackColor = inactiveTrackColor,
1594 activeTrackColor = activeTrackColor,
1595 inactiveTickColor = inactiveTickColor,
1596 activeTickColor = activeTickColor,
1597 startThumbWidth = 0.toDp(),
1598 startThumbHeight = 0.toDp(),
1599 endThumbWidth = sliderState.thumbWidth.toDp(),
1600 endThumbHeight = sliderState.thumbHeight.toDp(),
1601 thumbTrackGapSize = thumbTrackGapSize,
1602 trackInsideCornerSize = trackInsideCornerSize,
1603 trackCornerSize = cornerSize.toDp(),
1604 drawStopIndicator = drawStopIndicator,
1605 drawTick = drawTick,
1606 isRangeSlider = false,
1607 enableCornerShrinking = enableCornerShrinking,
1608 orientation = sliderState.orientation
1609 )
1610 }
1611 }
1612
1613 /**
1614 * The Default track for [RangeSlider]
1615 *
1616 * @param rangeSliderState [RangeSliderState] which is used to obtain the current active track.
1617 * @param modifier the [Modifier] to be applied to the track.
1618 * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1619 * different states. See [SliderDefaults.colors].
1620 * @param enabled controls the enabled state of this slider. When `false`, this component will
1621 * not respond to user input, and it will appear visually disabled and disabled to
1622 * accessibility services.
1623 */
1624 @Deprecated(
1625 message =
1626 "Use the overload that takes `drawStopIndicator`, `drawTick`, " +
1627 "`thumbTrackGapSize` and `trackInsideCornerSize`, see `LegacyRangeSliderSample` " +
1628 "on how to restore the previous behavior",
1629 replaceWith =
1630 ReplaceWith(
1631 "Track(rangeSliderState, modifier, colors, enabled, drawStopIndicator, " +
1632 "drawTick, thumbTrackGapSize, trackInsideCornerSize)"
1633 ),
1634 level = DeprecationLevel.HIDDEN
1635 )
1636 @OptIn(ExperimentalMaterial3Api::class)
1637 @Composable
Tracknull1638 fun Track(
1639 rangeSliderState: RangeSliderState,
1640 modifier: Modifier = Modifier,
1641 colors: SliderColors = colors(),
1642 enabled: Boolean = true
1643 ) {
1644 Track(
1645 rangeSliderState,
1646 modifier,
1647 enabled,
1648 colors,
1649 thumbTrackGapSize = ThumbTrackGapSize,
1650 trackInsideCornerSize = TrackInsideCornerSize
1651 )
1652 }
1653
1654 /**
1655 * The Default track for [RangeSlider]
1656 *
1657 * @param rangeSliderState [RangeSliderState] which is used to obtain the current active track.
1658 * @param modifier the [Modifier] to be applied to the track.
1659 * @param enabled controls the enabled state of this slider. When `false`, this component will
1660 * not respond to user input, and it will appear visually disabled and disabled to
1661 * accessibility services.
1662 * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1663 * different states. See [SliderDefaults.colors].
1664 * @param drawStopIndicator lambda that will be called to draw the stop indicator at the
1665 * start/end of the track.
1666 * @param drawTick lambda that will be called to draw the ticks if steps are greater than 0.
1667 * @param thumbTrackGapSize size of the gap between the thumbs and the track.
1668 * @param trackInsideCornerSize size of the corners towards the thumbs when a gap is set.
1669 */
1670 @OptIn(ExperimentalMaterial3Api::class)
1671 @Composable
Tracknull1672 fun Track(
1673 rangeSliderState: RangeSliderState,
1674 modifier: Modifier = Modifier,
1675 enabled: Boolean = true,
1676 colors: SliderColors = colors(),
1677 drawStopIndicator: (DrawScope.(Offset) -> Unit)? = {
1678 drawStopIndicator(
1679 offset = it,
1680 color = colors.trackColor(enabled, active = true),
1681 size = TrackStopIndicatorSize
1682 )
1683 },
offsetnull1684 drawTick: DrawScope.(Offset, Color) -> Unit = { offset, color ->
1685 drawStopIndicator(offset = offset, color = color, size = TickSize)
1686 },
1687 thumbTrackGapSize: Dp = ThumbTrackGapSize,
1688 trackInsideCornerSize: Dp = TrackInsideCornerSize
1689 ) {
1690 val inactiveTrackColor = colors.trackColor(enabled, active = false)
1691 val activeTrackColor = colors.trackColor(enabled, active = true)
1692 val inactiveTickColor = colors.tickColor(enabled, active = false)
1693 val activeTickColor = colors.tickColor(enabled, active = true)
<lambda>null1694 var trackCornerSize by remember { mutableIntStateOf(0) }
1695 Canvas(
1696 modifier
1697 .fillMaxWidth()
1698 .height(TrackHeight)
1699 .rotate(if (rangeSliderState.isRtl) 180f else 0f)
constraintsnull1700 .layout { measurable, constraints ->
1701 val placeable = measurable.measure(constraints)
1702 trackCornerSize = placeable.height / 2
1703 layout(
1704 width = placeable.width,
1705 height = placeable.height,
1706 alignmentLines = mapOf(CornerSizeAlignmentLine to trackCornerSize)
1707 ) {
1708 placeable.place(0, 0)
1709 }
1710 }
<lambda>null1711 ) {
1712 drawTrack(
1713 tickFractions = rangeSliderState.tickFractions,
1714 activeRangeStart = rangeSliderState.coercedActiveRangeStartAsFraction,
1715 activeRangeEnd = rangeSliderState.coercedActiveRangeEndAsFraction,
1716 inactiveTrackColor = inactiveTrackColor,
1717 activeTrackColor = activeTrackColor,
1718 inactiveTickColor = inactiveTickColor,
1719 activeTickColor = activeTickColor,
1720 startThumbWidth = rangeSliderState.startThumbWidth.toDp(),
1721 startThumbHeight = rangeSliderState.startThumbHeight.toDp(),
1722 endThumbWidth = rangeSliderState.endThumbWidth.toDp(),
1723 endThumbHeight = rangeSliderState.endThumbHeight.toDp(),
1724 thumbTrackGapSize = thumbTrackGapSize,
1725 trackInsideCornerSize = trackInsideCornerSize,
1726 trackCornerSize = trackCornerSize.toDp(),
1727 drawStopIndicator = drawStopIndicator,
1728 drawTick = drawTick,
1729 isRangeSlider = true,
1730 )
1731 }
1732 }
1733
DrawScopenull1734 private fun DrawScope.drawTrack(
1735 tickFractions: FloatArray,
1736 activeRangeStart: Float,
1737 activeRangeEnd: Float,
1738 inactiveTrackColor: Color,
1739 activeTrackColor: Color,
1740 inactiveTickColor: Color,
1741 activeTickColor: Color,
1742 startThumbWidth: Dp,
1743 startThumbHeight: Dp,
1744 endThumbWidth: Dp,
1745 endThumbHeight: Dp,
1746 thumbTrackGapSize: Dp,
1747 trackInsideCornerSize: Dp,
1748 trackCornerSize: Dp,
1749 drawStopIndicator: (DrawScope.(Offset) -> Unit)?,
1750 drawTick: DrawScope.(Offset, Color) -> Unit,
1751 isRangeSlider: Boolean,
1752 enableCornerShrinking: Boolean = false,
1753 orientation: Orientation = Horizontal
1754 ) {
1755 val isVertical = orientation == Vertical
1756 val cornerSize = trackCornerSize.toPx()
1757 val sliderStart = 0f
1758 val sliderEnd = if (isVertical) size.height else size.width
1759
1760 val isStartOnFirstOrLastStep =
1761 activeRangeStart == tickFractions.firstOrNull() ||
1762 activeRangeStart == tickFractions.lastOrNull()
1763 val isEndOnFirstOrLastStep =
1764 activeRangeEnd == tickFractions.firstOrNull() ||
1765 activeRangeEnd == tickFractions.lastOrNull()
1766 val sliderValueEnd =
1767 if (tickFractions.isNotEmpty() && !isEndOnFirstOrLastStep) {
1768 sliderStart +
1769 (sliderEnd - sliderStart - cornerSize * 2) * activeRangeEnd +
1770 cornerSize
1771 } else {
1772 sliderStart + (sliderEnd - sliderStart) * activeRangeEnd
1773 }
1774 val sliderValueStart =
1775 if (tickFractions.isNotEmpty() && !isStartOnFirstOrLastStep) {
1776 sliderStart +
1777 (sliderEnd - sliderStart - cornerSize * 2) * activeRangeStart +
1778 cornerSize
1779 } else {
1780 sliderStart + (sliderEnd - sliderStart) * activeRangeStart
1781 }
1782
1783 val insideCornerSize = trackInsideCornerSize.toPx()
1784 var startGap = 0f
1785 var endGap = 0f
1786 if (thumbTrackGapSize > 0.dp) {
1787 if (isVertical) {
1788 startGap = startThumbHeight.toPx() / 2 + thumbTrackGapSize.toPx()
1789 endGap = endThumbHeight.toPx() / 2 + thumbTrackGapSize.toPx()
1790 } else {
1791 startGap = startThumbWidth.toPx() / 2 + thumbTrackGapSize.toPx()
1792 endGap = endThumbWidth.toPx() / 2 + thumbTrackGapSize.toPx()
1793 }
1794 }
1795
1796 // inactive track (range slider)
1797 var rangeInactiveTrackThreshold = sliderStart + startGap
1798 if (!enableCornerShrinking || tickFractions.isNotEmpty()) {
1799 rangeInactiveTrackThreshold += cornerSize
1800 }
1801 if (isRangeSlider && sliderValueStart > rangeInactiveTrackThreshold) {
1802 val start = sliderStart
1803 val end = sliderValueStart - startGap
1804 val size =
1805 if (isVertical) Size(size.width, end - start) else Size(end - start, size.height)
1806 drawTrackPath(
1807 orientation,
1808 Offset.Zero,
1809 size,
1810 inactiveTrackColor,
1811 cornerSize,
1812 insideCornerSize
1813 )
1814 val stopIndicatorOffset =
1815 if (isVertical) Offset(center.x, start + cornerSize)
1816 else Offset(start + cornerSize, center.y)
1817 drawStopIndicator?.invoke(this, stopIndicatorOffset)
1818 }
1819 // inactive track
1820 var inactiveTrackThreshold = sliderEnd - endGap
1821 if (!enableCornerShrinking || tickFractions.isNotEmpty()) {
1822 inactiveTrackThreshold -= cornerSize
1823 }
1824 if (sliderValueEnd < inactiveTrackThreshold) {
1825 val start = sliderValueEnd + endGap
1826 val end = sliderEnd
1827 val inactiveTrackWidth = end - start
1828 val trackOffset = if (isVertical) Offset(0f, start) else Offset(start, 0f)
1829 val size =
1830 if (isVertical) Size(size.width, inactiveTrackWidth)
1831 else Size(inactiveTrackWidth, size.height)
1832 drawTrackPath(
1833 orientation,
1834 trackOffset,
1835 size,
1836 inactiveTrackColor,
1837 insideCornerSize,
1838 cornerSize
1839 )
1840 val stopIndicatorOffset =
1841 if (isVertical) Offset(center.x, end - cornerSize)
1842 else Offset(end - cornerSize, center.y)
1843 drawStopIndicator?.invoke(this, stopIndicatorOffset)
1844 }
1845 // active track
1846 val activeTrackStart = if (isRangeSlider) sliderValueStart + startGap else 0f
1847 val activeTrackEnd = sliderValueEnd - endGap
1848 val startCornerRadius = if (isRangeSlider) insideCornerSize else cornerSize
1849 val activeTrackWidth = activeTrackEnd - activeTrackStart
1850 val activeTrackThreshold =
1851 if (!enableCornerShrinking || tickFractions.isNotEmpty()) startCornerRadius else 0f
1852 if (activeTrackWidth > activeTrackThreshold) {
1853 val trackOffset =
1854 if (isVertical) Offset(0f, activeTrackStart) else Offset(activeTrackStart, 0f)
1855 val size =
1856 if (isVertical) Size(size.width, activeTrackWidth)
1857 else Size(activeTrackWidth, size.height)
1858 drawTrackPath(
1859 orientation,
1860 trackOffset,
1861 size,
1862 activeTrackColor,
1863 startCornerRadius,
1864 insideCornerSize
1865 )
1866 }
1867
1868 val start = sliderStart + cornerSize
1869 val end = sliderEnd - cornerSize
1870 val tickStartGap = sliderValueStart - startGap..sliderValueStart + startGap
1871 val tickEndGap = sliderValueEnd - endGap..sliderValueEnd + endGap
1872 tickFractions.forEachIndexed { index, tick ->
1873 // skip ticks that fall on the stop indicator
1874 if (drawStopIndicator != null) {
1875 if ((isRangeSlider && index == 0) || index == tickFractions.size - 1) {
1876 return@forEachIndexed
1877 }
1878 }
1879
1880 val outsideFraction = tick > activeRangeEnd || tick < activeRangeStart
1881 val centerTick = lerp(start, end, tick)
1882 // skip ticks that fall on a gap
1883 if ((isRangeSlider && centerTick in tickStartGap) || centerTick in tickEndGap) {
1884 return@forEachIndexed
1885 }
1886 val offset =
1887 if (isVertical) Offset(center.x, centerTick) else Offset(centerTick, center.y)
1888 drawTick(
1889 this,
1890 offset, // offset
1891 if (outsideFraction) inactiveTickColor else activeTickColor // color
1892 )
1893 }
1894 }
1895
DrawScopenull1896 private fun DrawScope.drawTrackPath(
1897 orientation: Orientation,
1898 offset: Offset,
1899 size: Size,
1900 color: Color,
1901 startCornerRadius: Float,
1902 endCornerRadius: Float
1903 ) {
1904 val startCorner = CornerRadius(startCornerRadius, startCornerRadius)
1905 val endCorner = CornerRadius(endCornerRadius, endCornerRadius)
1906 val track =
1907 if (orientation == Vertical) {
1908 RoundRect(
1909 rect = Rect(offset, size = Size(size.width, size.height)),
1910 topLeft = startCorner,
1911 topRight = startCorner,
1912 bottomRight = endCorner,
1913 bottomLeft = endCorner
1914 )
1915 } else {
1916 RoundRect(
1917 rect = Rect(offset, size = Size(size.width, size.height)),
1918 topLeft = startCorner,
1919 topRight = endCorner,
1920 bottomRight = endCorner,
1921 bottomLeft = startCorner
1922 )
1923 }
1924 trackPath.addRoundRect(track)
1925 drawPath(trackPath, color)
1926 trackPath.rewind()
1927 }
1928
1929 /**
1930 * The Default stop indicator.
1931 *
1932 * @param offset the coordinate where the indicator is to be drawn.
1933 * @param size the size of the indicator.
1934 * @param color the color of the indicator.
1935 */
DrawScopenull1936 fun DrawScope.drawStopIndicator(offset: Offset, size: Dp, color: Color) {
1937 drawCircle(color = color, center = offset, radius = size.toPx() / 2f)
1938 }
1939
1940 /** The default size for the stop indicator at the end of the track. */
1941 val TrackStopIndicatorSize: Dp = SliderTokens.StopIndicatorSize
1942
1943 /** The default size for the ticks if steps are greater than 0. */
1944 val TickSize: Dp = SliderTokens.StopIndicatorSize
1945
1946 private val trackPath = Path()
1947 }
1948
snapValueToTicknull1949 private fun snapValueToTick(
1950 current: Float,
1951 tickFractions: FloatArray,
1952 minPx: Float,
1953 maxPx: Float
1954 ): Float {
1955 // target is a closest anchor to the `current`, if exists
1956 return tickFractions
1957 .minByOrNull { abs(lerp(minPx, maxPx, it) - current) }
1958 ?.run { lerp(minPx, maxPx, this) } ?: current
1959 }
1960
awaitSlopnull1961 private suspend fun AwaitPointerEventScope.awaitSlop(
1962 id: PointerId,
1963 type: PointerType
1964 ): Pair<PointerInputChange, Float>? {
1965 var initialDelta = 0f
1966 val postPointerSlop = { pointerInput: PointerInputChange, offset: Float ->
1967 pointerInput.consume()
1968 initialDelta = offset
1969 }
1970 val afterSlopResult = awaitHorizontalPointerSlopOrCancellation(id, type, postPointerSlop)
1971 return if (afterSlopResult != null) afterSlopResult to initialDelta else null
1972 }
1973
stepsToTickFractionsnull1974 private fun stepsToTickFractions(steps: Int): FloatArray {
1975 return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) }
1976 }
1977
1978 // Scale x1 from a1..b1 range to a2..b2 range
scalenull1979 private fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) =
1980 lerp(a2, b2, calcFraction(a1, b1, x1))
1981
1982 // Scale x.start, x.endInclusive from a1..b1 range to a2..b2 range
1983 private fun scale(a1: Float, b1: Float, x: SliderRange, a2: Float, b2: Float) =
1984 SliderRange(scale(a1, b1, x.start, a2, b2), scale(a1, b1, x.endInclusive, a2, b2))
1985
1986 // Calculate the 0..1 fraction that `pos` value represents between `a` and `b`
1987 private fun calcFraction(a: Float, b: Float, pos: Float) =
1988 (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f)
1989
1990 @OptIn(ExperimentalMaterial3Api::class)
1991 private fun Modifier.sliderSemantics(state: SliderState, enabled: Boolean): Modifier {
1992 return semantics {
1993 if (!enabled) disabled()
1994 setProgress(
1995 action = { targetValue ->
1996 var newValue =
1997 targetValue.coerceIn(state.valueRange.start, state.valueRange.endInclusive)
1998 val originalVal = newValue
1999 val resolvedValue =
2000 if (state.steps > 0) {
2001 var distance: Float = newValue
2002 for (i in 0..state.steps + 1) {
2003 val stepValue =
2004 lerp(
2005 state.valueRange.start,
2006 state.valueRange.endInclusive,
2007 i.toFloat() / (state.steps + 1)
2008 )
2009 if (abs(stepValue - originalVal) <= distance) {
2010 distance = abs(stepValue - originalVal)
2011 newValue = stepValue
2012 }
2013 }
2014 newValue
2015 } else {
2016 newValue
2017 }
2018
2019 // This is to keep it consistent with AbsSeekbar.java: return false if no
2020 // change from current.
2021 if (resolvedValue == state.value) {
2022 false
2023 } else {
2024 if (resolvedValue != state.value) {
2025 if (state.onValueChange != null) {
2026 state.onValueChange?.let { it(resolvedValue) }
2027 } else {
2028 state.value = resolvedValue
2029 }
2030 }
2031 state.onValueChangeFinished?.invoke()
2032 true
2033 }
2034 }
2035 )
2036 }
2037 .then(IncreaseHorizontalSemanticsBounds)
2038 .progressSemantics(
2039 state.value,
2040 state.valueRange.start..state.valueRange.endInclusive,
2041 state.steps
2042 )
2043 }
2044
2045 @OptIn(ExperimentalMaterial3Api::class)
Modifiernull2046 private fun Modifier.rangeSliderStartThumbSemantics(
2047 state: RangeSliderState,
2048 enabled: Boolean
2049 ): Modifier {
2050 val valueRange = state.valueRange.start..state.activeRangeEnd
2051
2052 return semantics {
2053 if (!enabled) disabled()
2054 setProgress(
2055 action = { targetValue ->
2056 var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive)
2057 val originalVal = newValue
2058 val resolvedValue =
2059 if (state.startSteps > 0) {
2060 var distance: Float = newValue
2061 for (i in 0..state.startSteps + 1) {
2062 val stepValue =
2063 lerp(
2064 valueRange.start,
2065 valueRange.endInclusive,
2066 i.toFloat() / (state.startSteps + 1)
2067 )
2068 if (abs(stepValue - originalVal) <= distance) {
2069 distance = abs(stepValue - originalVal)
2070 newValue = stepValue
2071 }
2072 }
2073 newValue
2074 } else {
2075 newValue
2076 }
2077
2078 // This is to keep it consistent with AbsSeekbar.java: return false if no
2079 // change from current.
2080 if (resolvedValue == state.activeRangeStart) {
2081 false
2082 } else {
2083 val resolvedRange = SliderRange(resolvedValue, state.activeRangeEnd)
2084 val activeRange = SliderRange(state.activeRangeStart, state.activeRangeEnd)
2085 if (resolvedRange != activeRange) {
2086 if (state.onValueChange != null) {
2087 state.onValueChange?.let { it(resolvedRange) }
2088 } else {
2089 state.activeRangeStart = resolvedRange.start
2090 state.activeRangeEnd = resolvedRange.endInclusive
2091 }
2092 }
2093 state.onValueChangeFinished?.invoke()
2094 true
2095 }
2096 }
2097 )
2098 }
2099 .then(IncreaseHorizontalSemanticsBounds)
2100 .progressSemantics(state.activeRangeStart, valueRange, state.startSteps)
2101 }
2102
2103 @OptIn(ExperimentalMaterial3Api::class)
rangeSliderEndThumbSemanticsnull2104 private fun Modifier.rangeSliderEndThumbSemantics(
2105 state: RangeSliderState,
2106 enabled: Boolean
2107 ): Modifier {
2108 val valueRange = state.activeRangeStart..state.valueRange.endInclusive
2109
2110 return semantics {
2111 if (!enabled) disabled()
2112
2113 setProgress(
2114 action = { targetValue ->
2115 var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive)
2116 val originalVal = newValue
2117 val resolvedValue =
2118 if (state.endSteps > 0) {
2119 var distance: Float = newValue
2120 for (i in 0..state.endSteps + 1) {
2121 val stepValue =
2122 lerp(
2123 valueRange.start,
2124 valueRange.endInclusive,
2125 i.toFloat() / (state.endSteps + 1)
2126 )
2127 if (abs(stepValue - originalVal) <= distance) {
2128 distance = abs(stepValue - originalVal)
2129 newValue = stepValue
2130 }
2131 }
2132 newValue
2133 } else {
2134 newValue
2135 }
2136
2137 // This is to keep it consistent with AbsSeekbar.java: return false if no
2138 // change from current.
2139 if (resolvedValue == state.activeRangeEnd) {
2140 false
2141 } else {
2142 val resolvedRange = SliderRange(state.activeRangeStart, resolvedValue)
2143 val activeRange = SliderRange(state.activeRangeStart, state.activeRangeEnd)
2144 if (resolvedRange != activeRange) {
2145 if (state.onValueChange != null) {
2146 state.onValueChange?.let { it(resolvedRange) }
2147 } else {
2148 state.activeRangeStart = resolvedRange.start
2149 state.activeRangeEnd = resolvedRange.endInclusive
2150 }
2151 }
2152 state.onValueChangeFinished?.invoke()
2153 true
2154 }
2155 }
2156 )
2157 }
2158 .then(IncreaseHorizontalSemanticsBounds)
2159 .progressSemantics(state.activeRangeEnd, valueRange, state.endSteps)
2160 }
2161
2162 @OptIn(ExperimentalMaterial3Api::class)
2163 @Stable
sliderTapModifiernull2164 private fun Modifier.sliderTapModifier(
2165 state: SliderState,
2166 interactionSource: MutableInteractionSource,
2167 enabled: Boolean
2168 ) =
2169 if (enabled) {
2170 pointerInput(state, interactionSource) {
2171 detectTapGestures(
2172 onPress = { state.onPress(it) },
2173 onTap = {
2174 state.dispatchRawDelta(0f)
2175 state.gestureEndAction()
2176 }
2177 )
2178 }
2179 } else {
2180 this
2181 }
2182
2183 @OptIn(ExperimentalMaterial3Api::class)
2184 @Stable
rangeSliderPressDragModifiernull2185 private fun Modifier.rangeSliderPressDragModifier(
2186 state: RangeSliderState,
2187 startInteractionSource: MutableInteractionSource,
2188 endInteractionSource: MutableInteractionSource,
2189 enabled: Boolean
2190 ): Modifier =
2191 if (enabled) {
2192 pointerInput(startInteractionSource, endInteractionSource, state) {
2193 val rangeSliderLogic =
2194 RangeSliderLogic(state, startInteractionSource, endInteractionSource)
2195 coroutineScope {
2196 awaitEachGesture {
2197 val event = awaitFirstDown(requireUnconsumed = false)
2198 val interaction = DragInteraction.Start()
2199 var posX =
2200 if (state.isRtl) state.totalWidth - event.position.x else event.position.x
2201 val compare = rangeSliderLogic.compareOffsets(posX)
2202 var draggingStart =
2203 if (compare != 0) {
2204 compare < 0
2205 } else {
2206 state.rawOffsetStart > posX
2207 }
2208
2209 awaitSlop(event.id, event.type)?.let {
2210 val slop = viewConfiguration.pointerSlop(event.type)
2211 val shouldUpdateCapturedThumb =
2212 abs(state.rawOffsetEnd - posX) < slop &&
2213 abs(state.rawOffsetStart - posX) < slop
2214 if (shouldUpdateCapturedThumb) {
2215 val dir = it.second
2216 draggingStart = if (state.isRtl) dir >= 0f else dir < 0f
2217 posX += it.first.positionChange().x
2218 }
2219 }
2220
2221 rangeSliderLogic.captureThumb(
2222 draggingStart,
2223 posX,
2224 interaction,
2225 this@coroutineScope
2226 )
2227
2228 val finishInteraction =
2229 try {
2230 val success =
2231 horizontalDrag(pointerId = event.id) {
2232 val deltaX = it.positionChange().x
2233 state.onDrag(
2234 draggingStart,
2235 if (state.isRtl) -deltaX else deltaX
2236 )
2237 }
2238 if (success) {
2239 DragInteraction.Stop(interaction)
2240 } else {
2241 DragInteraction.Cancel(interaction)
2242 }
2243 } catch (e: CancellationException) {
2244 DragInteraction.Cancel(interaction)
2245 }
2246
2247 state.gestureEndAction(draggingStart)
2248 launch {
2249 rangeSliderLogic.activeInteraction(draggingStart).emit(finishInteraction)
2250 }
2251 }
2252 }
2253 }
2254 } else {
2255 this
2256 }
2257
2258 @OptIn(ExperimentalMaterial3Api::class)
2259 private class RangeSliderLogic(
2260 val state: RangeSliderState,
2261 val startInteractionSource: MutableInteractionSource,
2262 val endInteractionSource: MutableInteractionSource
2263 ) {
activeInteractionnull2264 fun activeInteraction(draggingStart: Boolean): MutableInteractionSource =
2265 if (draggingStart) startInteractionSource else endInteractionSource
2266
2267 fun compareOffsets(eventX: Float): Int {
2268 val diffStart = abs(state.rawOffsetStart - eventX)
2269 val diffEnd = abs(state.rawOffsetEnd - eventX)
2270 return diffStart.compareTo(diffEnd)
2271 }
2272
captureThumbnull2273 fun captureThumb(
2274 draggingStart: Boolean,
2275 posX: Float,
2276 interaction: Interaction,
2277 scope: CoroutineScope
2278 ) {
2279 state.onDrag(
2280 draggingStart,
2281 posX - if (draggingStart) state.rawOffsetStart else state.rawOffsetEnd
2282 )
2283 scope.launch { activeInteraction(draggingStart).emit(interaction) }
2284 }
2285 }
2286
2287 /**
2288 * Represents the color used by a [Slider] in different states.
2289 *
2290 * @param thumbColor thumb color when enabled
2291 * @param activeTrackColor color of the track in the part that is "active", meaning that the thumb
2292 * is ahead of it
2293 * @param activeTickColor colors to be used to draw tick marks on the active track, if `steps` is
2294 * specified
2295 * @param inactiveTrackColor color of the track in the part that is "inactive", meaning that the
2296 * thumb is before it
2297 * @param inactiveTickColor colors to be used to draw tick marks on the inactive track, if `steps`
2298 * are specified on the Slider is specified
2299 * @param disabledThumbColor thumb colors when disabled
2300 * @param disabledActiveTrackColor color of the track in the "active" part when the Slider is
2301 * disabled
2302 * @param disabledActiveTickColor colors to be used to draw tick marks on the active track when
2303 * Slider is disabled and when `steps` are specified on it
2304 * @param disabledInactiveTrackColor color of the track in the "inactive" part when the Slider is
2305 * disabled
2306 * @param disabledInactiveTickColor colors to be used to draw tick marks on the inactive part of the
2307 * track when Slider is disabled and when `steps` are specified on it
2308 * @constructor create an instance with arbitrary colors. See [SliderDefaults.colors] for the
2309 * default implementation that follows Material specifications.
2310 */
2311 @Immutable
2312 class SliderColors(
2313 val thumbColor: Color,
2314 val activeTrackColor: Color,
2315 val activeTickColor: Color,
2316 val inactiveTrackColor: Color,
2317 val inactiveTickColor: Color,
2318 val disabledThumbColor: Color,
2319 val disabledActiveTrackColor: Color,
2320 val disabledActiveTickColor: Color,
2321 val disabledInactiveTrackColor: Color,
2322 val disabledInactiveTickColor: Color
2323 ) {
2324
2325 /**
2326 * Returns a copy of this SelectableChipColors, optionally overriding some of the values. This
2327 * uses the Color.Unspecified to mean “use the value from the source”
2328 */
copynull2329 fun copy(
2330 thumbColor: Color = this.thumbColor,
2331 activeTrackColor: Color = this.activeTrackColor,
2332 activeTickColor: Color = this.activeTickColor,
2333 inactiveTrackColor: Color = this.inactiveTrackColor,
2334 inactiveTickColor: Color = this.inactiveTickColor,
2335 disabledThumbColor: Color = this.disabledThumbColor,
2336 disabledActiveTrackColor: Color = this.disabledActiveTrackColor,
2337 disabledActiveTickColor: Color = this.disabledActiveTickColor,
2338 disabledInactiveTrackColor: Color = this.disabledInactiveTrackColor,
2339 disabledInactiveTickColor: Color = this.disabledInactiveTickColor,
2340 ) =
2341 SliderColors(
2342 thumbColor.takeOrElse { this.thumbColor },
<lambda>null2343 activeTrackColor.takeOrElse { this.activeTrackColor },
<lambda>null2344 activeTickColor.takeOrElse { this.activeTickColor },
<lambda>null2345 inactiveTrackColor.takeOrElse { this.inactiveTrackColor },
<lambda>null2346 inactiveTickColor.takeOrElse { this.inactiveTickColor },
<lambda>null2347 disabledThumbColor.takeOrElse { this.disabledThumbColor },
<lambda>null2348 disabledActiveTrackColor.takeOrElse { this.disabledActiveTrackColor },
<lambda>null2349 disabledActiveTickColor.takeOrElse { this.disabledActiveTickColor },
<lambda>null2350 disabledInactiveTrackColor.takeOrElse { this.disabledInactiveTrackColor },
<lambda>null2351 disabledInactiveTickColor.takeOrElse { this.disabledInactiveTickColor },
2352 )
2353
2354 @Stable
thumbColornull2355 internal fun thumbColor(enabled: Boolean): Color =
2356 if (enabled) thumbColor else disabledThumbColor
2357
2358 @Stable
2359 internal fun trackColor(enabled: Boolean, active: Boolean): Color =
2360 if (enabled) {
2361 if (active) activeTrackColor else inactiveTrackColor
2362 } else {
2363 if (active) disabledActiveTrackColor else disabledInactiveTrackColor
2364 }
2365
2366 @Stable
tickColornull2367 internal fun tickColor(enabled: Boolean, active: Boolean): Color =
2368 if (enabled) {
2369 if (active) activeTickColor else inactiveTickColor
2370 } else {
2371 if (active) disabledActiveTickColor else disabledInactiveTickColor
2372 }
2373
equalsnull2374 override fun equals(other: Any?): Boolean {
2375 if (this === other) return true
2376 if (other == null || other !is SliderColors) return false
2377
2378 if (thumbColor != other.thumbColor) return false
2379 if (activeTrackColor != other.activeTrackColor) return false
2380 if (activeTickColor != other.activeTickColor) return false
2381 if (inactiveTrackColor != other.inactiveTrackColor) return false
2382 if (inactiveTickColor != other.inactiveTickColor) return false
2383 if (disabledThumbColor != other.disabledThumbColor) return false
2384 if (disabledActiveTrackColor != other.disabledActiveTrackColor) return false
2385 if (disabledActiveTickColor != other.disabledActiveTickColor) return false
2386 if (disabledInactiveTrackColor != other.disabledInactiveTrackColor) return false
2387 if (disabledInactiveTickColor != other.disabledInactiveTickColor) return false
2388
2389 return true
2390 }
2391
hashCodenull2392 override fun hashCode(): Int {
2393 var result = thumbColor.hashCode()
2394 result = 31 * result + activeTrackColor.hashCode()
2395 result = 31 * result + activeTickColor.hashCode()
2396 result = 31 * result + inactiveTrackColor.hashCode()
2397 result = 31 * result + inactiveTickColor.hashCode()
2398 result = 31 * result + disabledThumbColor.hashCode()
2399 result = 31 * result + disabledActiveTrackColor.hashCode()
2400 result = 31 * result + disabledActiveTickColor.hashCode()
2401 result = 31 * result + disabledInactiveTrackColor.hashCode()
2402 result = 31 * result + disabledInactiveTickColor.hashCode()
2403 return result
2404 }
2405 }
2406
2407 // Internal to be referred to in tests
2408 internal val TrackHeight = SliderTokens.InactiveTrackHeight
2409 internal val ThumbWidth = SliderTokens.HandleWidth
2410 private val ThumbHeight = SliderTokens.HandleHeight
2411 private val ThumbSize = DpSize(ThumbWidth, ThumbHeight)
2412 private val VerticalThumbSize = DpSize(ThumbHeight, ThumbWidth)
2413 private val ThumbTrackGapSize: Dp = SliderTokens.ActiveHandleLeadingSpace
2414 private val TrackInsideCornerSize: Dp = 2.dp
2415 private const val SliderRangeTolerance = 0.0001
2416
2417 private enum class SliderComponents {
2418 THUMB,
2419 TRACK
2420 }
2421
2422 private enum class RangeSliderComponents {
2423 ENDTHUMB,
2424 STARTTHUMB,
2425 TRACK
2426 }
2427
2428 /**
2429 * Class that holds information about [Slider]'s and [RangeSlider]'s active track and fractional
2430 * positions where the discrete ticks should be drawn on the track.
2431 */
2432 @Suppress("DEPRECATION")
2433 @Deprecated("Not necessary with the introduction of Slider state")
2434 @Stable
2435 class SliderPositions(
2436 initialActiveRange: ClosedFloatingPointRange<Float> = 0f..1f,
2437 initialTickFractions: FloatArray = floatArrayOf()
2438 ) {
2439 /**
2440 * [ClosedFloatingPointRange] that indicates the current active range for the start to thumb for
2441 * a [Slider] and start thumb to end thumb for a [RangeSlider].
2442 */
2443 var activeRange: ClosedFloatingPointRange<Float> by mutableStateOf(initialActiveRange)
2444 internal set
2445
2446 /**
2447 * The discrete points where a tick should be drawn on the track. Each value of tickFractions
2448 * should be within the range [0f, 1f]. If the track is continuous, then tickFractions will be
2449 * an empty [FloatArray].
2450 */
2451 var tickFractions: FloatArray by mutableStateOf(initialTickFractions)
2452 internal set
2453
equalsnull2454 override fun equals(other: Any?): Boolean {
2455 if (this === other) return true
2456 if (other !is SliderPositions) return false
2457
2458 if (activeRange != other.activeRange) return false
2459 if (!tickFractions.contentEquals(other.tickFractions)) return false
2460
2461 return true
2462 }
2463
hashCodenull2464 override fun hashCode(): Int {
2465 var result = activeRange.hashCode()
2466 result = 31 * result + tickFractions.contentHashCode()
2467 return result
2468 }
2469 }
2470
2471 /**
2472 * Class that holds information about [Slider]'s active range.
2473 *
2474 * @param value [Float] that indicates the initial position of the thumb. If outside of [valueRange]
2475 * provided, value will be coerced to this range.
2476 * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
2477 * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
2478 * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
2479 * continuously and allow any value from the range. Must not be negative.
2480 * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
2481 * shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
2482 * to know when the user has completed selecting a new value by ending a drag or a click.
2483 * @param valueRange range of values that Slider values can take. [value] will be coerced to this
2484 * range.
2485 */
2486 @ExperimentalMaterial3Api
2487 class SliderState(
2488 value: Float = 0f,
2489 @IntRange(from = 0) val steps: Int = 0,
2490 var onValueChangeFinished: (() -> Unit)? = null,
2491 val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
2492 ) : DraggableState {
2493
2494 private var valueState by mutableFloatStateOf(value)
2495
2496 /** [Float] that indicates the value that the thumb currently is in respect to the track. */
2497 var value: Float
2498 set(newVal) {
2499 valueState =
2500 if (shouldAutoSnap) {
2501 calculateSnappedValue(newVal)
2502 } else {
2503 newVal
2504 }
2505 }
2506 get() = valueState
2507
calculateSnappedValuenull2508 private fun calculateSnappedValue(newVal: Float): Float {
2509 val coercedValue = newVal.coerceIn(valueRange.start, valueRange.endInclusive)
2510 return snapValueToTick(
2511 coercedValue,
2512 tickFractions,
2513 valueRange.start,
2514 valueRange.endInclusive
2515 )
2516 }
2517
dragnull2518 override suspend fun drag(
2519 dragPriority: MutatePriority,
2520 block: suspend DragScope.() -> Unit
2521 ): Unit = coroutineScope {
2522 isDragging = true
2523 scrollMutex.mutateWith(dragScope, dragPriority, block)
2524 isDragging = false
2525 }
2526
dispatchRawDeltanull2527 override fun dispatchRawDelta(delta: Float) {
2528 val maxPx: Float
2529 val minPx: Float
2530 if (orientation == Vertical) {
2531 maxPx = max(totalHeight - thumbHeight / 2f, 0f)
2532 minPx = min(thumbHeight / 2f, maxPx)
2533 } else {
2534 maxPx = max(totalWidth - thumbWidth / 2f, 0f)
2535 minPx = min(thumbWidth / 2f, maxPx)
2536 }
2537 rawOffset = (rawOffset + delta + pressOffset)
2538 pressOffset = 0f
2539 val offsetInTrack = snapValueToTick(rawOffset, tickFractions, minPx, maxPx)
2540 val scaledUserValue = scaleToUserValue(minPx, maxPx, offsetInTrack)
2541 if (scaledUserValue != this.value) {
2542 if (onValueChange != null) {
2543 onValueChange?.let { it(scaledUserValue) }
2544 } else {
2545 this.value = scaledUserValue
2546 }
2547 }
2548 }
2549
2550 /** Callback in which value should be updated. */
2551 var onValueChange: ((Float) -> Unit)? = null
2552
2553 /** Controls the auto-snapping mechanism, disabling it may be useful for custom animations. */
2554 @get:JvmName("shouldAutoSnap") var shouldAutoSnap: Boolean = true
2555
2556 internal val tickFractions = stepsToTickFractions(steps)
2557 private var totalWidth by mutableIntStateOf(0)
2558 private var totalHeight by mutableIntStateOf(0)
2559 internal var isRtl = false
2560 internal var thumbWidth by mutableIntStateOf(0)
2561 internal var thumbHeight by mutableIntStateOf(0)
2562 internal var orientation = Horizontal
2563 internal var reverseVerticalDirection = false
2564
2565 /** The fraction of the track that the thumb currently is in. */
2566 val coercedValueAsFraction: Float
2567 get() =
2568 calcFraction(
2569 valueRange.start,
2570 valueRange.endInclusive,
2571 value.coerceIn(valueRange.start, valueRange.endInclusive)
2572 )
2573
2574 var isDragging by mutableStateOf(false)
2575 private set
2576
updateDimensionsnull2577 internal fun updateDimensions(newTotalWidth: Int, newTotalHeight: Int) {
2578 totalWidth = newTotalWidth
2579 totalHeight = newTotalHeight
2580 }
2581
<lambda>null2582 internal val gestureEndAction = {
2583 if (!isDragging) {
2584 // check isDragging in case the change is still in progress (touch -> drag case)
2585 onValueChangeFinished?.invoke()
2586 }
2587 }
2588
onPressnull2589 internal fun onPress(pos: Offset) {
2590 val to =
2591 if (orientation == Vertical) {
2592 if (reverseVerticalDirection) totalHeight - pos.y else pos.y
2593 } else {
2594 if (isRtl) totalWidth - pos.x else pos.x
2595 }
2596 pressOffset = to - rawOffset
2597 }
2598
2599 private var rawOffset by mutableFloatStateOf(scaleToOffset(0f, 0f, value))
2600 private var pressOffset by mutableFloatStateOf(0f)
2601 private val dragScope: DragScope =
2602 object : DragScope {
dragBynull2603 override fun dragBy(pixels: Float): Unit = dispatchRawDelta(pixels)
2604 }
2605
2606 private val scrollMutex = MutatorMutex()
2607
2608 private fun scaleToUserValue(minPx: Float, maxPx: Float, offset: Float) =
2609 scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
2610
2611 private fun scaleToOffset(minPx: Float, maxPx: Float, userValue: Float) =
2612 scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
2613
2614 companion object {
2615 /**
2616 * The default [Saver] implementation for [SliderState].
2617 *
2618 * @param onValueChangeFinished lambda to be invoked when value change has ended. This
2619 * callback shouldn't be used to update the range slider values (use [onValueChange] for
2620 * that), but rather to know when the user has completed selecting a new value by ending a
2621 * drag or a click.
2622 * @param valueRange range of values that Slider values can take. [value] will be coerced to
2623 * this range.
2624 */
2625 fun Saver(
2626 onValueChangeFinished: (() -> Unit)?,
2627 valueRange: ClosedFloatingPointRange<Float>
2628 ): Saver<SliderState, *> =
2629 listSaver(
2630 save = { listOf(it.value, it.steps) },
2631 restore = {
2632 SliderState(
2633 value = it[0] as Float,
2634 steps = it[1] as Int,
2635 onValueChangeFinished = onValueChangeFinished,
2636 valueRange = valueRange
2637 )
2638 }
2639 )
2640 }
2641 }
2642
2643 /**
2644 * Creates a [SliderState] that is remembered across compositions.
2645 *
2646 * Changes to the provided initial values will **not** result in the state being recreated or
2647 * changed in any way if it has already been created.
2648 *
2649 * @param value [Float] that indicates the initial position of the thumb. If outside of [valueRange]
2650 * provided, value will be coerced to this range.
2651 * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
2652 * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
2653 * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
2654 * continuously and allow any value from the range. Must not be negative.
2655 * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
2656 * shouldn't be used to update the range slider values (use [SliderState.onValueChange] for that),
2657 * but rather to know when the user has completed selecting a new value by ending a drag or a
2658 * click.
2659 * @param valueRange range of values that Slider values can take. [value] will be coerced to this
2660 * range.
2661 */
2662 @ExperimentalMaterial3Api
2663 @Composable
rememberSliderStatenull2664 fun rememberSliderState(
2665 value: Float = 0f,
2666 @IntRange(from = 0) steps: Int = 0,
2667 onValueChangeFinished: (() -> Unit)? = null,
2668 valueRange: ClosedFloatingPointRange<Float> = 0f..1f
2669 ): SliderState {
2670 return rememberSaveable(saver = SliderState.Saver(onValueChangeFinished, valueRange)) {
2671 SliderState(
2672 value = value,
2673 steps = steps,
2674 onValueChangeFinished = onValueChangeFinished,
2675 valueRange = valueRange
2676 )
2677 }
2678 }
2679
2680 /**
2681 * Class that holds information about [RangeSlider]'s active range.
2682 *
2683 * @param activeRangeStart [Float] that indicates the initial start of the active range of the
2684 * slider. If outside of [valueRange] provided, value will be coerced to this range.
2685 * @param activeRangeEnd [Float] that indicates the initial end of the active range of the slider.
2686 * If outside of [valueRange] provided, value will be coerced to this range.
2687 * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
2688 * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
2689 * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
2690 * continuously and allow any value from the range. Must not be negative.
2691 * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
2692 * shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
2693 * to know when the user has completed selecting a new value by ending a drag or a click.
2694 * @param valueRange range of values that Range Slider values can take. [activeRangeStart] and
2695 * [activeRangeEnd] will be coerced to this range.
2696 */
2697 @ExperimentalMaterial3Api
2698 class RangeSliderState(
2699 activeRangeStart: Float = 0f,
2700 activeRangeEnd: Float = 1f,
2701 @IntRange(from = 0) val steps: Int = 0,
2702 var onValueChangeFinished: (() -> Unit)? = null,
2703 val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
2704 ) {
2705 private var activeRangeStartState by mutableFloatStateOf(activeRangeStart)
2706 private var activeRangeEndState by mutableFloatStateOf(activeRangeEnd)
2707
2708 /** [Float] that indicates the start of the current active range for the [RangeSlider]. */
2709 var activeRangeStart: Float
2710 set(newVal) {
2711 val coercedValue = newVal.coerceIn(valueRange.start, activeRangeEnd)
2712 val snappedValue =
2713 snapValueToTick(
2714 coercedValue,
2715 tickFractions,
2716 valueRange.start,
2717 valueRange.endInclusive
2718 )
2719 activeRangeStartState = snappedValue
2720 }
2721 get() = activeRangeStartState
2722
2723 /** [Float] that indicates the end of the current active range for the [RangeSlider]. */
2724 var activeRangeEnd: Float
2725 set(newVal) {
2726 val coercedValue = newVal.coerceIn(activeRangeStart, valueRange.endInclusive)
2727 val snappedValue =
2728 snapValueToTick(
2729 coercedValue,
2730 tickFractions,
2731 valueRange.start,
2732 valueRange.endInclusive
2733 )
2734 activeRangeEndState = snappedValue
2735 }
2736 get() = activeRangeEndState
2737
2738 internal var onValueChange: ((SliderRange) -> Unit)? = null
2739
2740 internal val tickFractions = stepsToTickFractions(steps)
2741
2742 internal var startThumbWidth by mutableFloatStateOf(0f)
2743 internal var startThumbHeight by mutableFloatStateOf(0f)
2744 internal var endThumbWidth by mutableFloatStateOf(0f)
2745 internal var endThumbHeight by mutableFloatStateOf(0f)
2746 internal var totalWidth by mutableIntStateOf(0)
2747 internal var rawOffsetStart by mutableFloatStateOf(0f)
2748 internal var rawOffsetEnd by mutableFloatStateOf(0f)
2749
2750 internal var isRtl by mutableStateOf(false)
2751
<lambda>null2752 internal val gestureEndAction: (Boolean) -> Unit = { onValueChangeFinished?.invoke() }
2753
2754 private var maxPx by mutableFloatStateOf(0f)
2755 private var minPx by mutableFloatStateOf(0f)
2756
onDragnull2757 internal fun onDrag(isStart: Boolean, offset: Float) {
2758 val offsetRange =
2759 if (isStart) {
2760 rawOffsetStart = (rawOffsetStart + offset)
2761 rawOffsetEnd = scaleToOffset(minPx, maxPx, activeRangeEnd)
2762 val offsetEnd = rawOffsetEnd
2763 var offsetStart = rawOffsetStart.coerceIn(minPx, offsetEnd)
2764 offsetStart = snapValueToTick(offsetStart, tickFractions, minPx, maxPx)
2765 SliderRange(offsetStart, offsetEnd)
2766 } else {
2767 rawOffsetEnd = (rawOffsetEnd + offset)
2768 rawOffsetStart = scaleToOffset(minPx, maxPx, activeRangeStart)
2769 val offsetStart = rawOffsetStart
2770 var offsetEnd = rawOffsetEnd.coerceIn(offsetStart, maxPx)
2771 offsetEnd = snapValueToTick(offsetEnd, tickFractions, minPx, maxPx)
2772 SliderRange(offsetStart, offsetEnd)
2773 }
2774 val scaledUserValue = scaleToUserValue(minPx, maxPx, offsetRange)
2775 if (scaledUserValue != SliderRange(activeRangeStart, activeRangeEnd)) {
2776 if (onValueChange != null) {
2777 onValueChange?.let { it(scaledUserValue) }
2778 } else {
2779 this.activeRangeStart = scaledUserValue.start
2780 this.activeRangeEnd = scaledUserValue.endInclusive
2781 }
2782 }
2783 }
2784
2785 internal val coercedActiveRangeStartAsFraction
2786 get() = calcFraction(valueRange.start, valueRange.endInclusive, activeRangeStart)
2787
2788 internal val coercedActiveRangeEndAsFraction
2789 get() = calcFraction(valueRange.start, valueRange.endInclusive, activeRangeEnd)
2790
2791 internal val startSteps
2792 get() = floor(steps * coercedActiveRangeEndAsFraction).toInt()
2793
2794 internal val endSteps
2795 get() = floor(steps * (1f - coercedActiveRangeStartAsFraction)).toInt()
2796
2797 // scales range offset from within minPx..maxPx to within valueRange.start..valueRange.end
scaleToUserValuenull2798 private fun scaleToUserValue(minPx: Float, maxPx: Float, offset: SliderRange) =
2799 scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
2800
2801 // scales float userValue within valueRange.start..valueRange.end to within minPx..maxPx
2802 private fun scaleToOffset(minPx: Float, maxPx: Float, userValue: Float) =
2803 scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
2804
2805 internal fun updateMinMaxPx() {
2806 val newMaxPx = max(totalWidth - endThumbWidth / 2, 0f)
2807 val newMinPx = min(startThumbWidth / 2, newMaxPx)
2808 if (minPx != newMinPx || maxPx != newMaxPx) {
2809 minPx = newMinPx
2810 maxPx = newMaxPx
2811 rawOffsetStart = scaleToOffset(minPx, maxPx, activeRangeStart)
2812 rawOffsetEnd = scaleToOffset(minPx, maxPx, activeRangeEnd)
2813 }
2814 }
2815
2816 companion object {
2817 /**
2818 * The default [Saver] implementation for [RangeSliderState].
2819 *
2820 * @param onValueChangeFinished lambda to be invoked when value change has ended. This
2821 * callback shouldn't be used to update the range slider values (use [onValueChange] for
2822 * that), but rather to know when the user has completed selecting a new value by ending a
2823 * drag or a click.
2824 * @param valueRange range of values that Range Slider values can take. [activeRangeStart]
2825 * and [activeRangeEnd] will be coerced to this range.
2826 */
Savernull2827 fun Saver(
2828 onValueChangeFinished: (() -> Unit)?,
2829 valueRange: ClosedFloatingPointRange<Float>
2830 ): Saver<RangeSliderState, *> =
2831 listSaver(
2832 save = { listOf(it.activeRangeStart, it.activeRangeEnd, it.steps) },
<lambda>null2833 restore = {
2834 RangeSliderState(
2835 activeRangeStart = it[0] as Float,
2836 activeRangeEnd = it[1] as Float,
2837 steps = it[2] as Int,
2838 onValueChangeFinished = onValueChangeFinished,
2839 valueRange = valueRange
2840 )
2841 }
2842 )
2843 }
2844 }
2845
2846 /**
2847 * Creates a [SliderState] that is remembered across compositions.
2848 *
2849 * Changes to the provided initial values will **not** result in the state being recreated or
2850 * changed in any way if it has already been created.
2851 *
2852 * @param activeRangeStart [Float] that indicates the initial start of the active range of the
2853 * slider. If outside of [valueRange] provided, value will be coerced to this range.
2854 * @param activeRangeEnd [Float] that indicates the initial end of the active range of the slider.
2855 * If outside of [valueRange] provided, value will be coerced to this range.
2856 * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
2857 * of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
2858 * distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
2859 * continuously and allow any value from the range. Must not be negative.
2860 * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
2861 * shouldn't be used to update the range slider values (use [RangeSliderState.onValueChange] for
2862 * that), but rather to know when the user has completed selecting a new value by ending a drag or
2863 * a click.
2864 * @param valueRange range of values that Range Slider values can take. [activeRangeStart] and
2865 * [activeRangeEnd] will be coerced to this range.
2866 */
2867 @ExperimentalMaterial3Api
2868 @Composable
rememberRangeSliderStatenull2869 fun rememberRangeSliderState(
2870 activeRangeStart: Float = 0f,
2871 activeRangeEnd: Float = 1f,
2872 @IntRange(from = 0) steps: Int = 0,
2873 onValueChangeFinished: (() -> Unit)? = null,
2874 valueRange: ClosedFloatingPointRange<Float> = 0f..1f
2875 ): RangeSliderState {
2876 return rememberSaveable(saver = RangeSliderState.Saver(onValueChangeFinished, valueRange)) {
2877 RangeSliderState(
2878 activeRangeStart = activeRangeStart,
2879 activeRangeEnd = activeRangeEnd,
2880 steps = steps,
2881 onValueChangeFinished = onValueChangeFinished,
2882 valueRange = valueRange
2883 )
2884 }
2885 }
2886
2887 /**
2888 * Immutable float range for [RangeSlider]
2889 *
2890 * Used in [RangeSlider] to determine the active track range for the component. The range is as
2891 * follows: SliderRange.start..SliderRange.endInclusive.
2892 */
2893 @Immutable
2894 @JvmInline
2895 internal value class SliderRange(val packedValue: Long) {
2896 /** start of the [SliderRange] */
2897 @Stable
2898 val start: Float
2899 get() {
2900 // Explicitly compare against packed values to avoid auto-boxing of Size.Unspecified
<lambda>null2901 check(this.packedValue != Unspecified.packedValue) { "SliderRange is unspecified" }
2902 return unpackFloat1(packedValue)
2903 }
2904
2905 /** End (inclusive) of the [SliderRange] */
2906 @Stable
2907 val endInclusive: Float
2908 get() {
2909 // Explicitly compare against packed values to avoid auto-boxing of Size.Unspecified
<lambda>null2910 check(this.packedValue != Unspecified.packedValue) { "SliderRange is unspecified" }
2911 return unpackFloat2(packedValue)
2912 }
2913
2914 companion object {
2915 /**
2916 * Represents an unspecified [SliderRange] value, usually a replacement for `null` when a
2917 * primitive value is desired.
2918 */
2919 @Stable val Unspecified = SliderRange(Float.NaN, Float.NaN)
2920 }
2921
2922 /** String representation of the [SliderRange] */
toStringnull2923 override fun toString() =
2924 if (isSpecified) {
2925 "$start..$endInclusive"
2926 } else {
2927 "FloatRange.Unspecified"
2928 }
2929 }
2930
2931 /**
2932 * Creates a [SliderRange] from a given start and endInclusive float. It requires endInclusive to
2933 * be >= start.
2934 *
2935 * @param start float that indicates the start of the range
2936 * @param endInclusive float that indicates the end of the range
2937 */
2938 @Stable
SliderRangenull2939 internal fun SliderRange(start: Float, endInclusive: Float): SliderRange {
2940 val isUnspecified = start.isNaN() && endInclusive.isNaN()
2941
2942 require(isUnspecified || start <= endInclusive + SliderRangeTolerance) {
2943 "start($start) must be <= endInclusive($endInclusive)"
2944 }
2945 return SliderRange(packFloats(start, endInclusive))
2946 }
2947
2948 /**
2949 * Creates a [SliderRange] from a given [ClosedFloatingPointRange]. It requires
2950 * range.endInclusive >= range.start.
2951 *
2952 * @param range the ClosedFloatingPointRange<Float> for the range.
2953 */
2954 @Stable
SliderRangenull2955 internal fun SliderRange(range: ClosedFloatingPointRange<Float>): SliderRange {
2956 val start = range.start
2957 val endInclusive = range.endInclusive
2958 val isUnspecified = start.isNaN() && endInclusive.isNaN()
2959 require(isUnspecified || start <= endInclusive + SliderRangeTolerance) {
2960 "ClosedFloatingPointRange<Float>.start($start) must be <= " +
2961 "ClosedFloatingPoint.endInclusive($endInclusive)"
2962 }
2963 return SliderRange(packFloats(start, endInclusive))
2964 }
2965
2966 /** Check for if a given [SliderRange] is not [SliderRange.Unspecified]. */
2967 @Stable
2968 internal val SliderRange.isSpecified: Boolean
2969 get() = packedValue != SliderRange.Unspecified.packedValue
2970
2971 internal val CornerSizeAlignmentLine = VerticalAlignmentLine(::min)
2972