https://kotlinlang.org logo
#compose
Title
# compose
c

Christopher Elías

06/10/2021, 8:49 PM
Hello guys, I'm using jetpack compose on a project and it's awesome. I'm displaying a Linechart correctly but I believe the performance of the drawing can be improved with coroutines... I'm doing something like this 👇 but it crash with an
UnsupportedException
, does anybody has trying to do the same and actually succeed?
Copy code
@Composable
fun LinearTransactionsChart(
    modifier: Modifier = Modifier,
    transactionsPerSecond: TransactionsPerSecond,
    defaultDispatcher: CoroutineDispatcher
) {
    if (transactionsPerSecond.transactions.isEmpty()) return

    val composableScope = rememberCoroutineScope()

    Canvas(modifier = modifier) {
        composableScope.launch(Dispatchers.Main) {
            emitTransactions(
                defaultDispatcher = defaultDispatcher,
                maxTransaction = transactionsPerSecond.maxTransaction,
                transactions = transactionsPerSecond.transactions,
                canvasWidth = size.width,
                canvasHeight = size.height
            ) { start, end ->
                drawLine(
                    start = start,
                    end = end,
                    color = Color(0xFFFFFFFF)
                )
            }
        }
    }
}

// The list can have like 1K ~ 2K items...
suspend fun emitTransactions(
    defaultDispatcher: CoroutineDispatcher,
    maxTransaction: Double,
    transactions: List<TransactionRate>,
    canvasWidth: Float,
    canvasHeight: Float,
    dots: (start: Offset, end: Offset) -> Unit
) {
	// ... Do some heavy computation for creating the start and end Offset...
    withContext(defaultDispatcher) {       
        transactions.forEachIndexed { index, transactionRate ->           
            dots(Offset(x, y), Offset(x, y)) 
        }
    }
}
🧵 1
n

Nader Jawad

06/11/2021, 8:17 PM
Drawing in compose (and within Android) does not work in a multithreaded manner so even using coroutines like this would not work. More specifically when a View/Composeable is invalidated on screen, all of it's drawing instructions are re-recorded. An alternative approach would be to allocate an ImageBitmap that has the lines drawn into it incrementally as they are added and that bitmap is drawn on screen on each frame. This would be much more performant than trying to draw all of the lines at once each time.
c

Christopher Elías

06/11/2021, 9:01 PM
Uhm... I think I don't understand the ImageBitmap approach, do you have some sample maybe?
n

Nader Jawad

06/11/2021, 9:58 PM
Copy code
@Composable
fun DrawLines(modifier: Modifier = Modifier, pt1: Offset, pt2: Offset) {
    class DrawParams(val imageBitmap: ImageBitmap, val canvas: Canvas, val drawScope: CanvasDrawScope)
    val drawParamsRef = remember { Ref<DrawParams>() }
    Box(modifier = modifier.drawWithCache {
        val canvas: Canvas
        val drawScope: CanvasDrawScope
        val imageBitmap: ImageBitmap
        var drawParams = drawParamsRef.value
        // Create offscreen bitmap if we don't have one. Note this should also be updated
        // if the size changes. This bitmap is persisted across composition calls and retains
        // the previously drawn lines across each composition
        if (drawParams == null) {
            imageBitmap = ImageBitmap(size.width.roundToInt(), size.height.roundToInt())
            canvas = Canvas(imageBitmap)
            drawScope = CanvasDrawScope()
            drawParams = DrawParams(imageBitmap, canvas, drawScope)
            drawParamsRef.value = drawParams
        }
        drawParams.drawScope.draw(this, layoutDirection, drawParams.canvas, size) {
            drawLine(color = Color.Blue, start = pt1, end = pt2)
        }
        onDrawBehind {
            // After the bitmap is updated, draw the bitmap once here
            drawImage(drawParams.imageBitmap)
        }
    })
}

@Composable
fun RandomLinesDemo() {
    var pt1 by remember{ mutableStateOf(Offset.Zero) }
    var pt2 by remember{ mutableStateOf(Offset.Zero) }
    val size = 300
    val density = LocalDensity.current
    val sizeDp = with(density) {
        size.toDp()
    }
    val random = Random.Default
    DrawLines(modifier = Modifier.size(sizeDp).clickable {
        pt1 = Offset(random.nextInt(0, size).toFloat(), random.nextInt(0, size).toFloat())
        pt2 = Offset(random.nextInt(0, size).toFloat(), random.nextInt(0, size).toFloat())

    }, pt1, pt2)
}
With the sample above, we are allocating a bitmap once with the desired dimensions and each time we re-compose we draw an additional line with the points specified into this scratch bitmap. Then on the actual draw call, we just draw the bitmap itself. So on each composition we're only drawing the deltas of the newly added lines instead of drawing every line in the data set on each frame. If you tap the box repeatedly, we generate new random lines but still see the previous ones each time. Note an actual solution would be sure to update the bitmap if the size changes and probably include a clear method etc. etc. but just wanted to keep the bare minimum for the sake of the example
c

Christopher Elías

06/15/2021, 1:11 AM
Thanks for the explanation.
👍 1
8 Views