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