galaxygoldfish / waveslider

๐ŸŒŠ Animated wavy slider component for jetpack compose
Apache License 2.0
103 stars 9 forks source link

Crash #7

Open T8RIN opened 1 year ago

T8RIN commented 1 year ago

Screenshot_20230601-200121_Image Resizer.png

I have no idea, why this happens, maybe that's because i use newer compose version...

galaxygoldfish commented 1 year ago

What compose version are you using in that project?

T8RIN commented 1 year ago

Latest, 1.5.0-beta01 ๐Ÿ‘€

galaxygoldfish commented 1 year ago

It looks like in the waveslider library we use the compose BOM package so you could try that. It was automatically created by Android Studio as version 2023.05.01 (see libs.versions.toml)

T8RIN commented 1 year ago

The thing that worked, is that I copied the module to the project, and bumped compose to 1.5.0-beta01, and had to downgrade minSdk to 21, have no idea, why 26 used here, cause compose needs 21 only :(

galaxygoldfish commented 1 year ago

Oh so it doesn't crash when minSdk is 21?

T8RIN commented 1 year ago

No, I meant that there is no point in putting 26 if everything works with 21 ๐Ÿ‘€

T8RIN commented 1 year ago

And with the latest compose version it also doesn't crash

galaxygoldfish commented 1 year ago

Merged PR updating mikSdk, and I found that the latest compose BOM is what is currently in the project so I changed it to individual dependencies, however while the compose version and material versions are updated to the latest one, it breaks the current implementation of the steps because they changed some values to be internal only that we were using to calculate the position of each tick :(

T8RIN commented 1 year ago

Yes! But i found solution, how to fix it ๐Ÿ‘€

T8RIN commented 1 year ago

Just copy private function, which creates step fractions, and that's it :)

YounesBouhouche commented 1 year ago

I found the solution ! In the update of androidx.compose.material3:material3-*:1.2.0-alpha08, they made sliderPositions depreacted and sliderPositions.tickFractions internal, so I copied the WaveSlider function then I replaced this :

sliderPositions.tickFractions.groupBy {
                    it > sliderPositions.activeRange.endInclusive ||
                            it < sliderPositions.activeRange.start
                }

with this :

stepsToTickFractions(sliderState.steps).groupBy {
                    it > sliderState.valueRange.endInclusive ||
                            it < sliderState.valueRange.start
                }

Changes : Slider.kt ยท Gerrit Code Review

I found the stepsToTickFractions function : here

Here is the new file, just create new kt file "WaveSlider.kt" and paste this code :

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import com.galaxygoldfish.waveslider.LocalThumbColor
import com.galaxygoldfish.waveslider.PillThumb
import com.galaxygoldfish.waveslider.WaveAnimationOptions
import com.galaxygoldfish.waveslider.WaveOptions
import com.galaxygoldfish.waveslider.WaveSliderColors
import com.galaxygoldfish.waveslider.WaveSliderDefaults
import kotlin.math.sin

private fun stepsToTickFractions(steps: Int): FloatArray {
    return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WaveSlider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    onValueChangeFinished: (Float) -> Unit = {},
    colors: WaveSliderColors = WaveSliderDefaults.colors(),
    animationOptions: WaveAnimationOptions = WaveSliderDefaults.animationOptions(),
    waveOptions: WaveOptions = WaveSliderDefaults.waveOptions(),
    enabled: Boolean = true,
    thumb: @Composable () -> Unit = { PillThumb() },
    steps: Int = 0
) {
    val amplitude = waveOptions.amplitude
    val frequency = waveOptions.frequency

    var isDragging by remember { mutableStateOf(false) }
    val interactionSource = remember { MutableInteractionSource() }
    LaunchedEffect(interactionSource) {
        interactionSource.interactions.collect { interaction ->
            when (interaction) {
                is DragInteraction.Start -> {
                    isDragging = true
                }

                is DragInteraction.Stop -> {
                    isDragging = false
                }
            }
        }
    }
    val infiniteTransition = rememberInfiniteTransition(label = "Wave infinite transition")
    val phaseShiftFloat = infiniteTransition.animateFloat(
        label = "Wave phase shift",
        initialValue = 0F,
        targetValue = 90f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 1000
            },
            repeatMode = RepeatMode.Restart
        )
    ).value
    Slider(
        steps = steps,
        value = value,
        onValueChangeFinished = {
            onValueChangeFinished(value)
        },
        onValueChange = onValueChange,
        interactionSource = interactionSource,
        enabled = enabled,
        modifier = modifier,
        thumb = {
            CompositionLocalProvider(
                LocalThumbColor provides animateColorAsState(
                    targetValue = if (enabled) {
                        colors.thumbColor
                    } else {
                        colors.disabledThumbColor
                    },
                    label = "Thumb color"
                ).value
            ) {
                thumb()
            }
        },
        track = { sliderState ->
            val animatedAmplitude = animateFloatAsState(
                targetValue = if (animationOptions.flatlineOnDrag) {
                    if (animationOptions.reverseFlatline) {
                        if (isDragging) amplitude else 0F
                    } else {
                        if (isDragging) 0F else amplitude
                    }
                } else {
                    amplitude
                },
                label = "Wave amplitude"
            ).value
            Canvas(modifier = Modifier.fillMaxWidth()) {
                val centerY = size.height / 2f
                val startX = 0F
                val endX = size.width * value
                val path = Path()
                for (x in startX.toInt()..endX.toInt()) {
                    var modifiedX = x.toFloat()
                    if (animationOptions.animateWave && enabled) {
                        if (animationOptions.reverseDirection) {
                            modifiedX += phaseShiftFloat
                        } else {
                            modifiedX -= phaseShiftFloat
                        }
                    }
                    val y = (animatedAmplitude * sin(frequency * modifiedX))
                    path.moveTo(x.toFloat(), centerY - y)
                    path.lineTo(x.toFloat(), centerY - y)
                }
                drawPath(
                    path = path,
                    color = if (enabled) {
                        colors.activeTrackColor
                    } else {
                        colors.disabledActiveTrackColor
                    },
                    style = Stroke(width = 8f, cap = StrokeCap.Round)
                )
                drawLine(
                    color = if (enabled) {
                        colors.inactiveTrackColor
                    } else {
                        colors.disabledInactiveTrackColor
                    },
                    strokeWidth = 8F,
                    cap = StrokeCap.Round,
                    start = Offset(endX + 1, centerY),
                    end = Offset(size.width, centerY)
                )
                stepsToTickFractions(sliderState.steps).groupBy {
                    it > sliderState.valueRange.endInclusive ||
                            it < sliderState.valueRange.start
                }.forEach { (outsideFraction, list) ->
                    drawPoints(
                        points = list.map {
                            Offset(
                                x = lerp(
                                    start = Offset(startX, centerY),
                                    stop = Offset(size.width, centerY),
                                    fraction = it
                                ).x,
                                y = center.y
                            )
                        },
                        pointMode = PointMode.Points,
                        color = if (outsideFraction) {
                            if (enabled) {
                                colors.inactiveTickColor
                            } else {
                                colors.disabledInactiveTickColor
                            }
                        } else {
                            if (animatedAmplitude == 0F) {
                                if (enabled) {
                                    colors.activeTickColor
                                } else {
                                    colors.disabledActiveTickColor
                                }
                            } else {
                                Color.Transparent
                            }
                        },
                        strokeWidth = 10F,
                        cap = StrokeCap.Round
                    )
                }
            }
        }
    )
}

I hope the developer implement it as soon as possible

T8RIN commented 1 year ago

I found the solution ! In the update of androidx.compose.material3:material3-*:1.2.0-alpha08, they made sliderPositions depreacted and sliderPositions.tickFractions internal, so I copied the WaveSlider function then I replaced this :

sliderPositions.tickFractions.groupBy {
                    it > sliderPositions.activeRange.endInclusive ||
                            it < sliderPositions.activeRange.start
                }

with this :

stepsToTickFractions(sliderState.steps).groupBy {
                    it > sliderState.valueRange.endInclusive ||
                            it < sliderState.valueRange.start
                }

Changes : Slider.kt ยท Gerrit Code Review

I found the stepsToTickFractions function : here

Here is the new file, just create new kt file "WaveSlider.kt" and paste this code :

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import com.galaxygoldfish.waveslider.LocalThumbColor
import com.galaxygoldfish.waveslider.PillThumb
import com.galaxygoldfish.waveslider.WaveAnimationOptions
import com.galaxygoldfish.waveslider.WaveOptions
import com.galaxygoldfish.waveslider.WaveSliderColors
import com.galaxygoldfish.waveslider.WaveSliderDefaults
import kotlin.math.sin

private fun stepsToTickFractions(steps: Int): FloatArray {
    return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WaveSlider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    onValueChangeFinished: (Float) -> Unit = {},
    colors: WaveSliderColors = WaveSliderDefaults.colors(),
    animationOptions: WaveAnimationOptions = WaveSliderDefaults.animationOptions(),
    waveOptions: WaveOptions = WaveSliderDefaults.waveOptions(),
    enabled: Boolean = true,
    thumb: @Composable () -> Unit = { PillThumb() },
    steps: Int = 0
) {
    val amplitude = waveOptions.amplitude
    val frequency = waveOptions.frequency

    var isDragging by remember { mutableStateOf(false) }
    val interactionSource = remember { MutableInteractionSource() }
    LaunchedEffect(interactionSource) {
        interactionSource.interactions.collect { interaction ->
            when (interaction) {
                is DragInteraction.Start -> {
                    isDragging = true
                }

                is DragInteraction.Stop -> {
                    isDragging = false
                }
            }
        }
    }
    val infiniteTransition = rememberInfiniteTransition(label = "Wave infinite transition")
    val phaseShiftFloat = infiniteTransition.animateFloat(
        label = "Wave phase shift",
        initialValue = 0F,
        targetValue = 90f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 1000
            },
            repeatMode = RepeatMode.Restart
        )
    ).value
    Slider(
        steps = steps,
        value = value,
        onValueChangeFinished = {
            onValueChangeFinished(value)
        },
        onValueChange = onValueChange,
        interactionSource = interactionSource,
        enabled = enabled,
        modifier = modifier,
        thumb = {
            CompositionLocalProvider(
                LocalThumbColor provides animateColorAsState(
                    targetValue = if (enabled) {
                        colors.thumbColor
                    } else {
                        colors.disabledThumbColor
                    },
                    label = "Thumb color"
                ).value
            ) {
                thumb()
            }
        },
        track = { sliderState ->
            val animatedAmplitude = animateFloatAsState(
                targetValue = if (animationOptions.flatlineOnDrag) {
                    if (animationOptions.reverseFlatline) {
                        if (isDragging) amplitude else 0F
                    } else {
                        if (isDragging) 0F else amplitude
                    }
                } else {
                    amplitude
                },
                label = "Wave amplitude"
            ).value
            Canvas(modifier = Modifier.fillMaxWidth()) {
                val centerY = size.height / 2f
                val startX = 0F
                val endX = size.width * value
                val path = Path()
                for (x in startX.toInt()..endX.toInt()) {
                    var modifiedX = x.toFloat()
                    if (animationOptions.animateWave && enabled) {
                        if (animationOptions.reverseDirection) {
                            modifiedX += phaseShiftFloat
                        } else {
                            modifiedX -= phaseShiftFloat
                        }
                    }
                    val y = (animatedAmplitude * sin(frequency * modifiedX))
                    path.moveTo(x.toFloat(), centerY - y)
                    path.lineTo(x.toFloat(), centerY - y)
                }
                drawPath(
                    path = path,
                    color = if (enabled) {
                        colors.activeTrackColor
                    } else {
                        colors.disabledActiveTrackColor
                    },
                    style = Stroke(width = 8f, cap = StrokeCap.Round)
                )
                drawLine(
                    color = if (enabled) {
                        colors.inactiveTrackColor
                    } else {
                        colors.disabledInactiveTrackColor
                    },
                    strokeWidth = 8F,
                    cap = StrokeCap.Round,
                    start = Offset(endX + 1, centerY),
                    end = Offset(size.width, centerY)
                )
                stepsToTickFractions(sliderState.steps).groupBy {
                    it > sliderState.valueRange.endInclusive ||
                            it < sliderState.valueRange.start
                }.forEach { (outsideFraction, list) ->
                    drawPoints(
                        points = list.map {
                            Offset(
                                x = lerp(
                                    start = Offset(startX, centerY),
                                    stop = Offset(size.width, centerY),
                                    fraction = it
                                ).x,
                                y = center.y
                            )
                        },
                        pointMode = PointMode.Points,
                        color = if (outsideFraction) {
                            if (enabled) {
                                colors.inactiveTickColor
                            } else {
                                colors.disabledInactiveTickColor
                            }
                        } else {
                            if (animatedAmplitude == 0F) {
                                if (enabled) {
                                    colors.activeTickColor
                                } else {
                                    colors.disabledActiveTickColor
                                }
                            } else {
                                Color.Transparent
                            }
                        },
                        strokeWidth = 10F,
                        cap = StrokeCap.Round
                    )
                }
            }
        }
    )
}

I hope the developer implement it as soon as possible

Wow, thanks for that, i'll be waiting too, maybe you can open PR?

YounesBouhouche commented 1 year ago

I found the solution ! In the update of androidx.compose.material3:material3-*:1.2.0-alpha08, they made sliderPositions depreacted and sliderPositions.tickFractions internal, so I copied the WaveSlider function then I replaced this :

sliderPositions.tickFractions.groupBy {
                    it > sliderPositions.activeRange.endInclusive ||
                            it < sliderPositions.activeRange.start
                }

with this :

stepsToTickFractions(sliderState.steps).groupBy {
                    it > sliderState.valueRange.endInclusive ||
                            it < sliderState.valueRange.start
                }

Changes : Slider.kt ยท Gerrit Code Review I found the stepsToTickFractions function : here Here is the new file, just create new kt file "WaveSlider.kt" and paste this code :

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import com.galaxygoldfish.waveslider.LocalThumbColor
import com.galaxygoldfish.waveslider.PillThumb
import com.galaxygoldfish.waveslider.WaveAnimationOptions
import com.galaxygoldfish.waveslider.WaveOptions
import com.galaxygoldfish.waveslider.WaveSliderColors
import com.galaxygoldfish.waveslider.WaveSliderDefaults
import kotlin.math.sin

private fun stepsToTickFractions(steps: Int): FloatArray {
    return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WaveSlider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    onValueChangeFinished: (Float) -> Unit = {},
    colors: WaveSliderColors = WaveSliderDefaults.colors(),
    animationOptions: WaveAnimationOptions = WaveSliderDefaults.animationOptions(),
    waveOptions: WaveOptions = WaveSliderDefaults.waveOptions(),
    enabled: Boolean = true,
    thumb: @Composable () -> Unit = { PillThumb() },
    steps: Int = 0
) {
    val amplitude = waveOptions.amplitude
    val frequency = waveOptions.frequency

    var isDragging by remember { mutableStateOf(false) }
    val interactionSource = remember { MutableInteractionSource() }
    LaunchedEffect(interactionSource) {
        interactionSource.interactions.collect { interaction ->
            when (interaction) {
                is DragInteraction.Start -> {
                    isDragging = true
                }

                is DragInteraction.Stop -> {
                    isDragging = false
                }
            }
        }
    }
    val infiniteTransition = rememberInfiniteTransition(label = "Wave infinite transition")
    val phaseShiftFloat = infiniteTransition.animateFloat(
        label = "Wave phase shift",
        initialValue = 0F,
        targetValue = 90f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 1000
            },
            repeatMode = RepeatMode.Restart
        )
    ).value
    Slider(
        steps = steps,
        value = value,
        onValueChangeFinished = {
            onValueChangeFinished(value)
        },
        onValueChange = onValueChange,
        interactionSource = interactionSource,
        enabled = enabled,
        modifier = modifier,
        thumb = {
            CompositionLocalProvider(
                LocalThumbColor provides animateColorAsState(
                    targetValue = if (enabled) {
                        colors.thumbColor
                    } else {
                        colors.disabledThumbColor
                    },
                    label = "Thumb color"
                ).value
            ) {
                thumb()
            }
        },
        track = { sliderState ->
            val animatedAmplitude = animateFloatAsState(
                targetValue = if (animationOptions.flatlineOnDrag) {
                    if (animationOptions.reverseFlatline) {
                        if (isDragging) amplitude else 0F
                    } else {
                        if (isDragging) 0F else amplitude
                    }
                } else {
                    amplitude
                },
                label = "Wave amplitude"
            ).value
            Canvas(modifier = Modifier.fillMaxWidth()) {
                val centerY = size.height / 2f
                val startX = 0F
                val endX = size.width * value
                val path = Path()
                for (x in startX.toInt()..endX.toInt()) {
                    var modifiedX = x.toFloat()
                    if (animationOptions.animateWave && enabled) {
                        if (animationOptions.reverseDirection) {
                            modifiedX += phaseShiftFloat
                        } else {
                            modifiedX -= phaseShiftFloat
                        }
                    }
                    val y = (animatedAmplitude * sin(frequency * modifiedX))
                    path.moveTo(x.toFloat(), centerY - y)
                    path.lineTo(x.toFloat(), centerY - y)
                }
                drawPath(
                    path = path,
                    color = if (enabled) {
                        colors.activeTrackColor
                    } else {
                        colors.disabledActiveTrackColor
                    },
                    style = Stroke(width = 8f, cap = StrokeCap.Round)
                )
                drawLine(
                    color = if (enabled) {
                        colors.inactiveTrackColor
                    } else {
                        colors.disabledInactiveTrackColor
                    },
                    strokeWidth = 8F,
                    cap = StrokeCap.Round,
                    start = Offset(endX + 1, centerY),
                    end = Offset(size.width, centerY)
                )
                stepsToTickFractions(sliderState.steps).groupBy {
                    it > sliderState.valueRange.endInclusive ||
                            it < sliderState.valueRange.start
                }.forEach { (outsideFraction, list) ->
                    drawPoints(
                        points = list.map {
                            Offset(
                                x = lerp(
                                    start = Offset(startX, centerY),
                                    stop = Offset(size.width, centerY),
                                    fraction = it
                                ).x,
                                y = center.y
                            )
                        },
                        pointMode = PointMode.Points,
                        color = if (outsideFraction) {
                            if (enabled) {
                                colors.inactiveTickColor
                            } else {
                                colors.disabledInactiveTickColor
                            }
                        } else {
                            if (animatedAmplitude == 0F) {
                                if (enabled) {
                                    colors.activeTickColor
                                } else {
                                    colors.disabledActiveTickColor
                                }
                            } else {
                                Color.Transparent
                            }
                        },
                        strokeWidth = 10F,
                        cap = StrokeCap.Round
                    )
                }
            }
        }
    )
}

I hope the developer implement it as soon as possible

Wow, thanks for that, i'll be waiting too, maybe you can open PR?

Do you mean pull request ? I know it's silly question but I'm new in Github

T8RIN commented 1 year ago

@YounesBouhouche did you noticed Layout Node not attached to owner exception after updating to 1.2.0-06 or 07-08 ?

T8RIN commented 1 year ago

I found the solution ! In the update of androidx.compose.material3:material3-*:1.2.0-alpha08, they made sliderPositions depreacted and sliderPositions.tickFractions internal, so I copied the WaveSlider function then I replaced this :

sliderPositions.tickFractions.groupBy {
                    it > sliderPositions.activeRange.endInclusive ||
                            it < sliderPositions.activeRange.start
                }

with this :

stepsToTickFractions(sliderState.steps).groupBy {
                    it > sliderState.valueRange.endInclusive ||
                            it < sliderState.valueRange.start
                }

Changes : Slider.kt ยท Gerrit Code Review I found the stepsToTickFractions function : here Here is the new file, just create new kt file "WaveSlider.kt" and paste this code :

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import com.galaxygoldfish.waveslider.LocalThumbColor
import com.galaxygoldfish.waveslider.PillThumb
import com.galaxygoldfish.waveslider.WaveAnimationOptions
import com.galaxygoldfish.waveslider.WaveOptions
import com.galaxygoldfish.waveslider.WaveSliderColors
import com.galaxygoldfish.waveslider.WaveSliderDefaults
import kotlin.math.sin

private fun stepsToTickFractions(steps: Int): FloatArray {
    return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WaveSlider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    onValueChangeFinished: (Float) -> Unit = {},
    colors: WaveSliderColors = WaveSliderDefaults.colors(),
    animationOptions: WaveAnimationOptions = WaveSliderDefaults.animationOptions(),
    waveOptions: WaveOptions = WaveSliderDefaults.waveOptions(),
    enabled: Boolean = true,
    thumb: @Composable () -> Unit = { PillThumb() },
    steps: Int = 0
) {
    val amplitude = waveOptions.amplitude
    val frequency = waveOptions.frequency

    var isDragging by remember { mutableStateOf(false) }
    val interactionSource = remember { MutableInteractionSource() }
    LaunchedEffect(interactionSource) {
        interactionSource.interactions.collect { interaction ->
            when (interaction) {
                is DragInteraction.Start -> {
                    isDragging = true
                }

                is DragInteraction.Stop -> {
                    isDragging = false
                }
            }
        }
    }
    val infiniteTransition = rememberInfiniteTransition(label = "Wave infinite transition")
    val phaseShiftFloat = infiniteTransition.animateFloat(
        label = "Wave phase shift",
        initialValue = 0F,
        targetValue = 90f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 1000
            },
            repeatMode = RepeatMode.Restart
        )
    ).value
    Slider(
        steps = steps,
        value = value,
        onValueChangeFinished = {
            onValueChangeFinished(value)
        },
        onValueChange = onValueChange,
        interactionSource = interactionSource,
        enabled = enabled,
        modifier = modifier,
        thumb = {
            CompositionLocalProvider(
                LocalThumbColor provides animateColorAsState(
                    targetValue = if (enabled) {
                        colors.thumbColor
                    } else {
                        colors.disabledThumbColor
                    },
                    label = "Thumb color"
                ).value
            ) {
                thumb()
            }
        },
        track = { sliderState ->
            val animatedAmplitude = animateFloatAsState(
                targetValue = if (animationOptions.flatlineOnDrag) {
                    if (animationOptions.reverseFlatline) {
                        if (isDragging) amplitude else 0F
                    } else {
                        if (isDragging) 0F else amplitude
                    }
                } else {
                    amplitude
                },
                label = "Wave amplitude"
            ).value
            Canvas(modifier = Modifier.fillMaxWidth()) {
                val centerY = size.height / 2f
                val startX = 0F
                val endX = size.width * value
                val path = Path()
                for (x in startX.toInt()..endX.toInt()) {
                    var modifiedX = x.toFloat()
                    if (animationOptions.animateWave && enabled) {
                        if (animationOptions.reverseDirection) {
                            modifiedX += phaseShiftFloat
                        } else {
                            modifiedX -= phaseShiftFloat
                        }
                    }
                    val y = (animatedAmplitude * sin(frequency * modifiedX))
                    path.moveTo(x.toFloat(), centerY - y)
                    path.lineTo(x.toFloat(), centerY - y)
                }
                drawPath(
                    path = path,
                    color = if (enabled) {
                        colors.activeTrackColor
                    } else {
                        colors.disabledActiveTrackColor
                    },
                    style = Stroke(width = 8f, cap = StrokeCap.Round)
                )
                drawLine(
                    color = if (enabled) {
                        colors.inactiveTrackColor
                    } else {
                        colors.disabledInactiveTrackColor
                    },
                    strokeWidth = 8F,
                    cap = StrokeCap.Round,
                    start = Offset(endX + 1, centerY),
                    end = Offset(size.width, centerY)
                )
                stepsToTickFractions(sliderState.steps).groupBy {
                    it > sliderState.valueRange.endInclusive ||
                            it < sliderState.valueRange.start
                }.forEach { (outsideFraction, list) ->
                    drawPoints(
                        points = list.map {
                            Offset(
                                x = lerp(
                                    start = Offset(startX, centerY),
                                    stop = Offset(size.width, centerY),
                                    fraction = it
                                ).x,
                                y = center.y
                            )
                        },
                        pointMode = PointMode.Points,
                        color = if (outsideFraction) {
                            if (enabled) {
                                colors.inactiveTickColor
                            } else {
                                colors.disabledInactiveTickColor
                            }
                        } else {
                            if (animatedAmplitude == 0F) {
                                if (enabled) {
                                    colors.activeTickColor
                                } else {
                                    colors.disabledActiveTickColor
                                }
                            } else {
                                Color.Transparent
                            }
                        },
                        strokeWidth = 10F,
                        cap = StrokeCap.Round
                    )
                }
            }
        }
    )
}

I hope the developer implement it as soon as possible

Wow, thanks for that, i'll be waiting too, maybe you can open PR?

Me too, but what's PR? how to open it?

You need to fork the project and then apply your changes in your fork, and then open pull request on GitHub

YounesBouhouche commented 1 year ago

@YounesBouhouche did you noticed Layout Node not attached to owner exception after updating to 1.2.0-06 or 07-08 ?

No I didn't

T8RIN commented 1 year ago

@YounesBouhouche did you noticed Layout Node not attached to owner exception after updating to 1.2.0-06 or 07-08 ?

No I didn't

:(

galaxygoldfish commented 1 week ago

Hi all, got back to maintaining this project and I have updated WaveSlider.kt with the fix in this commit [cbab52b]

@T8RIN @YounesBouhouche Does this address the issue?