androidkotlinandroid-jetpack-compose

How to use a custom composable as drag decoration in jetpack Compose?


I'm working on an app where i want to use a custom composable as the drag decoration when dragging an element.

By default, jetpack compose uses the dragged element itself as the drag decoration, but i want to use a different composable instead.

If I do nothing, compose will automatically use the source element as the drag decoration, like in the example below:

Demo 1

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                App(
                    modifier = Modifier
                        .padding(innerPadding)
                        .fillMaxSize()
                )
            }
        }
    }
}

@Composable
fun DragSource(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
            .background(color = Color.Yellow)
            .size(100.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(text = "Drag Me")
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun App(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier,
        contentAlignment = Alignment.Center
    ) {
        DragSource(
            modifier = Modifier.dragAndDropSource {
                detectTapGestures(
                    onLongPress = {
                        startTransfer(
                            DragAndDropTransferData(
                                ClipData.newPlainText("image uri", "image uri")
                            )
                        )
                    }
                )
            }
        )
    }
}

Now, instead of using the source element as the drag decoration, I want to use a custom composable like this one:

@Composable
fun DraggedShape(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
            .size(100.dp)
            .background(color = Color.Red, shape = RoundedCornerShape(16.dp))
    )
}

How can I achieve this?

I know the dragAndDropSource provides a drawDragDecoration parameter where you can draw custom decorations using the canvas api, like in the example below, i draw a blue rect:

Demo 2

DragSource(
    modifier = Modifier.dragAndDropSource(
        drawDragDecoration = { // draw scope
            drawRect(color = Color.Blue)
        }
    ) {
        //...
    }
)

However, this only allows drawing with canvas, and I need to use a composable as the drag decoration, not just a a shape that i can drag with canvas api.

One idea i have is to somehow convert the composable into a bitmap and then draw it in the drawDragDecoration lambda using the canvas api (i do not know even if this is possible in compose). Is there a better way to do this? Any workarounds or suggestions would be greatly appreciated!

Thanks in advance.


Solution

  • As you suggested you can do it by turning that composable to ImageBitmap using , and easiest way is using rememberGraphicsLayer to get GraphicsLayer and

    graphicsLayer.record {
        this@drawWithContent.drawContent()
    }
    

    to record a Composable drawing and turn it into a ImageBitmap with

    onPress = {
        if (imageBitmap == null){
            coroutineScope.launch {
                imageBitmap = graphicsLayer.toImageBitmap()
            }
        }
    }
    

    Result

    enter image description here

    Full code

    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    fun App(modifier: Modifier = Modifier) {
        Box(
            modifier = modifier,
            contentAlignment = Alignment.Center
        ) {
    
            val graphicsLayer: GraphicsLayer = rememberGraphicsLayer()
            val coroutineScope = rememberCoroutineScope()
            var imageBitmap by remember {
                mutableStateOf<ImageBitmap?>(null)
            }
    
            Box(modifier = Modifier
                .drawWithContent {
                    graphicsLayer.record {
                        this@drawWithContent.drawContent()
                    }
                }
                .size(100.dp)
                .background(Color.Green, CutCornerShape(16.dp)),
                contentAlignment = Alignment.Center
            ) {
                Text("Custom shape")
            }
            DragSource(
                modifier = Modifier.dragAndDropSource(
                    drawDragDecoration = {
                        imageBitmap?.let {
                            drawImage(it)
                        }
                    }
                ) {
                    detectTapGestures(
                        onPress = {
                            if (imageBitmap == null) {
                                coroutineScope.launch {
                                    imageBitmap = graphicsLayer.toImageBitmap()
                                }
                            }
                        },
                        onLongPress = {
    
                            startTransfer(
                                DragAndDropTransferData(
                                    ClipData.newPlainText("image uri", "image uri")
                                )
                            )
                        }
                    )
                }
            )
        }
    }