'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
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.
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 |