0

YO! I am making a clone of Trello in which the BoardScreen displays a row of Lists, each list contains a List of cards. I want to make the card draggable across the lists so that a card remove from one list and gets saved into another.

After changing the parent from card to box and applying zINdex to the draggable card

I have tried many ways and read many articles about drag and drop implementation in jetpack compose but none of them helped me to know that how could I drag an item of a lazy column outside of its parent view and be draggable on the whole screen.

I am attaching the composable of ListItem which contains the LazyColumn of cards.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BoardListItem(
    modifier: Modifier = Modifier,
    boardList: BoardList,
    viewModel: BoardViewModel,
    state: CardState,
    editableBoardList : BoardList,
    onCardCreated : (CardState,BoardList) -> Unit,
) {


    Card(
        modifier = modifier,
        colors = CardDefaults.cardColors(Color.Black),
        elevation = CardDefaults.cardElevation(8.dp),
        shape = RoundedCornerShape(8.dp)
    ) {
        Column {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(start = 10.dp, end = 10.dp, top = 10.dp),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text(
                    text = boardList.listName,
                    color = LightBlue,
                    fontSize = 16.sp
                )
                Icon(
                    modifier = Modifier.size(20.dp),
                    imageVector = Icons.Default.MoreVert,
                    contentDescription = "options",
                    tint = LightBlue,
                )
            }
            if (boardList.cards.isEmpty()) {
                Spacer(modifier = Modifier.height(70.dp))
            }
            else {

                    val lazyListState = rememberLazyListState()

                    var draggedDistanceY by remember {
                        mutableStateOf(0f)
                    }
                    var draggedDistanceX by remember {
                        mutableStateOf(0f)
                    }

                    var initiallyDraggedElement by remember { mutableStateOf<LazyListItemInfo?>(null) }

                    var currentIndexOfDraggedItem by remember { mutableStateOf<Int?>(null) }


                        LazyColumn(
                            state = lazyListState,
                            modifier = Modifier
                               .pointerInput(Unit) {
                                    detectDragGesturesAfterLongPress(
                                        onDrag = { change, offset ->
                                            change.consume()
                                            draggedDistanceY += offset.y
                                            draggedDistanceX += offset.x

                                        },
                                        onDragStart = { offset ->
                                            lazyListState.layoutInfo.visibleItemsInfo
                                                .firstOrNull { item -> offset.y.toInt() in item.offset..item.offset+200}
                                                ?.also {
                                                    currentIndexOfDraggedItem = it.index
                                                    initiallyDraggedElement = it
                                                }
                                        },
                                        onDragEnd = { },
                                        onDragCancel = { }
                                    )
                                },
                            contentPadding = PaddingValues(
                                top = 20.dp,
                                end = 15.dp,
                                start = 10.dp,
                                bottom = 10.dp
                            )
                        ) {

                            items(boardList.cards.size) { index ->

                                    CardItem(
                                        modifier = Modifier
                                            .fillMaxWidth()
                                            .graphicsLayer {
                                            translationY = draggedDistanceY
                                                .takeIf { index == currentIndexOfDraggedItem } ?: 0f
                                            translationX = draggedDistanceX
                                                .takeIf { index == currentIndexOfDraggedItem } ?: 0f
                                        }
                                            .padding(bottom = 10.dp),
                                        card = boardList.cards[index]
                                    )

                            }
                        }

                    }

                }
            }
            Row(
                modifier = Modifier
                    .padding(start = 10.dp, end = 10.dp, bottom = 12.dp),
                horizontalArrangement = Arrangement.SpaceAround
            ) {
                if (boardList == editableBoardList && state == CardState.EDIT) {
                    TextField(
                        modifier = Modifier
                            .fillMaxWidth(),
                        placeholder = {
                            Text(text = "Card Name", color = Color.Gray)
                        },
                        value = viewModel.cardName.value,
                        onValueChange = {
                            viewModel.cardName.value = it
                        },
                        colors = TextFieldDefaults.textFieldColors(
                            focusedIndicatorColor = LightBlue
                        )
                    )
                }else{
                    Text(
                        modifier = Modifier
                            .clickable {
                                onCardCreated(CardState.EDIT,boardList)
                            },
                        text = "+ Add Card",
                        color = LightBlue,
                        fontSize = 14.sp
                    )
                    Box(modifier = Modifier.fillMaxWidth()) {
                        Icon(
                            modifier = Modifier
                                .size(20.dp)
                                .align(TopEnd),
                            painter = painterResource(id = R.drawable.image_icon),
                            contentDescription = "",
                            tint = Color.White
                        )
                    }
                }

            }
        }
    

And here is the lazyrow which contains the BoardListItem



                LazyRow(
                    state = lazyListScope,
                    modifier = Modifier
                        .padding(top = 20.dp),
                    contentPadding = PaddingValues(end = 15.dp),
                    flingBehavior = flingBehavior
                ) {
                    items(board.lists.size) { index ->
                        BoardListItem(
                            onCardCreated = { cs,boardList ->
                                cardState = cs
                                listBeingEdited = boardList
                                listBeingEdited.isEdited = true
                            },
                            state = cardState,
                            viewModel = viewModel,
                            modifier = Modifier
                                .width(listWidth)
                                .padding(end = 15.dp),

                            editableBoardList = listBeingEdited,
                            boardList = board.lists[index]
                        )
                        if(index == board.lists.size-1) {
                            AddList(
                                modifier = Modifier
                                    .width(width)
                                    .height(height),
                                state = boardState,
                                onClick = {
                                    boardState = it
                                    scope.launch {
                                        lazyListScope.animateScrollToItem(index + 1)
                                    }
                                },
                                viewModel = viewModel
                            )

                        }
                    }
                }

Hope so i was able to explain my problem, any help would be appreciated!

zaid khan
  • 825
  • 3
  • 11

1 Answers1

1

It happens because Card is Surface under the hood and Surface is assigned Modifier.clip(shape) which clips anything drawn out of Composable's bounds.

private fun Modifier.surface(
    shape: Shape,
    backgroundColor: Color,
    border: BorderStroke?,
    elevation: Dp
) = this.shadow(elevation, shape, clip = false)
    .then(if (border != null) Modifier.border(border, shape) else Modifier)
    .background(color = backgroundColor, shape = shape)
    .clip(shape)

You can replace Card with Box with Modifier.background(RoundedCornerShape()) to have same rounded background while not clipping moving content.

enter image description here

@Preview
@Composable
private fun Test() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(30.dp)
    ) {

        Card(
            modifier = Modifier
                .border(1.dp, Color.Red, RoundedCornerShape(10.dp))
                .size(100.dp)
        ) {
            Box(
                Modifier
                    .offset(x = 50.dp, y = 0.dp)
                    .size(100.dp)
                    .background(Color.Green)
            )
        }

        Spacer(modifier = Modifier.height(20.dp))
        Box(
            modifier = Modifier
                .border(1.dp, Color.Red, RoundedCornerShape(10.dp))
                .size(100.dp)
        ) {
            Box(
                Modifier
                    .offset(x = 50.dp, y = 0.dp)
                    .size(100.dp)
                    .background(Color.Green)
            )
        }
    }
}

Edit

To draw one Composable another you need to set zIndex of siblings. They should be direct siblings, and doing so while dragging you can change which one will be drawn another.

Another sample for this is

@Preview
@Composable
private fun Test() {
    LazyRow(
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(10) {
            DraggableBox()
        }
    }
}

@Composable
private fun DraggableBox() {

    var offsetX by remember {
        mutableStateOf(0f)
    }

    var isBeingDragged by remember {
        mutableStateOf(false)
    }

    Box(
        modifier = Modifier
            .zIndex(
                if (isBeingDragged) 10f else 1f
            )
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragStart = {
                        isBeingDragged = true
                    },
                    onDrag = { change: PointerInputChange, dragAmount: Offset ->
                        offsetX += dragAmount.x
                    },
                    onDragEnd = {
                        isBeingDragged = false
                    },
                    onDragCancel = {
                        isBeingDragged = false
                    }

                )
            }
            .background(Color.Gray, RoundedCornerShape(8.dp))
            .padding(10.dp),
        contentAlignment = Alignment.Center
    ) {
        Box(
            modifier = Modifier
                .graphicsLayer {
                    translationX = offsetX
                }
                .size(100.dp)
                .border(2.dp, Color.Green)
        )
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • The card is dragged outside of the box but it is going below the next list, I want that card to be dragged over other lists :__( – zaid khan May 27 '23 at 05:16
  • It's because items are drawn with order. When you drag items from second card you will see that they are drawn on the one that right of the parent. What you should do is change Modifier.zIndex() of parent when any of its item is being dragged. – Thracian May 27 '23 at 06:33
  • the change in zIndex is also not working I was wrong, the card is not going behind another list but it is still being clipped by something. please have a look at the edited question. – zaid khan May 27 '23 at 07:45
  • Z index works but it only works for sibling composables. And as i see from the image root parent also clips but moving a Composable out of it's bounds work and it means the one that contains items are no longer clipping – Thracian May 27 '23 at 13:51
  • You should put border to list items and parent to debug what's going on visually. And i posted a sample above that z index works, you can try it with and without z index to see difference. But for it to work it should be applied to sibling Composables. Just because you assigned z index in one of the LazyColumns it doesn't mean it will be top of everything else. It will be top of other siblings only – Thracian May 27 '23 at 13:52
  • Thanks for the explanation, to make it work i had to make a container and onTheLongClick I created a copy of the that will be draggable anywhere in the container – zaid khan May 27 '23 at 15:04