👍The big guys’ thumbs up is what keeps me writing👍







The first four chapters

Jetpack-compose basic layout

Jetpack-compose – Custom drawing

Jetpack-comement-flutter dynamic UI?

The Jetpack-compose UI concludes

First, the effect of ink painting

Two days ago, I found that the ink painting written by a front-end tycoon changed color effect very nice. Today, we use Compose to achieve the effect. The effect is as follows. The effect is what the front end bosses do.

Second, analyze the dynamic effect

1. First, we can see that the picture [black] changes to [color]. 2

The effect may be relatively simple, but to provide you with good use may need to do a few points:

Click anywhere to zoom in from here. Custom irregular enlargement area

Third, material search

We search for any image we like and then go to Photoshop -> Adjust -> Black and white. You can get the black and white picture of the ink painting effect, export the picture for use.

Four, PorterDuffXfermode

In Android drawing, PorterDuffXfermode can be used to mix the pixel of the drawing graph with the pixel of the corresponding position in the Canvas according to certain rules to form a new pixel value to update the final pixel color value in the Canvas, which will create a lot of possible special effects. SetXfermode (Xfermode Xfermode), and Android will use the PorterDuffXfermode passed in when drawing with that Paint. If you no longer want to use Xfermode, you can execute paint.setxfermode (null).

PorterDuffXfermode supports the following ten pixel color mixing modes, which are: CLEAR, SRC, DST, SRC_OVER, DST_OVER, SRC_IN, DST_IN, SRC_OUT, DST_OUT, SRC_ATOP, DST_ATOP, XOR, DARKEN, LIGHTEN, MULTIPLY, SCREEN.

Porterduff. Mode is an enumeration class with a total of 16 enumerations:

1.Porterduff.mode. CLEAR draws are not committed to the canvas.2.Porterduff.mode. SRC displays the upper rendering image3.Porterduff.mode. DST displays the underlying drawing image4.Porterduff.mode. SRC_OVER normal drawing display, upper and lower drawing overlay.5.Porterduff.mode. DST_OVER is displayed on both upper and lower levels. The lower strata are shown above.6.Porterduff.mode. SRC_IN draws the intersection of two layers. Display the upper layer.7.Porterduff.mode. DST_IN draws the intersection of two layers. Show the lower layer.8.Porterduff.mode. SRC_OUT takes the non-intersection part of the upper layer to draw.9.Porterduff.mode. DST_OUT takes the lower layer and draws the non-intersection part.10.Porterduff.mode. SRC_ATOP takes the non-intersection part of the lower layer and the intersection part of the upper layer11.Porterduff.mode. DST_ATOP takes the upper non-intersection part and the lower intersection part12.Porterduff.mode. XOR XOR: Remove the intersection of two layers13.Porterduff.mode. DARKEN takes the whole area of the two layers and deepens the color of the intersection part14.Porterduff.mode. LIGHTEN both layers and LIGHTEN the intersection of colors15.Porterduff.mode. MULTIPLY takes the color of the intersection of the two layers16.Porterduff.mode. SCREEN takes the entire area of the two layers and turns the intersection into a transparent colorCopy the code

1. Adapt the image to Canvas

A good custom, to make developers easy to use, and our image size is different, it will be different to draw on the canvas, so we set the width and height of the image to the width and height of the canvas.

Android we know that the image compression is divided into quality compression, sampling rate compression and so on. If you are familiar with it, you should be familiar with the following method.

// Provides us with a good tool for the length and width to scale the image. Get the new Bitmap.
Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,
boolean filter)
Copy the code

We drop two stock images under the resource file and start drawing the image onto the canvas. How big our canvas is, so the image should scale to our canvas. The code is as follows: we create a canvas 400.dp wide and 200.dp high, and then get the Bitmap scaled to match the canvas size and draw it on the canvas.

@Preview
@Composable
fun InkColorCanvas(a) {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    Canvas(
        modifier = Modifier
            .width(400.dp)
            .height(200.dp)
    ) {
        drawIntoCanvas { canva ->
            // Color image, get a new bitmap, the width and height of the canvas are consistent with the canvas width and height
            val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            // Black and white image
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                    imageBitmap_default.asAndroidBitmap(),
                    size.width.toInt(),
                    size.height.toInt(),
                    false
                )
            // Create a new brush
            val paint = Paint().asFrameworkPaint()
            // Draw the image onto the canvas
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f.0f, paint) 
        }
    }
}
Copy the code

Also, if we set the width and height of the canvas, it should automatically scale. Don’t worry us developers. The automatic fit is measured again even after the screen is rotated.

 // Automatically fills the entire screen, and the rotation screen also automatically ADAPTS.
 Canvas(modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
    )
Copy the code

The effect is as follows:

2. Save the current canvas layer

Whether in basic UI design software such as PhotoShop or video editing software such as Primere, layers are a basic concept, and of course there are layers in programming and drawing. Android Canvas. SaveLayer can save the current Canvas content as a Layer on the stack, to achieve the concept of Layer, create a new Layer on the “stack”, SaveLayer, savaLayerAlpha, can be used to push aLayer from the stack, and restore,restoreToCount can be used. However, when Layer is stacked, subsequent DrawXXX operations will take place on this Layer, and when Layer is unstacked, the image drawn by this Layer will be “drawn” to the upper Layer or Canvas. When copying Layer to Canvas, the transparency of Layer can be specified. This is specified at Layer creation: public int saveLayerAlpha(RectF Bounds, int alpha, int saveFlags) etc.

// Save the layer
val layerId: Int = canva.nativeCanvas.saveLayer(
    0f.0f,
    size.width,
    size.height,
    paint,
)
Copy the code

3. Draw black and white Bitmap

In step 2 we saved the colored canvas as a layer and pushed it inside the stack. Let’s draw the black and white Bitmap again as the top layer.

@Preview
@Composable
fun InkColorCanvas(a) {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
    ) {
        drawIntoCanvas { canva ->
            val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                    imageBitmap_default.asAndroidBitmap(),
                    size.width.toInt(),
                    size.height.toInt(),
                    false
                )
            val paint = Paint().asFrameworkPaint()
            // Draw color images
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f.0f, paint) 
            // Save layer to stack
            val layerId: Int = canva.nativeCanvas.saveLayer(
                0f.0f,
                size.width,
                size.height,
                paint,
            )
            // The current layer is also the top layer and draws black and white Btmap
            canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f.0f, paint)

        }
    }
}
Copy the code

Figure 1- Effect drawing. Figure 2- Stack layers

4, mixed Mode porterduff.mode.dst_in

Porterduff.mode. DST_IN draws the intersection of two layers. Show the lower layer. Next we set the brush blend Mode to porterduff.mode. DST_IN and draw a circle with the center of the screen and a radius of 250px.

//PorterDuffXfermode Sets the graphic blending mode of the brush
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)/ / draw circles
canva.nativeCanvas.drawCircle(size.width / 2, size.height / 2.250f, paint)            
Copy the code

The effect is as follows:

I think I’ve got the most important part covered here.

5. Animation expands the mixing area

So how do you get all the color images to zoom out? It’s very simple. There is no animation. As long as the final radius is greater than the diagonal of the canvas, change the radius from 0 to more than half the length of the hypotenuse.

Pythagorean theorem hypotenuse = SQRT (size.width.todouble ().pow(2.0) + size.height.todouble ().pow(2))

@Preview
@Composable
fun InkColorCanvas(a) {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    val animal = Animatable(0.0 f)
    var xbLength = 0.0 f
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight().pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        val offset = awaitPointerEventScope {
                            awaitFirstDown().position
                        }
                        launch {
                            animal.animateTo(
                                xbLength,
                                animationSpec = spring(stiffness = Spring.DampingRatioLowBouncy)
                            )
                        }

                    }
                }
            }
    ) {
        drawIntoCanvas { canva ->
            val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap_default.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(),
                false
            )
            val paint = Paint().asFrameworkPaint()
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f.0f, paint) // Draw a picture
            // Save the layer
            val layerId: Int = canva.nativeCanvas.saveLayer(
                0f.0f,
                size.width,
                size.height,
                paint,
            )
            canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f.0f, paint)
            //PorterDuffXfermode Sets the graphic blending mode of the brush
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
            / / draw circles
            canva.nativeCanvas.drawCircle(
                size.width / 2,
                size.height / 2,
                animal.value,
                paint
            )
            // Canvas hypotenuse
            xbLength = kotlin.math.sqrt(size.width.toDouble().pow(2.0) + size.height.toDouble().pow(2)).toFloat()
            paint.xfermode = null
            canva.nativeCanvas.restoreToCount(layerId)
        }
    }
}
Copy the code

Six, expand the area follow press

You might want to start expanding selection anywhere you press. Very simple, get screen pointer get screen press coordinates, set to the starting coordinates of the selection circle. Remember the pressed coordinates with remember {mutableStateOf(Offset(0f,0f))


@Preview
@Composable
fun InkColorCanvas(a) {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    val scrrenOffset = remember { mutableStateOf(Offset(0f.0f))}val animalState = remember { mutableStateOf(false)}val animal: Float by animateFloatAsState(
        if (animalState.value) {
            1f
        } else {
            0f
        }, animationSpec = TweenSpec(durationMillis = 4000)
    )
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight().pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        valposition=awaitPointerEventScope { awaitFirstDown().position } launch { scrrenOffset.value= Offset(position.x,position.y)  animalState.value=! animalState.value } } } } ) { drawIntoCanvas { canva ->val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap_default.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(),
                false
            )
            val paint = Paint().asFrameworkPaint()
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f.0f, paint) // Draw a picture
            // Save the layer
            val layerId: Int = canva.nativeCanvas.saveLayer(
                0f.0f,
                size.width,
                size.height,
                paint,
            )
            canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f.0f, paint)
            //PorterDuffXfermode Sets the graphic blending mode of the brush
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
            val xbLength = kotlin.math.sqrt(size.width.toDouble().pow(2.0) + size.height.toDouble().pow(2)).toFloat()*animal
            / / draw circles
            canva.nativeCanvas.drawCircle(
                scrrenOffset.value.x,
                scrrenOffset.value.y,
                xbLength,
                paint
            )
            // Canvas hypotenuse
            paint.xfermode = null
            canva.nativeCanvas.restoreToCount(layerId)
        }
    }
}
Copy the code

7. Expanding electoral districts irregularly

Above we diffuse for the convenience of the circle, because the circle’s scale can be calculated by the radius. But other shapes are not so easy to work with. Of course, we can transform the region roughly or precisely. Here because of the time problem, we roughly calculate the animation of the enlarged selection. Of course, the principle should be clear.

As shown above our path shapes can be varied. But implementation eventually needs to spread to all edges. So the path of our final transformation must cover all the canvas areas. Combined with the following understanding

@Preview
@Composable
fun InkColorCanvas(a) {
    val imageBitmap = getBitmap(R.drawable.csmr)
    val imageBitmap_default = getBitmap(R.drawable.hbmr)
    val scrrenOffset = remember { mutableStateOf(Offset(0f.0f))}val animalState = remember { mutableStateOf(false)}val animal: Float by animateFloatAsState(
        if (animalState.value) {
            1f
        } else {
            0f
        }, animationSpec = TweenSpec(durationMillis = 6000)
    )
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
            .pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        valposition = awaitPointerEventScope { awaitFirstDown().position } launch { scrrenOffset.value = Offset(position.x, position.y) animalState.value = ! animalState.value } } } } ) { drawIntoCanvas { canva ->val multiColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(), false
            )
            val blackColorBitmpa = Bitmap.createScaledBitmap(
                imageBitmap_default.asAndroidBitmap(),
                size.width.toInt(),
                size.height.toInt(),
                false
            )
            val paint = Paint().asFrameworkPaint()
            canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f.0f, paint) // Draw a picture
            // Save the layer
            val layerId: Int = canva.nativeCanvas.saveLayer(
                0f.0f,
                size.width,
                size.height,
                paint,
            )
            canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f.0f, paint)
            //PorterDuffXfermode Sets the graphic blending mode of the brush
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
            val xbLength = kotlin.math.sqrt(size.width.toDouble().pow(2.0) + size.height.toDouble().pow(2)).toFloat() * animal
            / / draw circles
// canva.nativeCanvas.drawCircle(
// scrrenOffset.value.x,
// scrrenOffset.value.y,
// xbLength,
// paint
/ /)
            val path = Path().asAndroidPath()
            path.moveTo(scrrenOffset.value.x, scrrenOffset.value.y)
            // draw a random area. Of course the curves can be more beautiful to look good.
            if (xbLength>0) {
                path.addOval(
                    RectF(
                        scrrenOffset.value.x - xbLength,
                        scrrenOffset.value.y - xbLength,
                        scrrenOffset.value.x + 100f + xbLength,
                        scrrenOffset.value.y + 130f + xbLength
                    ), android.graphics.Path.Direction.CCW
                )
                path.addCircle(
                    scrrenOffset.value.x, scrrenOffset.value.y, 100f + xbLength,
                    android.graphics.Path.Direction.CCW
                )
                path.addCircle(
                    scrrenOffset.value.x-100, scrrenOffset.value.y-100.50f + xbLength,
                    android.graphics.Path.Direction.CCW
                )
            }
            path.close()
            canva.nativeCanvas.drawPath(path, paint)
            // Canvas hypotenuse
            paint.xfermode = null
            canva.nativeCanvas.restoreToCount(layerId)
        }
    }
}
Copy the code

Eight, summary

The Compose declarative UI will definitely have a future. Of course, XML will not be left behind, but we need to step out of our comfort zone and make no excuses to learn how to do things. My experience with Compose has been very good in terms of efficiency and customization. I will continue to write articles when I have time. These effects will be collected into ComposeUnit, and the code will be open source and shared in the future. I hope to complete ComposeUnit to meet you. Recently more busy to write less can preview. 👍👍👍👍 big guys praise is my motivation 👍👍👍👍👍, MY QQ learning skirt 730772561