JetPack-Compose ink painting effect

JetPack-Compose ink painting effect

The likes of big guys are my motivation for writing




The first four chapters
Jetpack-Compose basic layout
JetPack-Compose-custom drawing
JetPack-Compose-Flutter dynamic UI?
JetPack-Compose UI end article

1. Ink painting effect

      I watched the Nuggets two days ago and found that the ink and wash color changes written by a front-end boss are very good. Today, let's use Compose to achieve the effect. The effect is as follows, the effect is the effect made by the front-end big guys.

2. Analyze dynamic effects

1. 1. we can see that the picture [black] changes to [color]
2. After the second click, the irregular area is gradually enlarged and the bottom color photo is displayed

The effect may be relatively simple, but to make it available to everyone, you may need to do a few things:

The picture display size fits the canvas size

Click anywhere to zoom in from here

Customize irregular zoom area

3. Material search

We feel like any Baidu a picture, then open with PS image -> adjustments -> black and white . You can get a black and white picture with ink painting effect and export the picture for backup.

4. PorterDuffXfermode

      In Android drawing, you can use PorterDuffXfermode to mix the pixels of the drawn graphics with the pixels 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 many possible special effects. When using PorterDuffXfermode, pass it as a parameter to the Paint.setXfermode(Xfermode xfermode) method, and then use the pen paint to draw Android will use the incoming PorterDuffXfermode, if you don t want to use Xfermode anymore, you can execute Paint.setXfermode(null ).

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

From the above we can see that PorterDuff.Mode is an enumeration class, with a total of 16 enumeration values:

1. PorterDuff.Mode.CLEAR    The drawn will not be submitted to the canvas. 2. PorterDuff.Mode.SRC    Display the upper layer drawing picture 3. PorterDuff.Mode.DST    Show the lower layer drawing picture 4. PorterDuff.Mode.SRC_OVER    Draw and display normally, and draw overlays on upper and lower layers. 5. PorterDuff.Mode.DST_OVER    Both upper and lower layers are displayed. The lower layer is displayed on the top. 6. PorterDuff.Mode.SRC_IN   Take two layers to draw the intersection. Show the upper layer. 7. PorterDuff.Mode.DST_IN    Take two layers to draw the intersection. Show the lower layer. 8. PorterDuff.Mode.SRC_OUT  Take the upper layer and draw the non-intersecting part. 9. PorterDuff.Mode.DST_OUT  Remove the lower layer and draw the non-intersecting part. 10. PorterDuff.Mode.SRC_ATOP   Remove the non-intersecting part of the lower layer and the intersection of the upper layer 11. PorterDuff.Mode.DST_ATOP  Take the non-intersection part of the upper layer and the intersection part of the lower layer 12. PorterDuff.Mode.XOR   XOR: remove the intersection of two layers 13. PorterDuff.Mode.DARKEN   Take all areas of the two layers, and darken the intersection part 14. PorterDuff.Mode.LIGHTEN   Take all of the two layers and light up the color of the intersection 15. PorterDuff.Mode.MULTIPLY   Take the intersection of the two layers and superimpose the color 16. PorterDuff.Mode.SCREEN   Take all areas of the two layers, the intersection part becomes transparent Copy code

1. Adapt the picture to Canvas

      A good customization is to make it convenient for developers to use, and our pictures are of different sizes, so drawing on the canvas will definitely be of different sizes, then we can set the width and height of the picture to the width and height of the canvas.

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

//Provide our very good tool to zoom in and out of the picture. Get a new Bitmap. Bitmap createScaledBitmap ( @NonNull Bitmap the src, int dstWidth, int dstHeight, Boolean filter) copying the code

We threw two material pictures under the resource file and started drawing the pictures onto the canvas. How big is our canvas, then the picture should be scaled to our canvas. The code is as follows. We create a canvas with a width of 400.dp and a height of 200.dp, and then get the Bitmap scaled to the same size as the canvas and draw it on the canvas.

@Preview @Composable fun InkColorCanvas () { 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 picture, get new bitmap, width and height are consistent with canvas width and height to fit the canvas val multiColorBitmpa = Bitmap.createScaledBitmap( imageBitmap.asAndroidBitmap(), size.width.toInt(), size.height.toInt(), false ) //Black and white picture val blackColorBitmpa = Bitmap.createScaledBitmap( imageBitmap_default.asAndroidBitmap(), size.width.toInt(), size.height.toInt(), false ) //New brush val paint = Paint().asFrameworkPaint() //Draw the picture on the canvas canvas.nativeCanvas.drawBitmap(multiColorBitmpa, 0f , 0f , paint) } } } Copy code

Similarly, we arbitrarily set the width and height of the canvas, it should be automatically scaled. Don't worry about our developers. Even after the screen is rotated, the automatic adaptation will be measured again.

//The entire screen is automatically filled, and the rotating screen is also automatically adapted. Canvas(modifier = Modifier .fillMaxWidth() .fillMaxHeight() ) Copy code

The effect is as follows:

2. Save the current canvas layer

      No matter in the basic UI design software such as

PhotoShop
Medium, or video editing software
Primere
Wait
Layer is the most basic concept
Of course, there is also the concept of layers in programming drawing. In Android drawing, Canvas.saveLayer can save the current canvas content as a layer to the stack, to achieve the concept of a layer, create a new Layer to the "stack", you can use saveLayer, savaLayerAlpha, and launch one from the "stack" Layer, you can use restore, restoreToCount. However, when the layer is pushed into the stack, subsequent DrawXXX operations will occur on this layer, and when the layer is unstacked, the image drawn on this layer will be "drawn" to the upper layer or Canvas. When copying the layer to the Canvas, you can Specify the transparency of the layer (Layer), which is specified when the layer is created: public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags), etc., one sentence
Do it more
.

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

3. Draw black and white Bitmap

      In step two, we have saved the colored canvas as a layer and pushed it into the stack. We again draw a black and white bitmap as the top layer.

@Preview @Composable fun InkColorCanvas () { 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 a color picture canva.nativeCanvas.drawBitmap(multiColorBitmpa, 0f , 0f , paint) //Save the layer to the stack val layerId: Int = canva.nativeCanvas.saveLayer( 0f , 0f , size.width, size.height, paint, ) //The current layer is also the top layer to draw black and white Btmap canva.nativeCanvas.drawBitmap(blackColorBitmpa, 0f , 0f , paint) } } } Copy code

The following picture 1-effect picture. Figure 2-Stacked layers

4. Mixed mode PorterDuff.Mode.DST_IN

        PorterDuff.Mode.DST_IN takes the intersection of two layers of drawing. 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 the radius of 250px.

//PorterDuffXfermode brush pattern provided mixed mode paint.xfermode = PorterDuffXfermode (PorterDuff.Mode.DST_OUT) //circle canva.nativeCanvas.drawCircle (Size.Width/2 , Size.Height/2 , 250f , Paint) copy the code

The effect is as follows:

At this point, I think I have basically finished the most important part.

Fifth, the animation expands the mixed area

       So how do you gradually expand to show all the color pictures? It's very simple, is there any animation? As long as the final radius is larger 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 () { val imageBitmap = getBitmap(R.drawable.csmr) val imageBitmap_default = getBitmap(R.drawable.hbmr) val animal = Animatable( 0.0f ) var xbLength = 0.0f 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 a circle 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 code

6. expand the area and follow the press

      You may want to start expanding the selection wherever you press. It's very simple, just get the screen pointer to get the screen press coordinates, and set it as the starting coordinates of the selection circle.
Get the screen press coordinates through pointerInput.
Remember {mutableStateOf(Offset(0f,0f))} to remember the pressed coordinates

@Preview @Composable fun InkColorCanvas () { 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 ) { val position=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 a circle canva.nativeCanvas.drawCircle( scrrenOffset.value.x, scrrenOffset.value.y, xbLength, paint ) //Canvas hypotenuse paint.xfermode = null canva.nativeCanvas.restoreToCount(layerId) } } } Copy code

7. Irregularly expand electoral districts

      Above, we spread the circle for convenience, because the scale of the circle can be calculated by the radius. But other shapes are not so easy to handle. Of course we can change the region roughly or finely. Here, due to time issues, we roughly calculate the animation to expand the selection. Of course, the principle should be clear.

As shown in the figure above, our path shape can be various. However, it needs to spread to all edges in the final execution. Therefore, the path of our final transformation result must enclose all the canvas area. Combine understanding as follows

@Preview @Composable fun InkColorCanvas () { 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 ) { val position = 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 a circle //canva.nativeCanvas .drawCircle( //scrrenOffset.value.x, //scrrenOffset.value.y, //xbLength, //paint //) val path = Path().asAndroidPath() path.moveTo(scrrenOffset.value.x, scrrenOffset.value.y) //I drew the brother area casually. Of course, the curve can be more beautiful in order 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 code

8. Summary

      Compose affirmed UI must be in the future, of course xml will not be abandoned, but we need to jump out of the comfort zone, do not make excuses, learn to do it, and I have experienced that Compose has brought a good experience in terms of efficiency and customization. I believe everyone who writes Flutter is extremely comfortable. I will continue to write articles when I have time later. These special effects will be collected in ComposeUnit, and the code will be open sourced and shared in the future. I hope to complete ComposeUnit to meet with you. You can preview it if you have been busy writing recently.

The likes of big guys are my motivation
, my QQ learning skirt 730772561