I’m having an issue trying to create an abstractio...
# compose
c
I’m having an issue trying to create an abstraction for my gestures and passing parameters to the modifier I created, more in the 🧵
Copy code
import android.util.Log
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Preview
@Composable
fun TestSlider() {
    Sample()
}

@Composable
fun Sample() {
    val height = 50.dp
    val diameter = 50f
    var x by remember { mutableFloatStateOf(0f) }
    var selectedKnob by remember { mutableStateOf<Int?>(null) }
    var primaryKnob by remember { mutableStateOf(100f) }
    Log.i("Slider", "primaryKnob: $primaryKnob")

    Column(
        modifier = Modifier.fillMaxSize().height(height),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        BoxWithConstraints(
            modifier = Modifier
        ) {
            val boxWidth = this.constraints.maxWidth.toFloat()
            Canvas(modifier = Modifier
                .fillMaxWidth()
                .height(height)
                .pointerInput(Unit) {
                    this.detectTapGestures(onPress = {
                        Log.i("Slider", "x: ${it.x}, knobArea: ${primaryKnob - diameter..primaryKnob}")
                        when {
                            it.x in primaryKnob - diameter..primaryKnob + diameter -> {
                                x = primaryKnob
                                selectedKnob = 0
                            }

                            else -> selectedKnob = null
                        }
                    })
                }
                // .detectTapGestures(
                //     primaryKnob = primaryKnob,
                //     knobTouchArea = 100f,
                //     onSelectedKnobChange = { _x, _selectedKnob ->
                //         Log.i("Slider", "x: $_x, selectedKnob: $_selectedKnob")
                //         if (_x != null) {
                //             x = _x
                //         }
                //         selectedKnob = _selectedKnob
                //     }
                // )
                .pointerInput(Unit) {
                    this.detectDragGestures(
                        onDragStart = {
                            x = it.x
                        },
                        onDragEnd = {
                            when (selectedKnob) {
                                0 -> {
                                    primaryKnob = x
                                }
                            }
                        },
                        onDragCancel = {},
                        onDrag = { pointerInputChange: PointerInputChange, offset: Offset ->
                            if (x < 0)
                                x = 0f
                            if (x > boxWidth)
                                x = boxWidth
                            if (selectedKnob != null) {
                                x += offset.x
                            }
                            when (selectedKnob) {
                                0 -> primaryKnob = x
                            }
                        })
                }) {

                this.drawCircle(
                    color = Color.Cyan, radius = diameter,
                    center = Offset(primaryKnob, this.center.y)
                )
            }
        }
    }
}

private fun Modifier.detectTapGestures(
    primaryKnob: Float,
    knobTouchArea: Float,
    onSelectedKnobChange: (Float?, Int?) -> Unit
) = pointerInput(Unit) {
    this.detectTapGestures(onPress = {
        Log.i("Slider", "x: ${it.x}, knobArea: ${primaryKnob - knobTouchArea..primaryKnob}")
        when {
            it.x in primaryKnob - knobTouchArea..primaryKnob + knobTouchArea -> onSelectedKnobChange(primaryKnob, 0)
            else -> onSelectedKnobChange(null, null)
        }
    })
}
This code should run out of the box and you should be able to switch between the newly created modifier and the regular pointerInput
I do not understand why
primaryKnob
, from the perspective of
detectTapGestures
(which receives and uses the
primaryKnob
value), is not updated.
I have a clue why when making my calculations directly in the exposed lambda from pointerInput
Copy code
.pointerInput(Unit) {
                    this.detectTapGestures(onPress = {
                        Log.i("Slider", "x: ${it.x}, knobArea: ${primaryKnob - diameter..primaryKnob}")
                        when {
                            it.x in primaryKnob - diameter..primaryKnob + diameter -> {
                                x = primaryKnob
                                selectedKnob = 0
                            }

                            else -> selectedKnob = null
                        }
                    })
                }
Is there a way to work with external and mutable parameters and modifiers? what is happening here?
Here is the answer:
Copy code
When you extract functions into Modifier extensions, the parameters used inside these functions are captured when the function is called, and they do not get updated if the state changes outside these functions. This is a common issue with Kotlin and Compose due to how lambdas and captured variables work.
To address this, we need to ensure that the state used within the modifier functions is correctly updated by using higher-order functions or by passing lambdas that will fetch the current state.
Copy code
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Preview
@Composable
fun TestSlider() {
    Sample()
}

@Composable
fun Sample() {
    val height = 50.dp
    val diameter = 50f
    var x by remember { mutableFloatStateOf(0f) }
    var selectedKnob by remember { mutableStateOf<Int?>(null) }
    var primaryKnob by remember { mutableStateOf(100f) }
    Log.i("Slider", "primaryKnob: $primaryKnob")

    Column(
        modifier = Modifier.fillMaxSize().height(height),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        BoxWithConstraints(
            modifier = Modifier
        ) {
            val boxWidth = this.constraints.maxWidth.toFloat()
            Canvas(modifier = Modifier
                .fillMaxWidth()
                .height(height)
                // .pointerInput(Unit) {
                //     this.detectTapGestures(onPress = {
                //         Log.i("Slider", "x: ${it.x}, knobArea: ${primaryKnob - diameter..primaryKnob}")
                //         when {
                //             it.x in primaryKnob - diameter..primaryKnob + diameter -> {
                //                 x = primaryKnob
                //                 selectedKnob = 0
                //             }
                //
                //             else -> selectedKnob = null
                //         }
                //     })
                // }
                .detectTapGestures(
                    getPrimaryKnob = { primaryKnob },
                    onSelectedKnobChange = { _x, _selectedKnob ->
                        Log.i("Slider", "x: $_x, selectedKnob: $_selectedKnob")
                        if (_x != null) {
                            x = _x
                        }
                        selectedKnob = _selectedKnob
                    }
                )
                .pointerInput(Unit) {
                    this.detectDragGestures(
                        onDragStart = {
                            x = it.x
                        },
                        onDragEnd = {
                            when (selectedKnob) {
                                0 -> {
                                    primaryKnob = x
                                }
                            }
                        },
                        onDragCancel = {},
                        onDrag = { pointerInputChange: PointerInputChange, offset: Offset ->
                            if (x < 0)
                                x = 0f
                            if (x > boxWidth)
                                x = boxWidth
                            if (selectedKnob != null) {
                                x += offset.x
                            }
                            when (selectedKnob) {
                                0 -> primaryKnob = x
                            }
                        })
                }) {

                this.drawCircle(
                    color = Color.Cyan, radius = diameter,
                    center = Offset(primaryKnob, this.center.y)
                )
            }
        }
    }
}

@SuppressLint("ModifierFactoryUnreferencedReceiver")
private fun Modifier.detectTapGestures(
    getPrimaryKnob: () -> Float,
    onSelectedKnobChange: (Float?, Int?) -> Unit
) = pointerInput(Unit) {
    detectTapGestures(onPress = {
        val primaryKnob = getPrimaryKnob()
        when {
            it.x in primaryKnob - 100..primaryKnob + 100 -> onSelectedKnobChange(primaryKnob, 0)
            else -> onSelectedKnobChange(null, null)
        }
    })
}
working code snippet