diff --git a/library/src/commonMain/kotlin/ir/mahozad/multiplatform/wavyslider/material/WavySlider.kt b/library/src/commonMain/kotlin/ir/mahozad/multiplatform/wavyslider/material/WavySlider.kt index bd9fcde..06e3656 100644 --- a/library/src/commonMain/kotlin/ir/mahozad/multiplatform/wavyslider/material/WavySlider.kt +++ b/library/src/commonMain/kotlin/ir/mahozad/multiplatform/wavyslider/material/WavySlider.kt @@ -1,4 +1,4 @@ -// Based on https://github.com/JetBrains/compose-multiplatform-core/blob/release/1.5.12/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt +// Based on https://github.com/JetBrains/compose-multiplatform-core/blob/release/1.6.0/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt @file:Suppress("UnusedReceiverParameter") @@ -90,28 +90,26 @@ val SliderDefaults.Incremental: Boolean get() = defaultIncremental val SliderDefaults.WaveAnimationSpecs: WaveAnimationSpecs get() = defaultWaveAnimationSpecs /** - * A wavy slider much like the [Material Design 2 slider](https://m2.material.io/components/sliders). + * A wavy slider much like the [Material Design 2 Slider](https://m2.material.io/components/sliders). * - * Setting [waveHeight] or [waveLength] to `0.dp` results in a regular Material [Slider]. + * Setting [waveHeight] or [waveLength] to `0.dp` results in a regular [Slider]. * - * This component can also be used as a progress bar. - * - * Note that range sliders do not make sense for the wavy slider. + * Note that range sliders do not make sense for the WavySlider. * So, there is no RangeWavySlider counterpart. * * @param value current value of the WavySlider. Will be coerced to [valueRange]. - * @param onValueChange lambda in which value should be updated - * @param modifier modifiers for the WavySlider layout - * @param enabled whether or not component is enabled and can be interacted with or not + * @param onValueChange lambda in which value should be updated. + * @param modifier modifiers for the WavySlider layout. + * @param enabled whether or not component is enabled and can be interacted with or not. * @param valueRange range of values that WavySlider value can take. Passed [value] will be coerced to * this range. * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback - * shouldn't be used to update the wavy slider value (use [onValueChange] for that), but rather to + * shouldn't be used to update the WavySlider 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 WavySlider. You can create and pass in your own remembered * [MutableInteractionSource] if you want to observe [Interaction]s and customize the - * appearance / behavior of this WavySlider in different [Interaction]s. + * appearance / behavior of this Slider in different [Interaction]s. * @param colors [SliderColors] that will be used to determine the color of the WavySlider parts in * different state. See [SliderDefaults.colors] to customize. * @@ -226,7 +224,7 @@ fun WavySlider( val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive) val fraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced) - SliderImpl( + WavySliderImpl( enabled, fraction, colors, @@ -332,7 +330,7 @@ private fun Modifier.slideOnKeyEvents( } @Composable -private fun SliderImpl( +private fun WavySliderImpl( enabled: Boolean, positionFraction: Float, colors: SliderColors, diff --git a/library/src/commonMain/kotlin/ir/mahozad/multiplatform/wavyslider/material3/WavySlider.kt b/library/src/commonMain/kotlin/ir/mahozad/multiplatform/wavyslider/material3/WavySlider.kt index 38f2f1b..fe93e24 100644 --- a/library/src/commonMain/kotlin/ir/mahozad/multiplatform/wavyslider/material3/WavySlider.kt +++ b/library/src/commonMain/kotlin/ir/mahozad/multiplatform/wavyslider/material3/WavySlider.kt @@ -1,39 +1,40 @@ -// Based on https://github.com/JetBrains/compose-multiplatform-core/blob/release/1.5.12/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt +// Based on https://github.com/JetBrains/compose-multiplatform-core/blob/release/1.6.0/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt @file:Suppress("UnusedReceiverParameter") package ir.mahozad.multiplatform.wavyslider.material3 -import androidx.compose.foundation.* -import androidx.compose.foundation.gestures.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.MutatePriority -import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.requiredSizeIn +import androidx.compose.foundation.progressSemantics import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.setProgress import androidx.compose.ui.unit.* +import androidx.compose.ui.util.fastFirst import ir.mahozad.multiplatform.wavyslider.* -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.math.max -import kotlin.math.min import kotlin.math.roundToInt /* @@ -76,6 +77,79 @@ private val ThumbWidth = SliderTokens.HandleWidth private val ThumbHeight = SliderTokens.HandleHeight private val ThumbSize = DpSize(ThumbWidth, ThumbHeight) +/** + * The Default track for [WavySlider]. + * + * @param sliderState [SliderState] which is used to obtain the current active track. + * @param modifier the [Modifier] to be applied to the track. + * @param colors [SliderColors] that will be used to resolve the colors used for this track in + * different states. See [SliderDefaults.colors]. + * @param enabled controls the enabled state of this slider. When `false`, this component will + * not respond to user input, and it will appear visually disabled and disabled to + * accessibility services. + * + * + * + * @param waveLength the distance over which the wave's shape repeats. + * @param waveHeight the total height of the wave (from crest to trough i.e. amplitude * 2). + * The final rendered height of the wave will be [waveHeight] + [waveThickness]. + * @param waveVelocity the horizontal movement (speed per second and direction) of the whole wave (aka phase shift). + * Setting speed to `0.dp` or less stops the movement. + * @param waveThickness the thickness of the active line (whether animated or not). + * @param trackThickness the thickness of the inactive line. + * @param incremental whether to gradually increase height from zero at start to [waveHeight] at thumb. + * @param animationSpecs animation configurations used for various properties of the wave. + */ +@Composable +@ExperimentalMaterial3Api +fun SliderDefaults.Track( + sliderState: SliderState, + modifier: Modifier = Modifier, + colors: SliderColors = colors(), + enabled: Boolean = true, + ///////////////// + ///////////////// + ///////////////// + waveLength: Dp = SliderDefaults.WaveLength, + waveHeight: Dp = SliderDefaults.WaveHeight, + waveVelocity: WaveVelocity = SliderDefaults.WaveVelocity, + waveThickness: Dp = SliderDefaults.WaveThickness, + trackThickness: Dp = SliderDefaults.TrackThickness, + incremental: Boolean = SliderDefaults.Incremental, + animationSpecs: WaveAnimationSpecs = SliderDefaults.WaveAnimationSpecs +) { + // @Suppress("INVISIBLE_MEMBER") is required to be able to access and use + // trackColor() function which is marked internal in Material library + // See https://stackoverflow.com/q/62500464/8583692 + val inactiveTrackColor = @Suppress("INVISIBLE_MEMBER") colors.trackColor(enabled, active = false) + val activeTrackColor = @Suppress("INVISIBLE_MEMBER") colors.trackColor(enabled, active = true) + val waveHeightAnimated by animateWaveHeight(waveHeight, animationSpecs.waveHeightAnimationSpec) + val waveShiftAnimated by animateWaveShift(waveVelocity, animationSpecs.waveVelocityAnimationSpec) + val trackHeight = max(waveThickness + if (waveHeight < 0.dp) -waveHeight else waveHeight, ThumbSize.height) + Canvas(modifier = modifier.fillMaxWidth().height(trackHeight)) { + val isRtl = layoutDirection == LayoutDirection.Rtl + val sliderLeft = Offset(0f, center.y) + val sliderRight = Offset(size.width, center.y) + val sliderStart = if (isRtl) sliderRight else sliderLeft + val sliderEnd = if (isRtl) sliderLeft else sliderRight + val sliderValueFraction = @Suppress("INVISIBLE_MEMBER") sliderState.coercedValueAsFraction + val sliderValueOffset = Offset(sliderStart.x + (sliderEnd.x - sliderStart.x) * sliderValueFraction, center.y) + drawTrack( + waveLength = waveLength, + waveHeight = waveHeightAnimated, + waveShift = waveShiftAnimated, + waveThickness = waveThickness, + trackThickness = trackThickness, + sliderValueOffset = sliderValueOffset, + sliderStart = sliderStart, + sliderEnd = sliderEnd, + incremental = incremental, + inactiveTrackColor = inactiveTrackColor, + activeTrackColor = activeTrackColor + ) + } +} + /** * The Default track for [WavySlider]. * @@ -100,6 +174,7 @@ private val ThumbSize = DpSize(ThumbWidth, ThumbHeight) * @param animationSpecs animation configurations used for various properties of the wave. */ @Composable +@Deprecated("Use the variant that supports SliderState") fun SliderDefaults.Track( sliderPositions: SliderPositions, modifier: Modifier = Modifier, @@ -141,15 +216,51 @@ fun SliderDefaults.Track( sliderStart = sliderStart, sliderEnd = sliderEnd, incremental = incremental, - inactiveTrackColor = inactiveTrackColor.value, - activeTrackColor = activeTrackColor.value + inactiveTrackColor = inactiveTrackColor, + activeTrackColor = activeTrackColor ) } } /** - * See the other overloaded Composable for documentations. + * A wavy slider much like the [Material Design 3 Slider](https://m3.material.io/components/sliders). + * + * Setting [waveHeight] or [waveLength] to `0.dp` results in a regular [Slider]. + * + * Note that range sliders do not make sense for the WavySlider. + * So, there is no RangeWavySlider counterpart. + * + * It uses [SliderDefaults.Thumb] and [SliderDefaults.Track] as the thumb and track. + * + * @param value current value of the WavySlider. Will be coerced to [valueRange]. + * @param onValueChange callback in which value should be updated. + * @param modifier the [Modifier] to be applied to this WavySlider. + * @param enabled controls the enabled state of this WavySlider. When `false`, this component will not + * respond to user input, and it will appear visually disabled and disabled to accessibility services. + * @param valueRange range of values that this WavySlider can take. The passed [value] will be coerced + * to this range. + * @param onValueChangeFinished called when value change has ended. This should not be used to + * update the WavySlider value (use [onValueChange] instead), 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 resolve the colors used for this WavySlider in + * different states. See [SliderDefaults.colors]. + * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s + * for this WavySlider. You can create and pass in your own `remember`ed instance to observe + * [Interaction]s and customize the appearance / behavior of this slider in different states. + * + * + * + * @param waveLength the distance over which the wave's shape repeats. + * @param waveHeight the total height of the wave (from crest to trough i.e. amplitude * 2). + * The final rendered height of the wave will be [waveHeight] + [waveThickness]. + * @param waveVelocity the horizontal movement (speed per second and direction) of the whole wave (aka phase shift). + * Setting speed to `0.dp` or less stops the movement. + * @param waveThickness the thickness of the active line (whether animated or not). + * @param trackThickness the thickness of the inactive line. + * @param incremental whether to gradually increase height from zero at start to [waveHeight] at thumb. + * @param animationSpecs animation configurations used for various properties of the wave. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun WavySlider( value: Float, @@ -171,14 +282,14 @@ fun WavySlider( incremental: Boolean = SliderDefaults.Incremental, animationSpecs: WaveAnimationSpecs = SliderDefaults.WaveAnimationSpecs ) { - WavySliderImpl( + WavySlider( + value = value, + onValueChange = onValueChange, modifier = modifier, enabled = enabled, - interactionSource = interactionSource, - onValueChange = onValueChange, onValueChangeFinished = onValueChangeFinished, - value = value, - valueRange = valueRange, + colors = colors, + interactionSource = interactionSource, thumb = { SliderDefaults.Thumb( interactionSource = interactionSource, @@ -186,11 +297,11 @@ fun WavySlider( enabled = enabled ) }, - track = { sliderPositions -> + track = { sliderState -> SliderDefaults.Track( colors = colors, enabled = enabled, - sliderPositions = sliderPositions, + sliderState = sliderState, ///////////////// ///////////////// ///////////////// @@ -202,35 +313,34 @@ fun WavySlider( incremental = incremental, animationSpecs = animationSpecs ) - } + }, + valueRange = valueRange ) } /** - * A wavy slider much like the [Material Design 3 slider](https://m3.material.io/components/sliders). - * - * Setting [waveHeight] or [waveLength] to `0.dp` results in a regular Material [Slider]. + * A wavy slider much like the [Material Design 3 Slider](https://m3.material.io/components/sliders). * - * This component can also be used as a progress bar. + * Setting [waveHeight] or [waveLength] to `0.dp` results in a regular [Slider]. * - * Note that range sliders do not make sense for the wavy slider. + * Note that range sliders do not make sense for the WavySlider. * So, there is no RangeWavySlider counterpart. * * @param value current value of the WavySlider. Will be coerced to [valueRange]. - * @param onValueChange onValueChange callback in which value should be updated - * @param modifier the [Modifier] to be applied to this WavySlider + * @param onValueChange callback in which value should be updated. + * @param modifier the [Modifier] to be applied to this WavySlider. * @param enabled controls the enabled state of this WavySlider. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility services. - * @param valueRange range of values that this slider can take. The passed [value] will be coerced + * @param valueRange range of values that this WavySlider can take. The passed [value] will be coerced * to this range. * @param onValueChangeFinished called when value change has ended. This should not be used to - * update the slider value (use [onValueChange] instead), but rather to know when the user has + * update the WavySlider value (use [onValueChange] instead), 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 resolve the colors used for this WavySlider in * different states. See [SliderDefaults.colors]. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this WavySlider. You can create and pass in your own `remember`ed instance to observe - * [Interaction]s and customize the appearance / behavior of this WavySlider in different states. + * [Interaction]s and customize the appearance / behavior of this slider in different states. * * * @@ -272,18 +382,124 @@ fun WavySlider( ///////////////// ///////////////// ///////////////// - thumb: @Composable (SliderPositions) -> Unit = { + thumb: @Composable (SliderState) -> Unit = { SliderDefaults.Thumb( interactionSource = interactionSource, colors = colors, enabled = enabled ) }, - track: @Composable (SliderPositions) -> Unit = { sliderPositions -> + track: @Composable (SliderState) -> Unit = { sliderState -> SliderDefaults.Track( colors = colors, enabled = enabled, - sliderPositions = sliderPositions, + sliderState = sliderState, + ///////////////// + ///////////////// + ///////////////// + waveLength = waveLength, + waveHeight = waveHeight, + waveVelocity = waveVelocity, + waveThickness = waveThickness, + trackThickness = trackThickness, + incremental = incremental, + animationSpecs= animationSpecs + ) + } +) { + val state = remember(valueRange, onValueChangeFinished) { + SliderState(value, 0, onValueChangeFinished, valueRange) + } + @Suppress("INVISIBLE_MEMBER") + state.onValueChange = onValueChange + state.value = value + WavySlider( + state = state, + modifier = modifier, + enabled = enabled, + interactionSource = interactionSource, + thumb = thumb, + track = track, + ///////////////// + ///////////////// + ///////////////// + waveLength = waveLength, + waveHeight = waveHeight, + waveVelocity = waveVelocity, + waveThickness = waveThickness, + trackThickness = trackThickness, + incremental = incremental, + animationSpecs = animationSpecs, + ) +} + +/** + * A wavy slider much like the [Material Design 3 Slider](https://m3.material.io/components/sliders). + * + * Setting [waveHeight] or [waveLength] to `0.dp` results in a regular [Slider]. + * + * Note that range sliders do not make sense for the WavySlider. + * So, there is no RangeWavySlider counterpart. + * + * @param state [SliderState] which contains the slider's current value. + * @param modifier the [Modifier] to be applied to this WavySlider. + * @param enabled controls the enabled state of this WavySlider. When `false`, this component will not + * respond to user input, and it will appear visually disabled and disabled to accessibility services. + * @param colors [SliderColors] that will be used to resolve the colors used for this WavySlider in + * different states. See [SliderDefaults.colors]. + * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s + * for this WavySlider. You can create and pass in your own `remember`ed instance to observe + * [Interaction]s and customize the appearance / behavior of this slider in different states. + * + * + * + * @param waveLength the distance over which the wave's shape repeats. + * @param waveHeight the total height of the wave (from crest to trough i.e. amplitude * 2). + * The final rendered height of the wave will be [waveHeight] + [waveThickness]. + * @param waveVelocity the horizontal movement (speed per second and direction) of the whole wave (aka phase shift). + * Setting speed to `0.dp` or less stops the movement. + * @param waveThickness the thickness of the active line (whether animated or not). + * @param trackThickness the thickness of the inactive line. + * @param incremental whether to gradually increase height from zero at start to [waveHeight] at thumb. + * @param animationSpecs animation configurations used for various properties of the wave. + * @param thumb the thumb to be displayed on the WavySlider, it is placed on top of the track. The lambda + * receives a [SliderPositions] which is used to obtain the current active track. + * @param track the track to be displayed on the WavySlider, it is placed underneath the thumb. The lambda + * receives a [SliderPositions] which is used to obtain the current active track. + */ +@Composable +@ExperimentalMaterial3Api +fun WavySlider( + state: SliderState, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: SliderColors = SliderDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + ///////////////// + ///////////////// + ///////////////// + waveLength: Dp = SliderDefaults.WaveLength, + waveHeight: Dp = SliderDefaults.WaveHeight, + waveVelocity: WaveVelocity = SliderDefaults.WaveVelocity, + waveThickness: Dp = SliderDefaults.WaveThickness, + trackThickness: Dp = SliderDefaults.TrackThickness, + incremental: Boolean = SliderDefaults.Incremental, + animationSpecs: WaveAnimationSpecs = SliderDefaults.WaveAnimationSpecs, + ///////////////// + ///////////////// + ///////////////// + thumb: @Composable (SliderState) -> Unit = { + SliderDefaults.Thumb( + interactionSource = interactionSource, + colors = colors, + enabled = enabled + ) + }, + track: @Composable (SliderState) -> Unit = { sliderState -> + SliderDefaults.Track( + colors = colors, + enabled = enabled, + sliderState = sliderState, ///////////////// ///////////////// ///////////////// @@ -298,12 +514,9 @@ fun WavySlider( } ) { WavySliderImpl( - value = value, - onValueChange = onValueChange, + state = state, modifier = modifier, enabled = enabled, - valueRange = valueRange, - onValueChangeFinished = onValueChangeFinished, interactionSource = interactionSource, thumb = thumb, track = track @@ -311,87 +524,36 @@ fun WavySlider( } @Composable +@OptIn(ExperimentalMaterial3Api::class) private fun WavySliderImpl( - modifier: Modifier, + state: SliderState, enabled: Boolean, + modifier: Modifier, interactionSource: MutableInteractionSource, - onValueChange: (Float) -> Unit, - onValueChangeFinished: (() -> Unit)?, - value: Float, - valueRange: ClosedFloatingPointRange, - thumb: @Composable (SliderPositions) -> Unit, - track: @Composable (SliderPositions) -> Unit + thumb: @Composable (SliderState) -> Unit, + track: @Composable (SliderState) -> Unit ) { - val onValueChangeState = rememberUpdatedState<(Float) -> Unit> { - if (it != value) { - onValueChange(it) - } - } - - val tickFractions = remember { floatArrayOf() } - - val thumbWidth = remember { mutableFloatStateOf(ThumbWidth.value) } - val totalWidth = remember { mutableIntStateOf(0) } - - fun scaleToUserValue(minPx: Float, maxPx: Float, offset: Float) = - scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive) - - fun scaleToOffset(minPx: Float, maxPx: Float, userValue: Float) = - scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx) - - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - val rawOffset = remember { mutableFloatStateOf(scaleToOffset(0f, 0f, value)) } - val pressOffset = remember { mutableFloatStateOf(0f) } - val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive) - - val positionFraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced) - val sliderPositions = remember(positionFraction, tickFractions) { - SliderPositions(0f..positionFraction, tickFractions) - } - - val draggableState = remember(valueRange) { - SliderDraggableState { - val maxPx = max(totalWidth.value - thumbWidth.value / 2, 0f) - val minPx = min(thumbWidth.value / 2, maxPx) - rawOffset.value = (rawOffset.value + it + pressOffset.value) - pressOffset.value = 0f - val offsetInTrack = snapValueToTick(rawOffset.value, tickFractions, minPx, maxPx) - onValueChangeState.value.invoke(scaleToUserValue(minPx, maxPx, offsetInTrack)) - } - } - - val gestureEndAction = rememberUpdatedState { - if (!draggableState.isDragging) { - // check isDragging in case the change is still in progress (touch -> drag case) - onValueChangeFinished?.invoke() - } - } - + @Suppress("INVISIBLE_MEMBER") + state.isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val press = Modifier.sliderTapModifier( - draggableState, + state, interactionSource, - totalWidth.value, - isRtl, - rawOffset, - gestureEndAction, - pressOffset, enabled ) - val drag = Modifier.draggable( orientation = Orientation.Horizontal, - reverseDirection = isRtl, + reverseDirection = @Suppress("INVISIBLE_MEMBER") state.isRtl, enabled = enabled, interactionSource = interactionSource, - onDragStopped = { _ -> gestureEndAction.value.invoke() }, - startDragImmediately = draggableState.isDragging, - state = draggableState + onDragStopped = { @Suppress("INVISIBLE_MEMBER") state.gestureEndAction.invoke() }, + startDragImmediately = @Suppress("INVISIBLE_MEMBER") state.isDragging, + state = state ) Layout( - { - Box(modifier = Modifier.layoutId(SliderComponents.THUMB)) { thumb(sliderPositions) } - Box(modifier = Modifier.layoutId(SliderComponents.TRACK)) { track(sliderPositions) } + content = { + Box(modifier = Modifier.layoutId(SliderComponents.THUMB)) { thumb(state) } + Box(modifier = Modifier.layoutId(SliderComponents.TRACK)) { track(state) } }, modifier = modifier .minimumInteractiveComponentSize() @@ -399,44 +561,32 @@ private fun WavySliderImpl( minWidth = SliderTokens.HandleWidth, minHeight = SliderTokens.HandleHeight ) - .sliderSemantics( - value, - enabled, - onValueChange, - onValueChangeFinished, - valueRange, - 0 - ) + .sliderSemantics(state, enabled) .focusable(enabled, interactionSource) .then(press) .then(drag) ) { measurables, constraints -> - val thumbPlaceable = measurables.first { - it.layoutId == SliderComponents.THUMB - }.measure(constraints) - - val trackPlaceable = measurables.first { - it.layoutId == SliderComponents.TRACK - }.measure( + val thumbPlaceable = measurables.fastFirst { it.layoutId == SliderComponents.THUMB }.measure(constraints) + val trackPlaceable = measurables.fastFirst { it.layoutId == SliderComponents.TRACK }.measure( constraints.offset(horizontal = - thumbPlaceable.width).copy(minHeight = 0) ) val sliderWidth = thumbPlaceable.width + trackPlaceable.width val sliderHeight = max(trackPlaceable.height, thumbPlaceable.height) - thumbWidth.value = thumbPlaceable.width.toFloat() - totalWidth.value = sliderWidth + @Suppress("INVISIBLE_MEMBER") + state.updateDimensions( + thumbPlaceable.width.toFloat(), + sliderWidth + ) val trackOffsetX = thumbPlaceable.width / 2 - val thumbOffsetX = ((trackPlaceable.width) * positionFraction).roundToInt() + val thumbOffsetX = ((trackPlaceable.width) * @Suppress("INVISIBLE_MEMBER") state.coercedValueAsFraction).roundToInt() val trackOffsetY = (sliderHeight - trackPlaceable.height) / 2 val thumbOffsetY = (sliderHeight - thumbPlaceable.height) / 2 - layout( - sliderWidth, - sliderHeight - ) { + layout(sliderWidth, sliderHeight) { trackPlaceable.placeRelative( trackOffsetX, trackOffsetY @@ -449,29 +599,28 @@ private fun WavySliderImpl( } } +@OptIn(ExperimentalMaterial3Api::class) // No need to name it wavySliderSemantics private fun Modifier.sliderSemantics( - value: Float, - enabled: Boolean, - onValueChange: (Float) -> Unit, - onValueChangeFinished: (() -> Unit)? = null, - valueRange: ClosedFloatingPointRange = 0f..1f, - steps: Int = 0 + state: SliderState, + enabled: Boolean ): 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) + var newValue = targetValue.coerceIn( + state.valueRange.start, + state.valueRange.endInclusive + ) val originalVal = newValue - val resolvedValue = if (steps > 0) { + val resolvedValue = if (state.steps > 0) { var distance: Float = newValue - for (i in 0..steps + 1) { - val stepValue = lerp( - valueRange.start, - valueRange.endInclusive, - i.toFloat() / (steps + 1) + for (i in 0..state.steps + 1) { + val stepValue = androidx.compose.ui.util.lerp( + state.valueRange.start, + state.valueRange.endInclusive, + i.toFloat() / (state.steps + 1) ) if (abs(stepValue - originalVal) <= distance) { distance = abs(stepValue - originalVal) @@ -485,95 +634,50 @@ private fun Modifier.sliderSemantics( // This is to keep it consistent with AbsSeekbar.java: return false if no // change from current. - if (resolvedValue == coerced) { + if (resolvedValue == state.value) { false } else { - onValueChange(resolvedValue) - onValueChangeFinished?.invoke() + if (resolvedValue != state.value) { + if (@Suppress("INVISIBLE_MEMBER") state.onValueChange != null) { + @Suppress("INVISIBLE_MEMBER") state.onValueChange?.let { + it(resolvedValue) + } + } else { + state.value = resolvedValue + } + } + state.onValueChangeFinished?.invoke() true } } ) - }.progressSemantics(value, valueRange, steps) + }.progressSemantics( + state.value, + state.valueRange.start..state.valueRange.endInclusive, + state.steps + ) } +@OptIn(ExperimentalMaterial3Api::class) +@Stable // No need to name it wavySliderTapModifier private fun Modifier.sliderTapModifier( - draggableState: DraggableState, + state: SliderState, interactionSource: MutableInteractionSource, - maxPx: Int, - isRtl: Boolean, - rawOffset: State, - gestureEndAction: State<() -> 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() - } - } - ) +) = if (enabled) { + pointerInput(state, interactionSource) { + detectTapGestures( + onPress = { @Suppress("INVISIBLE_MEMBER") state.onPress(it) }, + onTap = { + state.dispatchRawDelta(0f) + @Suppress("INVISIBLE_MEMBER") + state.gestureEndAction.invoke() } - } else { - this - } - }, - inspectorInfo = debugInspectorInfo { - name = "wavySliderTapModifier" - 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 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) + ) } +} else { + this } // No need to name it WavySliderComponents diff --git a/library/src/desktopTest/kotlin/ir/mahozad/multiplatform/wavyslider/VisualTest.kt b/library/src/desktopTest/kotlin/ir/mahozad/multiplatform/wavyslider/VisualTest.kt index 99276be..48d79ff 100644 --- a/library/src/desktopTest/kotlin/ir/mahozad/multiplatform/wavyslider/VisualTest.kt +++ b/library/src/desktopTest/kotlin/ir/mahozad/multiplatform/wavyslider/VisualTest.kt @@ -959,7 +959,7 @@ class VisualTest { given = "When a custom thumb is set", expected = "The custom thumb should be displayed and its height be taken into account in overall component height" ) { value, onChange -> - val thumb: @Composable (SliderPositions) -> Unit = @Composable { + val thumb: @Composable (SliderState) -> Unit = @Composable { Box(Modifier.width(6.dp).height(128.dp).background(Color.Red)) } Row(verticalAlignment = Alignment.CenterVertically) {