Exploring Jetpack Compose Canvas: the power of drawing

作者:Julien Salvi

The original link

In this article I’m going to share my experience with Canvas in Jetpack Compose. Jetpack Compose is a new set of UI tools from Google. Android Dev Challenge #2 gave me the opportunity to learn a lot about Canvas and how to use the advantages of Canvas to draw graphics and animate in a very elegant way.

Most of the code examples are based on the following project: github- timepack-Countpose

Disclaimer: The sample code is based on Compose 1.0.0-beta02, and these apis may change in the future.

Be familiar with the Canvas

If you’re familiar with the Canvas of Android View, you won’t be surprised to see the Canvas of Jetpack Compose. All function names are the same, in the treatment of the Path of the API, even Compose Canvas is more clear, such as relativeQuadraticBezierTo alternative rQuadto () () to draw a curve.

If you’re not familiar with the native Android Canvas, I recommend reading Rebecca Franks’ special introduction to Canvas. Getting Started with Android Canvas Drawing

Jetpack Compose includes a Canvas Composable class in the UI component library that allows your APP to release powerful graphics capabilities. I will draw a smiley face and use some simple graphics, such as circle, arc and rectangle, to show the ability of Canvas.

@Composable
fun SmileyFaceCanvas(
    modifier: Modifier
) {
    Canvas(
        modifier = modifier.size(300.dp),
        onDraw = {
            // Head
            drawCircle(
                Brush.linearGradient(
                    colors = listOf(greenLight700, green700)
                ),
                radius = size.width / 2,
                center = center,
                style = Stroke(width = size.width * 0.075 f))// Smile
            val smilePadding = size.width * 0.15 f
            drawArc(
                color = red700,
                startAngle = 0f,
                sweepAngle = 180f,
                useCenter = true,
                topLeft = Offset(smilePadding, smilePadding),
                size = Size(size.width - (smilePadding * 2f), size.height - (smilePadding * 2f)))// Left eye
            drawRect(
                color = dark,
                topLeft = Offset(size.width * 0.25 f, size.height / 4),
                size = Size(smilePadding, smilePadding)
            )
            
            // Right eye
            drawRect(
                color = dark,
                topLeft = Offset((size.width * 0.75 f) - smilePadding, size.height / 4),
                size = Size(smilePadding, smilePadding)
            )
        }
    )
}
Copy the code

We can access DrawScope in the Canvas onDraw lambda expression. This scope allows us to draw anything we want. Remember, the Canvas’s original coordinates (x=0,y=0) are in the top left corner.

To draw the head of the smiley face, we draw a circle using a stroke style. If style is left blank, there is a default padding. All drawing methods support passing a Color or Brush(for adding a gradient of a set of colors). How do I set the radius? We use DrawScope to get the size of the current drawing environment, so we can control the radius based on the size of the current component. The Center property supports passing Offset to set the position of the graph on the Canvas.

Then, we draw a single color to draw a curved mouth, and do the same for the rectangle of the eye. Now our smiley faces can be displayed on the screen:

DrawScope contains a number of DrawScope methods. Here are some of the functions that can be used:

  • drawCircle() // draws a circle at given coordinates
  • drawArc() // draws an arc scaled to fit inside a given rectangle
  • drawImage() // draws an ImageBitmap in the canvas
  • drawPoints() // draws a sequence of points
  • drawPath() // draws a path with a given color

There are many more…… Now let’s look at how to animate elements drawn on the Canvas.

Canvas and animation

Having covered some Canvas basics, let’s look at implementing some more complex animations and UIs. For Android Dev Challenge #2, I decided to create a wave animation that would slowly pan down while playing the wave animation until the time was up.

DrawScope adds great support for animating Canvas elements directly. You can use translation, rotation, or scale. To do the wave animation for the timer, we are going to use pan animation.

First, we define two types of animationStates to implement animation. In order to achieve a constant waves of animation, we use rememberInfiniteTransition () will be a floating point number from 0 to 1 infinite loop. We then expose the values of the animation via animateFloat() and the associated specification.

For all finite animations, we use the animationState-related functions directly, such as animateFloatAsState, animateColorAsState… You can set the target value of the animation and define the Specification of the animation.

val deltaXAnim = rememberInfiniteTransition()
val dx by deltaXAnim.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(500, easing = LinearEasing)
    )
)

val screenWidthPx = with(LocalDensity.current) {
    (LocalConfiguration.current.screenHeightDp * density)
}
val animTranslate by animateFloatAsState(
    targetValue = screenWidthPx,
    animationSpec = TweenSpec(10000, easing = LinearEasing)
)

val waveHeight by animateFloatAsState(
    targetValue = 0f,
    animationSpec = TweenSpec(10000, easing = LinearEasing)
)
Copy the code

With the animation state defined, we can begin to animate the wave. To draw this wave, we are going to add a Bezier curve with Path, which will look a bit like a sine function. After the wave is drawn, we need to wrap the wave with the translate translate() lambda function, which operates within the DrawScope, and then dynamically change the top pixel value of the wave with the value of the animation state.

Canvas(
    modifier = modifier.size(300.dp),
    onDraw = {
        translate(top = animTranslate) {
            drawPath(path = path, color = animColor)
            path.reset()
            val halfWaveWidth = waveWidth / 2
            path.moveTo(-waveWidth + (waveWidth * dx), originalY.dp.toPx())

            for (i in-waveWidth.. (size.width.toInt() + waveWidth) step waveWidth) { path.relativeQuadraticBezierTo( halfWaveWidth.toFloat() /2,
                    -waveHeight,
                    halfWaveWidth.toFloat(),
                    0f
                )
                path.relativeQuadraticBezierTo(
                    halfWaveWidth.toFloat() / 2,
                    waveHeight,
                    halfWaveWidth.toFloat(),
                    0f
                )
            }

            path.lineTo(size.width, size.height)
            path.lineTo(0f, size.height)
            path.close()
        }
    }
)
Copy the code

Below is a full animation.

Draw text content with native Canvas

Currently, you can’t draw Text directly on Jetpack Compose’s Canvas. To draw text, we can only use the Android framework’s native Canvas to draw text messages. In onDraw’s lambda, you can get nativeCanvas to manipulate the embedded canvas by calling drawIntoCanvas (which is useful because you can reuse some previously developed logic) and then you can call all of the methods associated with the nativeCanvas, such as drawText, DrawVertices and so on.

To style text, you must use Paint. Because we are now using the native canvas, we cannot use Compose’s drawText function directly. To get an instance of native paint, we can use the function asFrameworkPaint() to handle Android.graphics.paint.

Here is a code snippet for drawing text messages on a native canvas:

val textPaint = Paint().asFrameworkPaint().apply {
    isAntiAlias = true
    textSize = 24.sp.toPx()
    color = android.graphics.Color.BLUE
    typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
}
Canvas(
    modifier = modifier.fillMaxSize(),
    onDraw = {
        drawIntoCanvas {
            it.nativeCanvas.drawText(
                "My Jetpack Compose Text"
                0f.// x-coordinates of the origin (top left)
                120.dp.toPx(), // y-coordinates of the origin (top left)
                textPaint
            )
        }
    }
)
Copy the code

Here is an implementation added to the sample code.

You can use all of the Jetpack Compose Canvas animations (pan, rotate, zoom, etc.) and then wrap the drawIntoCanvas to animate the content you’re drawing.

Canvas(
    modifier = modifier.fillMaxSize(),
    onDraw = {
        translate(top = animTranslate * 0.92 f) {
            scale(scale = if (timePackViewModel.alertState.value!!) animAlertScale else 1f) {
                drawIntoCanvas {
                    it.nativeCanvas.drawText(
                        "My Jetpack Compose Text".0f.120.dp.toPx(),
                        textPaint
                    )
                }
            }
        }
    }
)
Copy the code

Using Canvas can unlock a lot of design possibilities! It might be easy to do at first, but as you go along you might find that if you want to do a complicated path drawing you need a very complicated mathematical algorithm. Jetpack Compose Canvas has a lot of similar support that you can take advantage of.

Finally, I would like to thank Annyce Davis for her review and feedback of many good suggestions.