Reorderable icon indicating copy to clipboard operation
Reorderable copied to clipboard

Unexpected stop of first item moving in intermediate state.

Open zhelenskiy opened this issue 1 year ago • 59 comments

I have issues with LazyRow when I add more items. The issue happens only when I add items and only when I move the first item.

https://github.com/Calvin-LL/Reorderable/assets/55230817/dabde38e-2ffd-4e35-8ab3-ebf798a0bae2

Here is working example when I move other items.

https://github.com/Calvin-LL/Reorderable/assets/55230817/740e6d75-5806-4ece-8327-cc2b205a5e6e

I'll try to figure out what happens or minimize the example later today, but if you already have any idea what happens, let me now.

The version is 2.0.1, the platform is Desktop. I test it on Mac OS 14.4.1.

zhelenskiy avatar May 05 '24 01:05 zhelenskiy

Oh this is very strange. I have no clue how this is happening. I will wait for your minimized example. Is this on the latest version of reorderable?

Calvin-LL avatar May 05 '24 01:05 Calvin-LL

Yes

zhelenskiy avatar May 05 '24 01:05 zhelenskiy

Did this work with an older version of the library? Also does this have a invisible item before the first item like in #4? I'm assuming so because I don't see a jump after moving the first item.

Calvin-LL avatar May 05 '24 02:05 Calvin-LL

No, I don't have any invisible items. I didn't test for older versions. But I reproduced it successfully with a smaller example.

https://github.com/Calvin-LL/Reorderable/assets/55230817/cd95d8a7-f3ef-4410-926f-39d587f9503d

import androidx.compose.animation.*
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import kotlinx.coroutines.launch
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState

@Composable
@Preview
fun App() {
    MaterialTheme {
        TabRow()
    }
}

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        App()
    }
}

typealias Id = Long

data class Tab(val id: Id)

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun TabRow() {
    var tabModels: List<Tab> by remember { mutableStateOf(listOf(Tab(-1))) }
    val coroutineScope = rememberCoroutineScope()
    val tabRowScrollState = rememberLazyListState()
    val reorderableLazyColumnState =
        rememberReorderableLazyListState(tabRowScrollState) { from, to ->
            coroutineScope.launch {
                tabModels = tabModels.toMutableList().apply {
                    add(to.index, removeAt(from.index))
                }
            }
        }
    var chosenId by remember { mutableStateOf(tabModels.first().id) }
    BoxWithConstraints {
        AnimatedVisibility(
            visible = maxHeight > 300.dp,
            enter = expandVertically(),
            exit = shrinkVertically(),
        ) {
            LazyRow(
                state = tabRowScrollState,
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.fillMaxWidth()
            ) {
                itemsIndexed(
                    tabModels,
                    key = { _, model -> model.id }) { index, tabModel ->
                    ReorderableItem(
                        reorderableLazyColumnState,
                        key = tabModel.id
                    ) { isDragging ->
                        val isSelected = tabModel.id == chosenId
                        val chooseThis = { coroutineScope.launch { chosenId = tabModel.id } }
                        Row(
                            Modifier
                                .height(IntrinsicSize.Min).draggableHandle()
                        ) {
                            Tab(
                                selected = isSelected,
                                enabled = !isSelected,
                                modifier = Modifier
                                    .padding(horizontal = 2.dp)
                                    .clip(RoundedCornerShape(topEnd = 4.dp, topStart = 4.dp)),
                                onClick = { chooseThis() },
                            ) {
                                Box(Modifier.width(IntrinsicSize.Min)) {
                                    val textColor = if (isSelected) Color.Black else Color.Blue
                                    val maxTabWidth = 300.dp
                                    Row(
                                        verticalAlignment = Alignment.CenterVertically,
                                        modifier = Modifier.widthIn(max = maxTabWidth)
                                            .padding(vertical = 4.dp)
                                            .padding(start = 12.dp),
                                    ) {
                                        Spacer(Modifier.width(6.dp))
                                        val textStyle =
                                            TextStyle(color = textColor, fontSize = 18.sp)
                                        Box(Modifier.weight(1f)) {
                                            Text(
                                                text = tabModel.id.toString(),
                                                style = textStyle,
                                                maxLines = 1,
                                                softWrap = false,
                                                modifier = Modifier
                                                    .horizontalScroll(rememberScrollState())
                                            )
                                        }
                                        val expectedCloseSize by animateDpAsState(if (tabModels.size > 1) 48.dp else 8.dp)
                                        Box(Modifier.width(expectedCloseSize)) {
                                            androidx.compose.animation.AnimatedVisibility(
                                                tabModels.size > 1,
                                                enter = scaleIn() + fadeIn(),
                                                exit = scaleOut() + fadeOut(),
                                            ) {
                                                IconButton(
                                                    onClick = {
                                                    },
                                                    enabled = tabModels.size > 1,
                                                ) {
                                                    Icon(
                                                        imageVector = Icons.Default.Close,
                                                        tint = textColor,
                                                        contentDescription = "Close tab"
                                                    )
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                item {
                    IconButton(
                        onClick = {
                            coroutineScope.launch {
                                tabModels += Tab(tabModels.size.toLong())
                            }
                        }
                    ) {
                        Icon(
                            Icons.Default.Add,
                            contentDescription = "Add tab",
                        )
                    }
                }
            }
        }
    }
}

zhelenskiy avatar May 05 '24 02:05 zhelenskiy

For 2.0.0 The library seems not to be working properly at all.

https://github.com/Calvin-LL/Reorderable/assets/55230817/e729bc23-1810-4428-b031-1c3efcaac237

zhelenskiy avatar May 05 '24 02:05 zhelenskiy

With 2.0.1, the previous case works well.

https://github.com/Calvin-LL/Reorderable/assets/55230817/c2ae048b-5a6e-4f5a-b80a-63857bde593c

zhelenskiy avatar May 05 '24 02:05 zhelenskiy

Thank you! I will take a look.

Calvin-LL avatar May 05 '24 03:05 Calvin-LL

Just for my internal planning, when are you going to take a look at the issue?

zhelenskiy avatar May 05 '24 16:05 zhelenskiy

Just got up. I think I see what was causing it last night, something in my library is causing your composable to constantly recompose. I will fix it in the next 48 hours but likely sooner. Sorry about the long timeline, I'm playing DEF CON CTF Qualifier this weekend.

Calvin-LL avatar May 05 '24 16:05 Calvin-LL

Well, my experience shows that average ETA of response in GitHub issues is several days in the best case and several weeks or months in the others. So, having responses within several minutes (7 minutes for the first reply and several for the rest) is insanely fast. Your eta for the fix is also tens of times less than the average. So there're no complaints!

Good luck at the Qualifier! Let all the flags be with you 🙏

zhelenskiy avatar May 05 '24 20:05 zhelenskiy

Just released v2.0.3! Let me know if that fixes it. Thank you again for reporting this issue 🙏.

Calvin-LL avatar May 07 '24 01:05 Calvin-LL

It partially fixes. It is not stuck at the point anymore, but it stops after exchanging with the next element.

https://github.com/Calvin-LL/Reorderable/assets/55230817/7ff367df-83c4-4f24-a33d-c9e52033e51d

zhelenskiy avatar May 07 '24 14:05 zhelenskiy

It partially fixes. It is not stuck at the point anymore, but it stops after exchanging with the next element.

https://github.com/Calvin-LL/Reorderable/assets/55230817/7ff367df-83c4-4f24-a33d-c9e52033e51d

Oh strange I'll take a look.

Calvin-LL avatar May 07 '24 15:05 Calvin-LL

Hi, I also have the same issue (but with a vertical lazylist), and another one, I'm not sure if it's linked to this issue so I just comment here, maybe it can help: When I try to move an item to the last position, it's impossible, the item doesn't move, there is no unexpected stop but maybe the problem is the same. I can drag the last item to above, but not the before last to bellow Thank you for your time, this library is the best drag and drop lib I found, despite these issues :+1:

liltof avatar May 07 '24 17:05 liltof

Hi, I also have the same issue (but with a vertical lazylist), and another one, I'm not sure if it's linked to this issue so I just comment here, maybe it can help: When I try to move an item to the last position, it's impossible, the item doesn't move, there is no unexpected stop but maybe the problem is the same. I can drag the last item to above, but not the before last to bellow Thank you for your time, this library is the best drag and drop lib I found, despite these issues 👍

That sounds like a different issue. Can you post a screen recording?

Calvin-LL avatar May 07 '24 17:05 Calvin-LL

Here it is (the true/false) at the begining is just debug, I was checking "isDraging" to see if I did something wrong

https://github.com/Calvin-LL/Reorderable/assets/3226611/53e38acb-9837-45a1-bbdf-3d902abea86d

liltof avatar May 07 '24 17:05 liltof

Here it is (the true/false) at the begining is just debug, I was checking "isDraging" to see if I did something wrong

Screen_recording_20240507_193401.mp4

That looks like a different issue. Does the ReorderableItem on the last item have (enable = true)? It would help me debug this if you can provide a simplified version of your code that can reproduce this bug in a new issue 🙏.

Calvin-LL avatar May 07 '24 17:05 Calvin-LL

No the last one is like the other ones, nothin special: ReorderableItem(reorderableLazyListState, key = item.metaId.hashCode()) {

I will try to make a simple example and come back here

liltof avatar May 07 '24 17:05 liltof

It partially fixes. It is not stuck at the point anymore, but it stops after exchanging with the next element.

Screen.Recording.2024-05-07.at.16.30.35.mov

Should work in v2.0.4

Calvin-LL avatar May 07 '24 18:05 Calvin-LL

Here it is (the true/false) at the begining is just debug, I was checking "isDraging" to see if I did something wrong

Screen_recording_20240507_193401.mp4

I'm also having this problem where an item couldn't push the bottom item up and when the first item swaps position with the second item, the first item loses the drag. Both in a vertical lazy list. And I've just tried with 2.0.4.

(Upgraded from 1.5.2, which the first item losing drag problem already existed)

LOOHP avatar May 07 '24 19:05 LOOHP

Ok I made an example, AND making it I found a workaround So the bottom issue seems to only happen when the last item is at the bottom of the screen (at least on android) So what I did is add a contentPadding to the list (which is exactly what I need in the specs I have so it's ok for me) the contentPadding is commented in the code below But I still have the issue with the top item, even in 2.0.4

package com.example.testsapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.testsapplication.ui.theme.TestsApplicationTheme
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TestsApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    val listState = rememberLazyListState()
                    var itms by remember {
                        mutableStateOf(
                            listOf(
                            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o")
                        )
                    }
                    val reorderableLazyListState = rememberReorderableLazyListState(lazyListState = listState) { from, to ->
                        val fromObject = itms[from.index]
                        val toObject = itms[to.index]
                        val newlist = itms.toMutableStateList()
                        newlist[from.index] = toObject
                        newlist[to.index] = fromObject
                        itms = newlist.toList()
                    }


                    LazyColumn(
                        modifier = Modifier
                            .padding(innerPadding)
                            .fillMaxSize(),
                        state = listState, // contentPadding = PaddingValues(bottom = 50.dp)
                    ) {

                        items(items = itms, key = {  item ->
                            item.hashCode()
                        }){ item->
                            ReorderableItem(reorderableLazyListState, key = item.hashCode()) { isDragging ->
                                val elevation = if (isDragging) 4.dp else 0.dp
                                Surface(shadowElevation = elevation) {
                                    Row(modifier = Modifier.fillMaxWidth().longPressDraggableHandle()) {
                                        Text(text = "$isDragging")
                                        Text(text = item, modifier = Modifier
                                            .padding(10.dp)
                                            .height(50.dp)
                                            )
                                    }
                                }

                            }
                        }
                    }
                }
            }
        }
    }
}

liltof avatar May 07 '24 20:05 liltof

Nothing changed for me with 2.0.4. @Calvin-LL

https://github.com/Calvin-LL/Reorderable/assets/55230817/6e98bb4c-35d2-4d91-be62-89f369dfc1dd

zhelenskiy avatar May 07 '24 20:05 zhelenskiy

@zhelenskiy I finally manage to reliably reproduce this bug. I have some mildly bad news. As this library currently works right now there's no way for me to fix it until Compose Foundation v1.7.0 gets into Compose Multiplatform (See #4 and #26). But you can add contentPadding = PaddingValues(horizontal = 1.dp) to the LazyRow as a workaround.

Compose Foundation 1.6.0-alpha08 to 1.6.0 took almost 3 months, the latest Compose Multiplatform 1.6.2 uses Compose Foundation 1.6.4 which is a 20-day gap. So expect Compose Foundation v1.7.0 to be in Compose Multiplatform in around 4 months at the current speed.

Unnecessary Details

When the first item swaps with the second, because of the small size difference in this case, the first item is pushed off the screen. Once the item is off screen, LazyRow disposes that item, dragging state is lost.

The Workaround

Adding a small content padding with contentPadding = PaddingValues(horizontal = 1.dp) keeps a little bit of the dragging item on screen so the dragging state is not lost.

Calvin-LL avatar May 07 '24 23:05 Calvin-LL

That solution is OK for me, it also works with my LazyColumn (with a top contentPadding to 1.dp), I'll wait for the new Compose Foundation just to have the final solution but for the moment that's good. thanks! :+1:

liltof avatar May 08 '24 00:05 liltof

The workaround works great, solved both top and bottom issues. Would be nice if I knew this sooner. Thanks!

LOOHP avatar May 08 '24 00:05 LOOHP

Thank you for the workaround! However, I have another issue. When I start moving a tab backwards and forwards a lot, it disappears at some point in time. And find it always being placed at position 1.

https://github.com/Calvin-LL/Reorderable/assets/55230817/203b4552-1e38-4b9c-a7e8-848cd42b6291

zhelenskiy avatar May 08 '24 01:05 zhelenskiy

I never considered the case where an item could be dragged off the viewport. (The gesture detector keeps reporting the items position even when it's outside the window) I will fix that.

Calvin-LL avatar May 08 '24 05:05 Calvin-LL

Again, just for internal planning, is there any ETA?

zhelenskiy avatar May 08 '24 21:05 zhelenskiy

I'll get it sorted out before the end of this week

Calvin-LL avatar May 08 '24 21:05 Calvin-LL

@zhelenskiy try v2.1.0. If you try really hard you can still get an item to lose the dragging state by dragging it wildly outside the window back and forth, that's another bug that will be fixed when Compose Foundation v1.7.0 is out. Thank you so much for finding all these bugs.

Calvin-LL avatar May 09 '24 06:05 Calvin-LL