diff --git a/test_compose_ui_slider_100/.gitignore b/test_compose_ui_slider_100/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..10cfdbfaf8ed53e905f414c7ae3c4c4050d83105
--- /dev/null
+++ b/test_compose_ui_slider_100/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/test_compose_ui_slider_100/app/.gitignore b/test_compose_ui_slider_100/app/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..67e07b8fea40e2050dce30710b53f2278836ecb9
--- /dev/null
+++ b/test_compose_ui_slider_100/app/.gitignore
@@ -0,0 +1,2 @@
+/build
+/release
diff --git a/test_compose_ui_slider_100/app/build.gradle b/test_compose_ui_slider_100/app/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..eb37cdc0d91a532bafaef2721cb57bb832edfb43
--- /dev/null
+++ b/test_compose_ui_slider_100/app/build.gradle
@@ -0,0 +1,66 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.example.jetpackcompose'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.example.jetpackcompose.slider100"
+ minSdk 21
+ targetSdk 32
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.4.3'
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
+ implementation 'androidx.activity:activity-compose:1.3.1'
+ implementation "androidx.compose.ui:ui:$compose_ui_version"
+ implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
+ implementation "androidx.compose.ui:ui-util:$compose_ui_version"
+ implementation 'androidx.compose.material:material:1.4.3'
+ implementation 'androidx.core:core-ktx:+'
+ implementation 'androidx.core:core-ktx:+'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
+ debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
+ debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/proguard-rules.pro b/test_compose_ui_slider_100/app/proguard-rules.pro
new file mode 100644
index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce
--- /dev/null
+++ b/test_compose_ui_slider_100/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/androidTest/java/com/example/jetpackcompose/ExampleInstrumentedTest.kt b/test_compose_ui_slider_100/app/src/androidTest/java/com/example/jetpackcompose/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..249f64a0dbbf9915c1091ce2fe1bd12fcb806e35
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/androidTest/java/com/example/jetpackcompose/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.jetpackcompose
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.jetpackcompose", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/AndroidManifest.xml b/test_compose_ui_slider_100/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dbf307973b241533eb1ed3abb43bde55f6412a17
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/DragGestureDetectorCopy.kt b/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/DragGestureDetectorCopy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0f83b3768c8bdb2d8b30784b60ae9463613179e5
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/DragGestureDetectorCopy.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material1
+
+// Copy-paste version of DragGestureDetector.kt. Please don't change this file without changing
+// DragGestureDetector.kt
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlin.math.abs
+import kotlin.math.sign
+
+internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
+ pointerId: PointerId,
+ pointerType: PointerType,
+ onPointerSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
+) = awaitPointerSlopOrCancellation(
+ pointerId = pointerId,
+ pointerType = pointerType,
+ onPointerSlopReached = onPointerSlopReached,
+ getDragDirectionValue = { it.x }
+)
+
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+ pointerId: PointerId,
+ pointerType: PointerType,
+ onPointerSlopReached: (PointerInputChange, Float) -> Unit,
+ getDragDirectionValue: (Offset) -> Float
+): PointerInputChange? {
+ if (currentEvent.isPointerUp(pointerId)) {
+ return null // The pointer has already been lifted, so the gesture is canceled
+ }
+ val touchSlop = viewConfiguration.pointerSlop(pointerType)
+ var pointer: PointerId = pointerId
+ var totalPositionChange = 0f
+
+ while (true) {
+ val event = awaitPointerEvent()
+ val dragEvent = event.changes.fastFirstOrNull { it.id == pointer }!!
+ if (dragEvent.isConsumed) {
+ return null
+ } else if (dragEvent.changedToUpIgnoreConsumed()) {
+ val otherDown = event.changes.fastFirstOrNull { it.pressed }
+ if (otherDown == null) {
+ // This is the last "up"
+ return null
+ } else {
+ pointer = otherDown.id
+ }
+ } else {
+ val currentPosition = dragEvent.position
+ val previousPosition = dragEvent.previousPosition
+ val positionChange = getDragDirectionValue(currentPosition) -
+ getDragDirectionValue(previousPosition)
+ totalPositionChange += positionChange
+
+ val inDirection = abs(totalPositionChange)
+ if (inDirection < touchSlop) {
+ // verify that nothing else consumed the drag event
+ awaitPointerEvent(PointerEventPass.Final)
+ if (dragEvent.isConsumed) {
+ return null
+ }
+ } else {
+ onPointerSlopReached(
+ dragEvent,
+ totalPositionChange - (sign(totalPositionChange) * touchSlop)
+ )
+ if (dragEvent.isConsumed) {
+ return dragEvent
+ } else {
+ totalPositionChange = 0f
+ }
+ }
+ }
+ }
+}
+
+private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =
+ changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
+
+private val mouseSlop = 0.125.dp
+private val defaultTouchSlop = 18.dp // The default touch slop on Android devices
+private val mouseToTouchSlopRatio = mouseSlop / defaultTouchSlop
+
+internal fun ViewConfiguration.pointerSlop(pointerType: PointerType): Float {
+ return when (pointerType) {
+ PointerType.Mouse -> touchSlop * mouseToTouchSlopRatio
+ else -> touchSlop
+ }
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Slider.kt b/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Slider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3c471e3712ffc3e87c4ab559ebac7b9a7dcd4a70
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Slider.kt
@@ -0,0 +1,1195 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material1
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.DragScope
+import androidx.compose.foundation.gestures.DraggableState
+import androidx.compose.foundation.gestures.GestureCancellationException
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.horizontalDrag
+import androidx.compose.foundation.hoverable
+import androidx.compose.foundation.indication
+import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSizeIn
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.progressSemantics
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.contentColorFor
+import androidx.compose.material.minimumInteractiveComponentSize
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PointMode
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import kotlin.math.abs
+import kotlin.math.floor
+import kotlin.math.max
+import kotlin.math.min
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Material Design slider.
+ *
+ * Sliders allow users to make selections from a range of values.
+ *
+ * Sliders reflect a range of values along a bar, from which users may select a single value.
+ * They are ideal for adjusting settings such as volume, brightness, or applying image filters.
+ *
+ * ![Sliders image](https://developer.android.com/images/reference/androidx/compose/material/sliders.png)
+ *
+ * Use continuous sliders to allow users to make meaningful selections that don’t
+ * require a specific value:
+ *
+ * @sample androidx.compose.material.samples.SliderSample
+ *
+ * You can allow the user to choose only between predefined set of values by specifying the amount
+ * of steps between min and max values:
+ *
+ * @sample androidx.compose.material.samples.StepsSliderSample
+ *
+ * @param value current value of the Slider. If outside of [valueRange] provided, value will be
+ * coerced to this range.
+ * @param onValueChange lambda in which value should be updated
+ * @param modifier modifiers for the Slider layout
+ * @param enabled whether or not component is enabled and can be interacted with or not
+ * @param valueRange range of values that Slider value can take. Passed [value] will be coerced to
+ * this range
+ * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
+ * between across the whole value range. If 0, slider will behave as a continuous slider and allow
+ * to choose any value from the range specified. Must not be negative.
+ * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
+ * shouldn't be used to update the slider value (use [onValueChange] for that), but rather to
+ * know when the user has completed selecting a new value by ending a drag or a click.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this Slider. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this Slider in different [Interaction]s.
+ * @param colors [SliderColors] that will be used to determine the color of the Slider parts in
+ * different state. See [SliderDefaults.colors] to customize.
+ */
+@Composable
+fun Slider(
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ /*@IntRange(from = 0)*/
+ steps: Int = 0,
+ onValueChangeFinished: (() -> Unit)? = null,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ colors: SliderColors = SliderDefaults.colors()
+) {
+ require(steps >= 0) { "steps should be >= 0" }
+ val onValueChangeState = rememberUpdatedState(onValueChange)
+ val tickFractions = remember(steps) {
+ stepsToTickFractions(steps)
+ }
+ BoxWithConstraints(
+ modifier
+ .minimumInteractiveComponentSize()
+ .requiredSizeIn(minWidth = ThumbRadius * 2, minHeight = ThumbRadius * 2)
+ .sliderSemantics(
+ value,
+ enabled,
+ onValueChange,
+ onValueChangeFinished,
+ valueRange,
+ steps
+ )
+ .focusable(enabled, interactionSource)
+ ) {
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val widthPx = constraints.maxWidth.toFloat()
+ val maxPx: Float
+ val minPx: Float
+
+ with(LocalDensity.current) {
+ maxPx = max(widthPx - ThumbRadius.toPx(), 0f)
+ minPx = min(ThumbRadius.toPx(), maxPx)
+ }
+
+ fun scaleToUserValue(offset: Float) =
+ scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
+
+ fun scaleToOffset(userValue: Float) =
+ scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
+
+ val scope = rememberCoroutineScope()
+ val rawOffset = remember { mutableStateOf(scaleToOffset(value)) }
+ val pressOffset = remember { mutableStateOf(0f) }
+
+ val draggableState = remember(minPx, maxPx, valueRange) {
+ SliderDraggableState {
+ rawOffset.value = (rawOffset.value + it + pressOffset.value)
+ pressOffset.value = 0f
+ val offsetInTrack = rawOffset.value.coerceIn(minPx, maxPx)
+ onValueChangeState.value.invoke(scaleToUserValue(offsetInTrack))
+ }
+ }
+
+ CorrectValueSideEffect(::scaleToOffset, valueRange, minPx..maxPx, rawOffset, value)
+
+ val gestureEndAction = rememberUpdatedState<(Float) -> Unit> { velocity: Float ->
+ val current = rawOffset.value
+ val target = snapValueToTick(current, tickFractions, minPx, maxPx)
+ if (current != target) {
+ scope.launch {
+ animateToTarget(draggableState, current, target, velocity)
+ onValueChangeFinished?.invoke()
+ }
+ } else if (!draggableState.isDragging) {
+ // check ifDragging in case the change is still in progress (touch -> drag case)
+ onValueChangeFinished?.invoke()
+ }
+ }
+ val press = Modifier.sliderTapModifier(
+ draggableState,
+ interactionSource,
+ widthPx,
+ isRtl,
+ rawOffset,
+ gestureEndAction,
+ pressOffset,
+ enabled
+ )
+
+ val drag = Modifier.draggable(
+ orientation = Orientation.Horizontal,
+ reverseDirection = isRtl,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ onDragStopped = { velocity -> gestureEndAction.value.invoke(velocity) },
+ startDragImmediately = draggableState.isDragging,
+ state = draggableState
+ )
+
+ val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive)
+ val fraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced)
+ SliderImpl(
+ enabled,
+ fraction,
+ tickFractions,
+ colors,
+ maxPx - minPx,
+ interactionSource,
+ modifier = press.then(drag)
+ )
+ }
+}
+
+/**
+ * Material Design slider.
+ *
+ * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
+ *
+ * The two values are still bounded by the value range but they also cannot cross each other.
+ *
+ * Use continuous Range Sliders to allow users to make meaningful selections that don’t
+ * require a specific values:
+ *
+ * @sample androidx.compose.material.samples.RangeSliderSample
+ *
+ * You can allow the user to choose only between predefined set of values by specifying the amount
+ * of steps between min and max values:
+ *
+ * @sample androidx.compose.material.samples.StepRangeSliderSample
+ *
+ * @param value current values of the RangeSlider. If either value is outside of [valueRange]
+ * provided, it will be coerced to this range.
+ * @param onValueChange lambda in which values should be updated
+ * @param modifier modifiers for the Range Slider layout
+ * @param enabled whether or not component is enabled and can we interacted with or not
+ * @param valueRange range of values that Range Slider values can take. Passed [value] will be
+ * coerced to this range
+ * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
+ * between across the whole value range. If 0, range slider will behave as a continuous slider and
+ * allow to choose any values from the range specified. Must not be negative.
+ * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
+ * shouldn't be used to update the range slider values (use [onValueChange] for that), but rather to
+ * know when the user has completed selecting a new value by ending a drag or a click.
+ * @param colors [SliderColors] that will be used to determine the color of the Range Slider
+ * parts in different state. See [SliderDefaults.colors] to customize.
+ */
+@Composable
+@ExperimentalMaterialApi
+fun RangeSlider(
+ value: ClosedFloatingPointRange,
+ onValueChange: (ClosedFloatingPointRange) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ /*@IntRange(from = 0)*/
+ steps: Int = 0,
+ onValueChangeFinished: (() -> Unit)? = null,
+ colors: SliderColors = SliderDefaults.colors()
+) {
+ val startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+ val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+
+ require(steps >= 0) { "steps should be >= 0" }
+ val onValueChangeState = rememberUpdatedState(onValueChange)
+ val tickFractions = remember(steps) {
+ stepsToTickFractions(steps)
+ }
+
+ BoxWithConstraints(
+ modifier = modifier
+ .minimumInteractiveComponentSize()
+ .requiredSizeIn(minWidth = ThumbRadius * 4, minHeight = ThumbRadius * 2)
+ ) {
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val widthPx = constraints.maxWidth.toFloat()
+ val maxPx: Float
+ val minPx: Float
+
+ with(LocalDensity.current) {
+ maxPx = widthPx - ThumbRadius.toPx()
+ minPx = ThumbRadius.toPx()
+ }
+
+ fun scaleToUserValue(offset: ClosedFloatingPointRange) =
+ scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
+
+ fun scaleToOffset(userValue: Float) =
+ scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
+
+ val rawOffsetStart = remember { mutableStateOf(scaleToOffset(value.start)) }
+ val rawOffsetEnd = remember { mutableStateOf(scaleToOffset(value.endInclusive)) }
+
+ CorrectValueSideEffect(
+ ::scaleToOffset,
+ valueRange,
+ minPx..maxPx,
+ rawOffsetStart,
+ value.start
+ )
+ CorrectValueSideEffect(
+ ::scaleToOffset,
+ valueRange,
+ minPx..maxPx,
+ rawOffsetEnd,
+ value.endInclusive
+ )
+
+ val scope = rememberCoroutineScope()
+ val gestureEndAction = rememberUpdatedState<(Boolean) -> Unit> { isStart ->
+ val current = (if (isStart) rawOffsetStart else rawOffsetEnd).value
+ // target is a closest anchor to the `current`, if exists
+ val target = snapValueToTick(current, tickFractions, minPx, maxPx)
+ if (current == target) {
+ onValueChangeFinished?.invoke()
+ return@rememberUpdatedState
+ }
+
+ scope.launch {
+ Animatable(initialValue = current).animateTo(
+ target, SliderToTickAnimation,
+ 0f
+ ) {
+ (if (isStart) rawOffsetStart else rawOffsetEnd).value = this.value
+ onValueChangeState.value.invoke(
+ scaleToUserValue(rawOffsetStart.value..rawOffsetEnd.value)
+ )
+ }
+
+ onValueChangeFinished?.invoke()
+ }
+ }
+
+ val onDrag = rememberUpdatedState<(Boolean, Float) -> Unit> { isStart, offset ->
+ val offsetRange = if (isStart) {
+ rawOffsetStart.value = (rawOffsetStart.value + offset)
+ rawOffsetEnd.value = scaleToOffset(value.endInclusive)
+ val offsetEnd = rawOffsetEnd.value
+ val offsetStart = rawOffsetStart.value.coerceIn(minPx, offsetEnd)
+ offsetStart..offsetEnd
+ } else {
+ rawOffsetEnd.value = (rawOffsetEnd.value + offset)
+ rawOffsetStart.value = scaleToOffset(value.start)
+ val offsetStart = rawOffsetStart.value
+ val offsetEnd = rawOffsetEnd.value.coerceIn(offsetStart, maxPx)
+ offsetStart..offsetEnd
+ }
+
+ onValueChangeState.value.invoke(scaleToUserValue(offsetRange))
+ }
+
+ val pressDrag = Modifier.rangeSliderPressDragModifier(
+ startInteractionSource,
+ endInteractionSource,
+ rawOffsetStart,
+ rawOffsetEnd,
+ enabled,
+ isRtl,
+ widthPx,
+ valueRange,
+ gestureEndAction,
+ onDrag,
+ )
+
+ // The positions of the thumbs are dependant on each other.
+ val coercedStart = value.start.coerceIn(valueRange.start, value.endInclusive)
+ val coercedEnd = value.endInclusive.coerceIn(value.start, valueRange.endInclusive)
+ val fractionStart = calcFraction(valueRange.start, valueRange.endInclusive, coercedStart)
+ val fractionEnd = calcFraction(valueRange.start, valueRange.endInclusive, coercedEnd)
+ val startSteps = floor(steps * fractionEnd).toInt()
+ val endSteps = floor(steps * (1f - fractionStart)).toInt()
+
+ val startThumbSemantics = Modifier.sliderSemantics(
+ coercedStart,
+ enabled,
+ { value -> onValueChangeState.value.invoke(value..coercedEnd) },
+ onValueChangeFinished,
+ valueRange.start..coercedEnd,
+ startSteps
+ )
+ val endThumbSemantics = Modifier.sliderSemantics(
+ coercedEnd,
+ enabled,
+ { value -> onValueChangeState.value.invoke(coercedStart..value) },
+ onValueChangeFinished,
+ coercedStart..valueRange.endInclusive,
+ endSteps
+ )
+
+ RangeSliderImpl(
+ enabled,
+ fractionStart,
+ fractionEnd,
+ tickFractions,
+ colors,
+ maxPx - minPx,
+ startInteractionSource,
+ endInteractionSource,
+ modifier = pressDrag,
+ startThumbSemantics,
+ endThumbSemantics
+ )
+ }
+}
+
+/**
+ * Object to hold defaults used by [Slider]
+ */
+object SliderDefaults {
+
+ /**
+ * Creates a [SliderColors] that represents the different colors used in parts of the
+ * [Slider] in different states.
+ *
+ * For the name references below the words "active" and "inactive" are used. Active part of
+ * the slider is filled with progress, so if slider's progress is 30% out of 100%, left (or
+ * right in RTL) 30% of the track will be active, the rest is not active.
+ *
+ * @param thumbColor thumb color when enabled
+ * @param disabledThumbColor thumb colors when disabled
+ * @param activeTrackColor color of the track in the part that is "active", meaning that the
+ * thumb is ahead of it
+ * @param inactiveTrackColor color of the track in the part that is "inactive", meaning that the
+ * thumb is before it
+ * @param disabledActiveTrackColor color of the track in the "active" part when the Slider is
+ * disabled
+ * @param disabledInactiveTrackColor color of the track in the "inactive" part when the
+ * Slider is disabled
+ * @param activeTickColor colors to be used to draw tick marks on the active track, if `steps`
+ * is specified
+ * @param inactiveTickColor colors to be used to draw tick marks on the inactive track, if
+ * `steps` are specified on the Slider is specified
+ * @param disabledActiveTickColor colors to be used to draw tick marks on the active track
+ * when Slider is disabled and when `steps` are specified on it
+ * @param disabledInactiveTickColor colors to be used to draw tick marks on the inactive part
+ * of the track when Slider is disabled and when `steps` are specified on it
+ */
+ @Composable
+ fun colors(
+ thumbColor: Color = MaterialTheme.colors.primary,
+ disabledThumbColor: Color = MaterialTheme.colors.onSurface
+ .copy(alpha = ContentAlpha.disabled)
+ .compositeOver(MaterialTheme.colors.surface),
+ activeTrackColor: Color = MaterialTheme.colors.primary,
+ inactiveTrackColor: Color = activeTrackColor.copy(alpha = InactiveTrackAlpha),
+ disabledActiveTrackColor: Color =
+ MaterialTheme.colors.onSurface.copy(alpha = DisabledActiveTrackAlpha),
+ disabledInactiveTrackColor: Color =
+ disabledActiveTrackColor.copy(alpha = DisabledInactiveTrackAlpha),
+ activeTickColor: Color = contentColorFor(activeTrackColor).copy(alpha = TickAlpha),
+ inactiveTickColor: Color = activeTrackColor.copy(alpha = TickAlpha),
+ disabledActiveTickColor: Color = activeTickColor.copy(alpha = DisabledTickAlpha),
+ disabledInactiveTickColor: Color = disabledInactiveTrackColor
+ .copy(alpha = DisabledTickAlpha)
+ ): SliderColors = DefaultSliderColors(
+ thumbColor = thumbColor,
+ disabledThumbColor = disabledThumbColor,
+ activeTrackColor = activeTrackColor,
+ inactiveTrackColor = inactiveTrackColor,
+ disabledActiveTrackColor = disabledActiveTrackColor,
+ disabledInactiveTrackColor = disabledInactiveTrackColor,
+ activeTickColor = activeTickColor,
+ inactiveTickColor = inactiveTickColor,
+ disabledActiveTickColor = disabledActiveTickColor,
+ disabledInactiveTickColor = disabledInactiveTickColor
+ )
+
+ /**
+ * Default alpha of the inactive part of the track
+ */
+ const val InactiveTrackAlpha = 0.24f
+
+ /**
+ * Default alpha for the track when it is disabled but active
+ */
+ const val DisabledInactiveTrackAlpha = 0.12f
+
+ /**
+ * Default alpha for the track when it is disabled and inactive
+ */
+ const val DisabledActiveTrackAlpha = 0.32f
+
+ /**
+ * Default alpha of the ticks that are drawn on top of the track
+ */
+ const val TickAlpha = 0.54f
+
+ /**
+ * Default alpha for tick marks when they are disabled
+ */
+ const val DisabledTickAlpha = 0.12f
+}
+
+/**
+ * Represents the colors used by a [Slider] and its parts in different states
+ *
+ * See [SliderDefaults.colors] for the default implementation that follows Material
+ * specifications.
+ */
+@Stable
+interface SliderColors {
+
+ /**
+ * Represents the color used for the sliders's thumb, depending on [enabled].
+ *
+ * @param enabled whether the [Slider] is enabled or not
+ */
+ @Composable
+ fun thumbColor(enabled: Boolean): State
+
+ /**
+ * Represents the color used for the sliders's track, depending on [enabled] and [active].
+ *
+ * Active part is filled with progress, so if sliders progress is 30% out of 100%, left (or
+ * right in RTL) 30% of the track will be active, the rest is not active.
+ *
+ * @param enabled whether the [Slider] is enabled or not
+ * @param active whether the part of the track is active of not
+ */
+ @Composable
+ fun trackColor(enabled: Boolean, active: Boolean): State
+
+ /**
+ * Represents the color used for the sliders's tick which is the dot separating steps, if
+ * they are set on the slider, depending on [enabled] and [active].
+ *
+ * Active tick is the tick that is in the part of the track filled with progress, so if
+ * sliders progress is 30% out of 100%, left (or right in RTL) 30% of the track and the ticks
+ * in this 30% will be active, the rest is not active.
+ *
+ * @param enabled whether the [Slider] is enabled or not
+ * @param active whether the part of the track this tick is in is active of not
+ */
+ @Composable
+ fun tickColor(enabled: Boolean, active: Boolean): State
+}
+
+@Composable
+private fun SliderImpl(
+ enabled: Boolean,
+ positionFraction: Float,
+ tickFractions: List,
+ colors: SliderColors,
+ width: Float,
+ interactionSource: MutableInteractionSource,
+ modifier: Modifier
+) {
+ Box(modifier.then(DefaultSliderConstraints)) {
+ val trackStrokeWidth: Float
+ val thumbPx: Float
+ val widthDp: Dp
+ with(LocalDensity.current) {
+ trackStrokeWidth = TrackHeight.toPx()
+ thumbPx = ThumbRadius.toPx()
+ widthDp = width.toDp()
+ }
+
+ val thumbSize = ThumbRadius * 2
+ val offset = widthDp * positionFraction
+
+ Track(
+ Modifier.fillMaxSize(),
+ colors,
+ enabled,
+ 0f,
+ positionFraction,
+ tickFractions,
+ thumbPx,
+ trackStrokeWidth
+ )
+ SliderThumb(Modifier, offset, interactionSource, colors, enabled, thumbSize)
+ }
+}
+
+
+@Composable
+private fun RangeSliderImpl(
+ enabled: Boolean,
+ positionFractionStart: Float,
+ positionFractionEnd: Float,
+ tickFractions: List,
+ colors: SliderColors,
+ width: Float,
+ startInteractionSource: MutableInteractionSource,
+ endInteractionSource: MutableInteractionSource,
+ modifier: Modifier,
+ startThumbSemantics: Modifier,
+ endThumbSemantics: Modifier
+) {
+
+ val startContentDescription = getString(Strings.SliderRangeStart)
+ val endContentDescription = getString(Strings.SliderRangeEnd)
+ Box(modifier.then(DefaultSliderConstraints)) {
+ val trackStrokeWidth: Float
+ val thumbPx: Float
+ val widthDp: Dp
+ with(LocalDensity.current) {
+ trackStrokeWidth = TrackHeight.toPx()
+ thumbPx = ThumbRadius.toPx()
+ widthDp = width.toDp()
+ }
+
+ val thumbSize = ThumbRadius * 2
+ val offsetStart = widthDp * positionFractionStart
+ val offsetEnd = widthDp * positionFractionEnd
+ Track(
+ Modifier
+ .align(Alignment.CenterStart)
+ .fillMaxSize(),
+ colors,
+ enabled,
+ positionFractionStart,
+ positionFractionEnd,
+ tickFractions,
+ thumbPx,
+ trackStrokeWidth
+ )
+
+ SliderThumb(
+ Modifier
+ .semantics(mergeDescendants = true) { contentDescription = startContentDescription }
+ .focusable(true, startInteractionSource)
+ .then(startThumbSemantics),
+ offsetStart,
+ startInteractionSource,
+ colors,
+ enabled,
+ thumbSize
+ )
+ SliderThumb(
+ Modifier
+ .semantics(mergeDescendants = true) { contentDescription = endContentDescription }
+ .focusable(true, endInteractionSource)
+ .then(endThumbSemantics),
+ offsetEnd,
+ endInteractionSource,
+ colors,
+ enabled,
+ thumbSize
+ )
+ }
+}
+
+@Composable
+private fun BoxScope.SliderThumb(
+ modifier: Modifier,
+ offset: Dp,
+ interactionSource: MutableInteractionSource,
+ colors: SliderColors,
+ enabled: Boolean,
+ thumbSize: Dp
+) {
+ Box(
+ Modifier
+ .padding(start = offset)
+ .align(Alignment.CenterStart)) {
+ val interactions = remember { mutableStateListOf() }
+ LaunchedEffect(interactionSource) {
+ interactionSource.interactions.collect { interaction ->
+ when (interaction) {
+ is PressInteraction.Press -> interactions.add(interaction)
+ is PressInteraction.Release -> interactions.remove(interaction.press)
+ is PressInteraction.Cancel -> interactions.remove(interaction.press)
+ is DragInteraction.Start -> interactions.add(interaction)
+ is DragInteraction.Stop -> interactions.remove(interaction.start)
+ is DragInteraction.Cancel -> interactions.remove(interaction.start)
+ }
+ }
+ }
+
+ val elevation = if (interactions.isNotEmpty()) {
+ ThumbPressedElevation
+ } else {
+ ThumbDefaultElevation
+ }
+ Spacer(
+ modifier
+ .size(thumbSize, thumbSize)
+// .indication(
+// interactionSource = interactionSource,
+// indication = rememberRipple(bounded = false, radius = ThumbRippleRadius)
+// )
+ .hoverable(interactionSource = interactionSource)
+// .shadow(if (enabled) elevation else 0.dp, CircleShape, clip = false)
+ .background(colors.thumbColor(enabled).value, CircleShape)
+ )
+ }
+}
+
+@Composable
+private fun Track(
+ modifier: Modifier,
+ colors: SliderColors,
+ enabled: Boolean,
+ positionFractionStart: Float,
+ positionFractionEnd: Float,
+ tickFractions: List,
+ thumbPx: Float,
+ trackStrokeWidth: Float
+) {
+ val inactiveTrackColor = colors.trackColor(enabled, active = false)
+ val activeTrackColor = colors.trackColor(enabled, active = true)
+ val inactiveTickColor = colors.tickColor(enabled, active = false)
+ val activeTickColor = colors.tickColor(enabled, active = true)
+ Canvas(modifier) {
+ val isRtl = layoutDirection == LayoutDirection.Rtl
+ val sliderLeft = Offset(thumbPx, center.y)
+ val sliderRight = Offset(size.width - thumbPx, center.y)
+ val sliderStart = if (isRtl) sliderRight else sliderLeft
+ val sliderEnd = if (isRtl) sliderLeft else sliderRight
+ drawLine(
+ inactiveTrackColor.value,
+ sliderStart,
+ sliderEnd,
+ trackStrokeWidth,
+ StrokeCap.Round
+ )
+ val sliderValueEnd = Offset(
+ sliderStart.x + (sliderEnd.x - sliderStart.x) * positionFractionEnd,
+ center.y
+ )
+
+ val sliderValueStart = Offset(
+ sliderStart.x + (sliderEnd.x - sliderStart.x) * positionFractionStart,
+ center.y
+ )
+
+ drawLine(
+ activeTrackColor.value,
+ sliderValueStart,
+ sliderValueEnd,
+ trackStrokeWidth,
+ StrokeCap.Round
+ )
+ tickFractions.groupBy { it > positionFractionEnd || it < positionFractionStart }
+ .forEach { (outsideFraction, list) ->
+ drawPoints(
+ list.map {
+ Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
+ },
+ PointMode.Points,
+ (if (outsideFraction) inactiveTickColor else activeTickColor).value,
+ trackStrokeWidth,
+ StrokeCap.Round
+ )
+ }
+ }
+}
+
+private fun snapValueToTick(
+ current: Float,
+ tickFractions: List,
+ minPx: Float,
+ maxPx: Float
+): Float {
+ // target is a closest anchor to the `current`, if exists
+ return tickFractions
+ .minByOrNull { abs(lerp(minPx, maxPx, it) - current) }
+ ?.run { lerp(minPx, maxPx, this) }
+ ?: current
+}
+
+private suspend fun AwaitPointerEventScope.awaitSlop(
+ id: PointerId,
+ type: PointerType
+): Pair? {
+ var initialDelta = 0f
+ val postPointerSlop = { pointerInput: PointerInputChange, offset: Float ->
+ pointerInput.consume()
+ initialDelta = offset
+ }
+ val afterSlopResult = awaitHorizontalPointerSlopOrCancellation(id, type, postPointerSlop)
+ return if (afterSlopResult != null) afterSlopResult to initialDelta else null
+}
+
+private fun stepsToTickFractions(steps: Int): List {
+ return if (steps == 0) emptyList() else List(steps + 2) { it.toFloat() / (steps + 1) }
+}
+
+// Scale x1 from a1..b1 range to a2..b2 range
+private fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) =
+ lerp(a2, b2, calcFraction(a1, b1, x1))
+
+// Scale x.start, x.endInclusive from a1..b1 range to a2..b2 range
+private fun scale(a1: Float, b1: Float, x: ClosedFloatingPointRange, a2: Float, b2: Float) =
+ scale(a1, b1, x.start, a2, b2)..scale(a1, b1, x.endInclusive, a2, b2)
+
+// Calculate the 0..1 fraction that `pos` value represents between `a` and `b`
+private fun calcFraction(a: Float, b: Float, pos: Float) =
+ (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f)
+
+@Composable
+private fun CorrectValueSideEffect(
+ scaleToOffset: (Float) -> Float,
+ valueRange: ClosedFloatingPointRange,
+ trackRange: ClosedFloatingPointRange,
+ valueState: MutableState,
+ value: Float
+) {
+ SideEffect {
+ val error = (valueRange.endInclusive - valueRange.start) / 1000
+ val newOffset = scaleToOffset(value)
+ if (abs(newOffset - valueState.value) > error) {
+ if (valueState.value in trackRange) {
+ valueState.value = newOffset
+ }
+ }
+ }
+}
+
+private fun Modifier.sliderSemantics(
+ value: Float,
+ enabled: Boolean,
+ onValueChange: (Float) -> Unit,
+ onValueChangeFinished: (() -> Unit)? = null,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ steps: Int = 0
+): Modifier {
+ val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive)
+ return semantics {
+ if (!enabled) disabled()
+ setProgress(
+ action = { targetValue ->
+ var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive)
+ val originalVal = newValue
+ val resolvedValue = if (steps > 0) {
+ var distance: Float = newValue
+ for (i in 0..steps + 1) {
+ val stepValue = lerp(
+ valueRange.start,
+ valueRange.endInclusive,
+ i.toFloat() / (steps + 1))
+ if (abs(stepValue - originalVal) <= distance) {
+ distance = abs(stepValue - originalVal)
+ newValue = stepValue
+ }
+ }
+ newValue
+ } else {
+ newValue
+ }
+ // This is to keep it consistent with AbsSeekbar.java: return false if no
+ // change from current.
+ if (resolvedValue == coerced) {
+ false
+ } else {
+ onValueChange(resolvedValue)
+ onValueChangeFinished?.invoke()
+ true
+ }
+ }
+ )
+ }.progressSemantics(value, valueRange, steps)
+}
+
+private fun Modifier.sliderTapModifier(
+ draggableState: DraggableState,
+ interactionSource: MutableInteractionSource,
+ maxPx: Float,
+ isRtl: Boolean,
+ rawOffset: State,
+ gestureEndAction: State<(Float) -> Unit>,
+ pressOffset: MutableState,
+ enabled: Boolean
+) = composed(
+ factory = {
+ if (enabled) {
+ val scope = rememberCoroutineScope()
+ pointerInput(draggableState, interactionSource, maxPx, isRtl) {
+ detectTapGestures(
+ onPress = { pos ->
+ val to = if (isRtl) maxPx - pos.x else pos.x
+ pressOffset.value = to - rawOffset.value
+ try {
+ awaitRelease()
+ } catch (_: GestureCancellationException) {
+ pressOffset.value = 0f
+ }
+ },
+ onTap = {
+ scope.launch {
+ draggableState.drag(MutatePriority.UserInput) {
+ // just trigger animation, press offset will be applied
+ dragBy(0f)
+ }
+ gestureEndAction.value.invoke(0f)
+ }
+ }
+ )
+ }
+ } else {
+ this
+ }
+ },
+ inspectorInfo = debugInspectorInfo {
+ name = "sliderTapModifier"
+ properties["draggableState"] = draggableState
+ properties["interactionSource"] = interactionSource
+ properties["maxPx"] = maxPx
+ properties["isRtl"] = isRtl
+ properties["rawOffset"] = rawOffset
+ properties["gestureEndAction"] = gestureEndAction
+ properties["pressOffset"] = pressOffset
+ properties["enabled"] = enabled
+ })
+
+private suspend fun animateToTarget(
+ draggableState: DraggableState,
+ current: Float,
+ target: Float,
+ velocity: Float
+) {
+ draggableState.drag {
+ var latestValue = current
+ Animatable(initialValue = current).animateTo(target, SliderToTickAnimation, velocity) {
+ dragBy(this.value - latestValue)
+ latestValue = this.value
+ }
+ }
+}
+
+private fun Modifier.rangeSliderPressDragModifier(
+ startInteractionSource: MutableInteractionSource,
+ endInteractionSource: MutableInteractionSource,
+ rawOffsetStart: State,
+ rawOffsetEnd: State,
+ enabled: Boolean,
+ isRtl: Boolean,
+ maxPx: Float,
+ valueRange: ClosedFloatingPointRange,
+ gestureEndAction: State<(Boolean) -> Unit>,
+ onDrag: State<(Boolean, Float) -> Unit>,
+): Modifier =
+ if (enabled) {
+ pointerInput(startInteractionSource, endInteractionSource, maxPx, isRtl, valueRange) {
+ val rangeSliderLogic = RangeSliderLogic(
+ startInteractionSource,
+ endInteractionSource,
+ rawOffsetStart,
+ rawOffsetEnd,
+ onDrag
+ )
+ coroutineScope {
+ awaitEachGesture {
+ val event = awaitFirstDown(requireUnconsumed = false)
+ val interaction = DragInteraction.Start()
+ var posX = if (isRtl) maxPx - event.position.x else event.position.x
+ val compare = rangeSliderLogic.compareOffsets(posX)
+ var draggingStart = if (compare != 0) {
+ compare < 0
+ } else {
+ rawOffsetStart.value > posX
+ }
+
+ awaitSlop(event.id, event.type)?.let {
+ val slop = viewConfiguration.pointerSlop(event.type)
+ val shouldUpdateCapturedThumb = abs(rawOffsetEnd.value - posX) < slop &&
+ abs(rawOffsetStart.value - posX) < slop
+ if (shouldUpdateCapturedThumb) {
+ val dir = it.second
+ draggingStart = if (isRtl) dir >= 0f else dir < 0f
+ posX += it.first.positionChange().x
+ }
+ }
+
+ rangeSliderLogic.captureThumb(
+ draggingStart,
+ posX,
+ interaction,
+ this@coroutineScope
+ )
+
+ val finishInteraction = try {
+ val success = horizontalDrag(pointerId = event.id) {
+ val deltaX = it.positionChange().x
+ onDrag.value.invoke(draggingStart, if (isRtl) -deltaX else deltaX)
+ }
+ if (success) {
+ DragInteraction.Stop(interaction)
+ } else {
+ DragInteraction.Cancel(interaction)
+ }
+ } catch (e: CancellationException) {
+ DragInteraction.Cancel(interaction)
+ }
+
+ gestureEndAction.value.invoke(draggingStart)
+ launch {
+ rangeSliderLogic
+ .activeInteraction(draggingStart)
+ .emit(finishInteraction)
+ }
+ }
+ }
+ }
+ } else {
+ this
+ }
+
+private class RangeSliderLogic(
+ val startInteractionSource: MutableInteractionSource,
+ val endInteractionSource: MutableInteractionSource,
+ val rawOffsetStart: State,
+ val rawOffsetEnd: State,
+ val onDrag: State<(Boolean, Float) -> Unit>,
+) {
+ fun activeInteraction(draggingStart: Boolean): MutableInteractionSource =
+ if (draggingStart) startInteractionSource else endInteractionSource
+
+ fun compareOffsets(eventX: Float): Int {
+ val diffStart = abs(rawOffsetStart.value - eventX)
+ val diffEnd = abs(rawOffsetEnd.value - eventX)
+ return diffStart.compareTo(diffEnd)
+ }
+
+ fun captureThumb(
+ draggingStart: Boolean,
+ posX: Float,
+ interaction: Interaction,
+ scope: CoroutineScope
+ ) {
+ onDrag.value.invoke(
+ draggingStart,
+ posX - if (draggingStart) rawOffsetStart.value else rawOffsetEnd.value
+ )
+ scope.launch {
+ activeInteraction(draggingStart).emit(interaction)
+ }
+ }
+}
+
+@Immutable
+private class DefaultSliderColors(
+ private val thumbColor: Color,
+ private val disabledThumbColor: Color,
+ private val activeTrackColor: Color,
+ private val inactiveTrackColor: Color,
+ private val disabledActiveTrackColor: Color,
+ private val disabledInactiveTrackColor: Color,
+ private val activeTickColor: Color,
+ private val inactiveTickColor: Color,
+ private val disabledActiveTickColor: Color,
+ private val disabledInactiveTickColor: Color
+) : SliderColors {
+
+ @Composable
+ override fun thumbColor(enabled: Boolean): State {
+ return rememberUpdatedState(if (enabled) thumbColor else disabledThumbColor)
+ }
+
+ @Composable
+ override fun trackColor(enabled: Boolean, active: Boolean): State {
+ return rememberUpdatedState(
+ if (enabled) {
+ if (active) activeTrackColor else inactiveTrackColor
+ } else {
+ if (active) disabledActiveTrackColor else disabledInactiveTrackColor
+ }
+ )
+ }
+
+ @Composable
+ override fun tickColor(enabled: Boolean, active: Boolean): State {
+ return rememberUpdatedState(
+ if (enabled) {
+ if (active) activeTickColor else inactiveTickColor
+ } else {
+ if (active) disabledActiveTickColor else disabledInactiveTickColor
+ }
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as DefaultSliderColors
+
+ if (thumbColor != other.thumbColor) return false
+ if (disabledThumbColor != other.disabledThumbColor) return false
+ if (activeTrackColor != other.activeTrackColor) return false
+ if (inactiveTrackColor != other.inactiveTrackColor) return false
+ if (disabledActiveTrackColor != other.disabledActiveTrackColor) return false
+ if (disabledInactiveTrackColor != other.disabledInactiveTrackColor) return false
+ if (activeTickColor != other.activeTickColor) return false
+ if (inactiveTickColor != other.inactiveTickColor) return false
+ if (disabledActiveTickColor != other.disabledActiveTickColor) return false
+ if (disabledInactiveTickColor != other.disabledInactiveTickColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = thumbColor.hashCode()
+ result = 31 * result + disabledThumbColor.hashCode()
+ result = 31 * result + activeTrackColor.hashCode()
+ result = 31 * result + inactiveTrackColor.hashCode()
+ result = 31 * result + disabledActiveTrackColor.hashCode()
+ result = 31 * result + disabledInactiveTrackColor.hashCode()
+ result = 31 * result + activeTickColor.hashCode()
+ result = 31 * result + inactiveTickColor.hashCode()
+ result = 31 * result + disabledActiveTickColor.hashCode()
+ result = 31 * result + disabledInactiveTickColor.hashCode()
+ return result
+ }
+}
+
+// Internal to be referred to in tests
+internal val ThumbRadius = 10.dp
+private val ThumbRippleRadius = 24.dp
+private val ThumbDefaultElevation = 1.dp
+private val ThumbPressedElevation = 6.dp
+
+// Internal to be referred to in tests
+internal val TrackHeight = 4.dp
+private val SliderHeight = 48.dp
+private val SliderMinWidth = 144.dp // TODO: clarify min width
+private val DefaultSliderConstraints =
+ Modifier
+ .widthIn(min = SliderMinWidth)
+ .heightIn(max = SliderHeight)
+
+private val SliderToTickAnimation = TweenSpec(durationMillis = 100)
+
+private class SliderDraggableState(
+ val onDelta: (Float) -> Unit
+) : DraggableState {
+
+ var isDragging by mutableStateOf(false)
+ private set
+
+ private val dragScope: DragScope = object : DragScope {
+ override fun dragBy(pixels: Float): Unit = onDelta(pixels)
+ }
+
+ private val scrollMutex = MutatorMutex()
+
+ override suspend fun drag(
+ dragPriority: MutatePriority,
+ block: suspend DragScope.() -> Unit
+ ): Unit = coroutineScope {
+ isDragging = true
+ scrollMutex.mutateWith(dragScope, dragPriority, block)
+ isDragging = false
+ }
+
+ override fun dispatchRawDelta(delta: Float) {
+ return onDelta(delta)
+ }
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Strings.android.kt b/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Strings.android.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ba7d864f0704fdd54d3c31e26d49dd08c5415364
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Strings.android.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material1
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.R
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+internal fun getString(string: Strings): String {
+ LocalConfiguration.current
+ val resources = LocalContext.current.resources
+ return when (string) {
+ Strings.NavigationMenu -> resources.getString(R.string.navigation_menu)
+ Strings.CloseDrawer -> resources.getString(R.string.close_drawer)
+ Strings.CloseSheet -> resources.getString(R.string.close_sheet)
+ Strings.DefaultErrorMessage -> resources.getString(R.string.default_error_message)
+ Strings.ExposedDropdownMenu -> resources.getString(R.string.dropdown_menu)
+ Strings.SliderRangeStart -> resources.getString(R.string.range_start)
+ Strings.SliderRangeEnd -> resources.getString(R.string.range_end)
+ else -> ""
+ }
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Strings.kt b/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Strings.kt
new file mode 100644
index 0000000000000000000000000000000000000000..091db652c56bd4d9b076a558168971fe0fdfb816
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/androidx/compose/material1/Strings.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material1
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+@kotlin.jvm.JvmInline
+internal value class Strings private constructor(@Suppress("unused") private val value: Int) {
+ companion object {
+ val NavigationMenu = Strings(0)
+ val CloseDrawer = Strings(1)
+ val CloseSheet = Strings(2)
+ val DefaultErrorMessage = Strings(3)
+ val ExposedDropdownMenu = Strings(4)
+ val SliderRangeStart = Strings(5)
+ val SliderRangeEnd = Strings(6)
+ }
+}
diff --git a/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/MainActivity.kt b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/MainActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1f128866417bb5174519efefc5552e4a6de05e6f
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/MainActivity.kt
@@ -0,0 +1,51 @@
+package com.example.jetpackcompose
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.*
+import androidx.compose.material1.Slider
+import androidx.compose.material.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.jetpackcompose.ui.theme.JetpackComposeTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ JetpackComposeTheme {
+ // A surface container using the 'background' color from the theme
+ Row {
+ var sliderPosition by remember { mutableStateOf(0f) }
+ repeat(4) {
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ for (i in 1..25) {
+ Slider(value = sliderPosition, modifier = Modifier
+ .height(25.dp), onValueChange = { sliderPosition = it })
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun Greeting(name: String) {
+ Text(text = "Hello $name!")
+}
+
+@Preview(showBackground = true)
+@Composable
+fun DefaultPreview() {
+ JetpackComposeTheme {
+ Greeting("Android")
+ }
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Color.kt b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Color.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fff6c866dc14478e948e525d28b8ab0fb69f4f86
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Color.kt
@@ -0,0 +1,8 @@
+package com.example.jetpackcompose.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple200 = Color(0xFFBB86FC)
+val Purple500 = Color(0xFF6200EE)
+val Purple700 = Color(0xFF3700B3)
+val Teal200 = Color(0xFF03DAC5)
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Shape.kt b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Shape.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f44b5917e2de039d0393b35d29cd54f6bac6173b
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Shape.kt
@@ -0,0 +1,11 @@
+package com.example.jetpackcompose.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.ui.unit.dp
+
+val Shapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(4.dp),
+ large = RoundedCornerShape(0.dp)
+)
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Theme.kt b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Theme.kt
new file mode 100644
index 0000000000000000000000000000000000000000..885909a891619aa32c91237ea19e43484c5afb76
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Theme.kt
@@ -0,0 +1,47 @@
+package com.example.jetpackcompose.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+
+private val DarkColorPalette = darkColors(
+ primary = Purple200,
+ primaryVariant = Purple700,
+ secondary = Teal200
+)
+
+private val LightColorPalette = lightColors(
+ primary = Purple500,
+ primaryVariant = Purple700,
+ secondary = Teal200
+
+ /* Other default colors to override
+ background = Color.White,
+ surface = Color.White,
+ onPrimary = Color.White,
+ onSecondary = Color.Black,
+ onBackground = Color.Black,
+ onSurface = Color.Black,
+ */
+)
+
+@Composable
+fun JetpackComposeTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val colors = if (darkTheme) {
+ DarkColorPalette
+ } else {
+ LightColorPalette
+ }
+
+ MaterialTheme(
+ colors = colors,
+ typography = Typography,
+ shapes = Shapes,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Type.kt b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Type.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9a8b184abc1f41a81ce72cb29e3c22f05ba4cd9b
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/java/com/example/jetpackcompose/ui/theme/Type.kt
@@ -0,0 +1,28 @@
+package com.example.jetpackcompose.ui.theme
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ body1 = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp
+ )
+ /* Other default text styles to override
+ button = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/test_compose_ui_slider_100/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2b068d11462a4b96669193de13a711a3a36220a0
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/res/drawable/ic_launcher_background.xml b/test_compose_ui_slider_100/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000000000000000000000000000000000..07d5da9cbf141911847041df5d7b87f0dd5ef9d4
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/test_compose_ui_slider_100/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eca70cfe52eac1ba66ba280a68ca7be8fcf88a16
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/test_compose_ui_slider_100/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eca70cfe52eac1ba66ba280a68ca7be8fcf88a16
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..62b611da081676d42f6c3f78a2c91e7bcedddedb
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..28d4b77f9f036a47549d47db79c16788749dca10
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..9287f5083623b375139afb391af71cc533a7dd37
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..aa7d6427e6fa1074b79ccd52ef67ac15c5637e85
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/test_compose_ui_slider_100/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..9126ae37cbc3587421d6889eadd1d91fbf1994d4
Binary files /dev/null and b/test_compose_ui_slider_100/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/test_compose_ui_slider_100/app/src/main/res/values/colors.xml b/test_compose_ui_slider_100/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f8c6127d327620c93d2b2d00342a68e97b98a48d
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/res/values/strings.xml b/test_compose_ui_slider_100/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..317470401e528cfe944377fb3eee84a0299c26c4
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ ComposeUISlider100
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/res/values/themes.xml b/test_compose_ui_slider_100/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b99644b24c99490d0d2ea10d2103c418c1ec43ba
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/res/xml/backup_rules.xml b/test_compose_ui_slider_100/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fa0f996d2c2a6bdd11f5371de4268c8389d6c720
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/main/res/xml/data_extraction_rules.xml b/test_compose_ui_slider_100/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9ee9997b0b4726e57c27b2f7b21462b604ff8a88
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/app/src/test/java/com/example/jetpackcompose/ExampleUnitTest.kt b/test_compose_ui_slider_100/app/src/test/java/com/example/jetpackcompose/ExampleUnitTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a92f2dedfa2774f84cf677cec87638331a3fc81e
--- /dev/null
+++ b/test_compose_ui_slider_100/app/src/test/java/com/example/jetpackcompose/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.jetpackcompose
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/build.gradle b/test_compose_ui_slider_100/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..4b0e7f00199a56e9893c296dfd685878c43c9624
--- /dev/null
+++ b/test_compose_ui_slider_100/build.gradle
@@ -0,0 +1,11 @@
+buildscript {
+ ext {
+ compose_ui_version = '1.3.2'
+ agp_version = '8.1.0'
+ }
+}// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '8.1.0' apply false
+ id 'com.android.library' version '7.4.2' apply false
+ id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
+}
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/gradle.properties b/test_compose_ui_slider_100/gradle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..3c5031eb7d63f785752b1914cc8692a453d1cc63
--- /dev/null
+++ b/test_compose_ui_slider_100/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/test_compose_ui_slider_100/gradle/wrapper/gradle-wrapper.jar b/test_compose_ui_slider_100/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f
Binary files /dev/null and b/test_compose_ui_slider_100/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/test_compose_ui_slider_100/gradle/wrapper/gradle-wrapper.properties b/test_compose_ui_slider_100/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000000000000000000000000000000000..a4af7ce46c68a74abc50304e88fdd97ba39eb603
--- /dev/null
+++ b/test_compose_ui_slider_100/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jul 28 13:40:34 CST 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/test_compose_ui_slider_100/gradlew b/test_compose_ui_slider_100/gradlew
new file mode 100644
index 0000000000000000000000000000000000000000..4f906e0c811fc9e230eb44819f509cd0627f2600
--- /dev/null
+++ b/test_compose_ui_slider_100/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/test_compose_ui_slider_100/gradlew.bat b/test_compose_ui_slider_100/gradlew.bat
new file mode 100644
index 0000000000000000000000000000000000000000..107acd32c4e687021ef32db511e8a206129b88ec
--- /dev/null
+++ b/test_compose_ui_slider_100/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/test_compose_ui_slider_100/settings.gradle b/test_compose_ui_slider_100/settings.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..93c5c1521c906144e8167e16031005a9a9a300e0
--- /dev/null
+++ b/test_compose_ui_slider_100/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "JetpackCompose"
+include ':app'
diff --git a/test_compose_ui_slider_100/slider100 b/test_compose_ui_slider_100/slider100
new file mode 100644
index 0000000000000000000000000000000000000000..2171818bc38141868c383a08ee9568d48d15dd89
Binary files /dev/null and b/test_compose_ui_slider_100/slider100 differ
diff --git a/test_compose_ui_slider_100/test_compose_ui_slider_100.apk b/test_compose_ui_slider_100/test_compose_ui_slider_100.apk
new file mode 100644
index 0000000000000000000000000000000000000000..ed5a20b8b2f3ab9301083ea6fd82aa3fb5a71799
Binary files /dev/null and b/test_compose_ui_slider_100/test_compose_ui_slider_100.apk differ