'Jetpack Compose Slider in LazyColumn changing while scroll

I have a list that includes Slider the problem is when I scroll the slider detecting the tap event and change it, see attached gif

 see attached gif

The excepted behavior is when the user scroll the slider not will change just when the user tap on the slider.

How can I avoid changing the slider while scrolling?

Relevant code:

    @Composable
fun TodoScreen(
    items: List<TodoItem>,
    currentlyEditing: TodoItem?,
    onAddItem: (TodoItem) -> Unit,
    onRemoveItem: (TodoItem) -> Unit,
    onStartEdit: (TodoItem) -> Unit,
    onEditItemChange: (TodoItem) -> Unit,
    onEditDone: () -> Unit,
    onPositionChanged: (TodoItem, Float) -> Unit
) {
    Column {
        val enableTopSection = currentlyEditing == null
        TodoItemInputBackground(elevate = enableTopSection) {
            if (enableTopSection) {
                TodoItemEntryInput(onAddItem)
            } else {
                Text(
                    text = "Editing item",
                    style = MaterialTheme.typography.h6,
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                        .align(Alignment.CenterVertically)
                        .padding(16.dp)
                        .fillMaxWidth()
                )
            }
        }

        LazyColumn(
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(top = 8.dp)
        ) {
            items(items = items) { todo ->
                if (currentlyEditing?.id == todo.id) {
                    Log.d("ListTodo", "task: ${todo.task}")
                    TodoItemInlineEditor(
                        item = currentlyEditing,
                        onEditItemChange = onEditItemChange,
                        onEditDone = onEditDone,
                        onRemoveItem = { onRemoveItem(todo) }
                    )
                } else {
                    TodoRow(
                        todo = todo,
                        onItemClicked = { onStartEdit(it) },
                        modifier = Modifier.fillParentMaxWidth(),
                        position = todo.position,
                        onPositionChanged = onPositionChanged
                    )
                }
            }
        }

        // For quick testing, a random item generator button
        Button(
            onClick = { onAddItem(generateRandomTodoItem()) },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth(),
        ) {
            Text("Add random item")
        }
    }
}

And

@Composable
fun TodoRow(
    todo: TodoItem,
    onItemClicked: (TodoItem) -> Unit,
    modifier: Modifier = Modifier,
    iconAlpha: Float = remember(todo.id) { randomTint() },
    position: Float,
    onPositionChanged: (TodoItem, Float) -> Unit
) {
    Column() {
        Row(
            modifier
                .clickable { onItemClicked(todo) }
                .padding(horizontal = 16.dp, vertical = 8.dp),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(todo.task)
            Icon(
                imageVector = todo.icon.imageVector,
                tint = LocalContentColor.current.copy(alpha = iconAlpha),
                contentDescription = stringResource(id = todo.icon.contentDescription)
            )
        }
        Slider(value = position, onValueChange = { onPositionChanged(todo.copy(position = it), it) })
    }
}


Solution 1:[1]

A very simple and out of the box solution could be to use rememberLazyListState() to check if isScrollInProgress is false and only then allow the code in the TodoRow modifier .clickable and the Slider onValueChange lambda to run.

Something like this:

@Composable
fun Content() {
    //This instance of rememberLazyListState
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState, // Go here!
        contentPadding = PaddingValues(top = 8.dp)
    ) {
        items(listOfItems) { item ->
            TodoRow(listState = listState, todo = item) {
                Log.d("TAG", "onItemClicked")
            }
        }
    }
}

And then in the TodoRow:

@Composable
fun TodoRow(
    listState: LazyListState,
    todo: String,
    onItemClicked: (String) -> Unit
) {
    Column {
        
        Row(Modifier
                .clickable {
                    if (!listState.isScrollInProgress) { //This line
                        onItemClicked(todo)
                    }
                }
        ) {
            //Your Code...
        }
        
        Slider(
            value = someRememberableValue,
            onValueChange = {
                if (!listState.isScrollInProgress) {  //This line
                    //Your Code...
                }
            }
        )
    }
}

Also, if you want you can extract only the isScrollInProgress boolean out of the state and pass only it to the TodoRow.

This should work but it could be that the first touch point will be registered. (when the list is actually not scrolling).

If this not enough I would consider listen and override the screen touch and click events.

https://developer.android.com/jetpack/compose/gestures

Solution 2:[2]

Faced exactly the same problem, here is my solution.

Define a wrapper over the Slider composable like follows. Compared to jetpack implementation, It basically defers reacting to changes unless it is dragged, keeps track of the ignored change, and on interaction end, decides whether to fire an additional onchange to cater for the ignored value. The behavior in on end is controlled from outside via a state. IF a drag happens on it, it behaves normally like a Slider would.

@Composable
fun MySlider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
    /*@IntRange(from = 0)*/
    steps: Int = 0,
    onValueChangeFinished: (() -> Unit)? = null,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: SliderColors = SliderDefaults.colors(),
    reactToTap: State<Boolean> = remember { mutableStateOf(true) }
) {
    val pressed = interactionSource.collectIsPressedAsState()
    val dragged = interactionSource.collectIsDraggedAsState()
    val valueChangedSinceLastInteraction = remember { mutableStateOf(false)}
    val ignoredChange = remember { mutableStateOf(Float.NaN) }

    Slider(
        value,
        onValueChange = {
            if (pressed.value || dragged.value) {
                onValueChange(
                    if (ignoredChange.value.isNaN()) it
                    else ignoredChange.value
                )
                ignoredChange.value = Float.NaN
                valueChangedSinceLastInteraction.value = true
            } else {
                ignoredChange.value = it
            }
        },
        modifier,
        enabled,
        valueRange,
        steps,
        onValueChangeFinished = {
            if (valueChangedSinceLastInteraction.value) {
                valueChangedSinceLastInteraction.value = false
                onValueChangeFinished?.invoke()
            } else if (reactToTap.value) {
                if (!ignoredChange.value.isNaN()) {
                    onValueChange(ignoredChange.value)
                }
                ignoredChange.value = Float.NaN
                onValueChangeFinished?.invoke()
            }
        },
        interactionSource,
        colors
    )
}

and then in the lazycolumn

val listState = rememberLazyListState()
val isListNotScrolling = derivedStateOf { !listState.isScrollInProgress }
LazyColumn(state = listState) {
  items(list) { item -> MySlider(..., reactToTap = isListNotScrolling)
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 42Geek
Solution 2 mainak biswas