Advanced Android custom controls: Canvas operation

I originally wanted to put the canvas operation in the later part, but I found that many graphics rendering are inseparable from the canvas operation, so I'll explain the basic operation methods of the canvas first.

1, Quick reference table for common operation of Canvas

Operation typeRelated APIremarks
Draw colordrawColor, drawRGB, drawARGBFill the entire canvas with a single color
Draw basic shapedrawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArcPoint, line, rectangle, rounded rectangle, ellipse, circle and arc
Draw picturedrawBitmap, drawPictureDraw bitmaps and pictures
Draw textdrawText, drawPosText, drawTextOnPathDraw text successively, specify each text position when drawing text, and draw text according to the path
Draw pathdrawPathThis function is also needed to draw paths and Bezier curves
vertex operations drawVertices, drawBitmapMeshThe image can be deformed by operating on vertices. drawVertices directly acts on the canvas and drawBitmapMesh only acts on the drawn Bitmap
Canvas clippingclipPath, clipRectSets the display area of the canvas
Canvas snapshotsave, restore, saveLayerXxx, restoreToCount, getSaveCountSave the current state, roll back to the last saved state, save the layer state, roll back to the specified state, and obtain the number of saves
Canvas transformationtranslate, scale, rotate, skewThey are displacement, scaling, rotation and staggered cutting
MatrixgetMatrix, setMatrix, concatIn fact, the displacement, scaling and other operations of the canvas are all image Matrix, but the Matrix is difficult to understand and use, so it encapsulates some common methods.

2, Basic operation of Canvas

1. Canvas operation

Why do I need canvas operation?

Canvas manipulation can help us make graphics in a more understandable way.

For example, how to draw a line segment with a length of 20dp and an angle of 30 degrees with the horizontal line from the coordinate origin?

According to our usual idea (the mathematical thinking that has been trained all the year round), we first use the trigonometric function to calculate the coordinates of the end point of the line segment, and then call drawLine.

However, is this confined by inherent thinking?

Suppose we first draw a horizontal line with a length of 20dp, and then rotate the horizontal line by 30 degrees, the final effect looks the same, and there is no need to calculate the trigonometric function. Is it a little simpler?

Reasonable use of canvas operation can help you create the effect you want in a more understandable way, which is also the reason for the existence of canvas operation.

PS: all canvas operations only affect the subsequent painting, and have no impact on the previously painted content.

(1) displacement (translate)

translate is the movement of the coordinate system. You can select an appropriate coordinate system for drawing. Note that the displacement is based on the current position, not each time based on the (0,0) point in the upper left corner of the screen, as follows:

// The code to create the brush is omitted

// Draw a black circle at the coordinate origin
mPaint.setColor(Color.BLACK);
canvas.translate(200,200);
canvas.drawCircle(0,0,100,mPaint);

// Draw a blue circle at the coordinate origin
mPaint.setColor(Color.BLUE);
canvas.translate(200,200);
canvas.drawCircle(0,0,100,mPaint);

We first move the coordinate system a distance to draw a circle, and then move a distance to draw a circle. The two movements can be superimposed.

(2) scale

Scaling provides two methods, as follows:

public void scale (float sx, float sy)

public final void scale (float sx, float sy, float px, float py)

The first two parameters in the two methods are the same, which are the scale of x-axis and y-axis respectively. The second method has two more parameters than the previous one to control the position of the zoom center.

Detailed explanation of the value range of scaling (sx,sy):

Value range (n)explain
(-∞, -1)Zoom in n times according to the zoom center, and then flip according to the central axis
-1Flip according to the scale center axis
(-1, 0)First zoom out to n according to the zoom center, and then flip according to the central axis
0It will not be displayed. If sx is 0, the width is 0 and will not be displayed. The same is true for sy
(0, 1)Zoom out to n according to the zoom center
1No change
(1, +∞)Zoom in n times according to the zoom center

If you pay a little attention during scaling, you will find that the scaling center is the coordinate origin by default, and the scaling center axis is the coordinate axis, as follows:

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // Rectangular area

mPaint.setColor(Color.BLACK);           // Draw a black rectangle
canvas.drawRect(rect,mPaint);

canvas.scale(0.5f,0.5f);                // Canvas zoom

mPaint.setColor(Color.BLUE);            // Draw a blue rectangle
canvas.drawRect(rect,mPaint);

(in order to be more intuitive, I added a coordinate system. It can be seen that the scaling center is the coordinate origin)

Next, we use the second method to change the zoom center position slightly, as follows:

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // Rectangular area

mPaint.setColor(Color.BLACK);           // Draw a black rectangle
canvas.drawRect(rect,mPaint);

canvas.scale(0.5f,0.5f,200,0);          // Canvas zoom < -- the zoom center is offset by 200 units to the right

mPaint.setColor(Color.BLUE);            // Draw a blue rectangle
canvas.drawRect(rect,mPaint);

(the arrow in the figure indicates the zoom center.)

The scaling values of the previous two examples are positive numbers. According to the instructions in the table, when the scaling scale is negative, it will be flipped according to the scaling center axis. Let's experiment:

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // Rectangular area

mPaint.setColor(Color.BLACK);           // Draw a black rectangle
canvas.drawRect(rect,mPaint);


canvas.scale(-0.5f,-0.5f);          // Canvas zoom

mPaint.setColor(Color.BLUE);            // Draw a blue rectangle
canvas.drawRect(rect,mPaint);

For obvious effect, this time I not only added the coordinate system, but also marked several important points in the rectangle. The points marked with the same letter correspond to each other one by one.

Since the scaling center is not offset this time, all default scaling centers are the coordinate origin, and the central axis is the x-axis and y-axis.

This scaling can be seen as first scaling to 0.5 times the original according to the scaling Center (coordinate origin), and then flipping according to the x-axis and y-axis respectively.

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // Rectangular area

mPaint.setColor(Color.BLACK);           // Draw a black rectangle
canvas.drawRect(rect,mPaint);

canvas.scale(-0.5f,-0.5f,200,0);          // Canvas zoom < -- the zoom center is offset by 200 units to the right

mPaint.setColor(Color.BLUE);            // Draw a blue rectangle
canvas.drawRect(rect,mPaint);

So many auxiliary contents have been added. I hope you can understand them.

This time, the y-axis coordinates of the scaling center point are offset, so the center axis is also offset to the right.

PS: like translate, scaling can be superimposed.

canvas.scale(0.5f,0.5f);
canvas.scale(0.5f,0.1f);

Call scaling twice, then the actual scaling of X axis is 0.5x0.5 = 0.25, and the actual scaling of Y axis is 0.5x0.1=0.05

Let's use this feature to make an interesting figure.

Note: set the brush mode to stroke

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(-400,-400,400,400);   // Rectangular area

for (int i=0; i<=20; i++)
{
	canvas.scale(0.9f,0.9f);
	canvas.drawRect(rect,mPaint);
}

(3) rotate

Rotation provides two methods:

public void rotate (float degrees)

public final void rotate (float degrees, float px, float py)

Like scaling, the two additional parameters of the second method still control the rotation center point.

The default rotation center is still the coordinate origin:

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // Rectangular area

mPaint.setColor(Color.BLACK);           // Draw a black rectangle
canvas.drawRect(rect,mPaint);

canvas.rotate(180);                     // Rotate 180 degrees < -- the defau lt rotation center is the origin

mPaint.setColor(Color.BLUE);            // Draw a blue rectangle
canvas.drawRect(rect,mPaint);

Change the rotation center position:

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // Rectangular area

mPaint.setColor(Color.BLACK);           // Draw a black rectangle
canvas.drawRect(rect,mPaint);

canvas.rotate(180,200,0);               // Rotate 180 degrees < -- the center of rotation is offset 200 units to the right

mPaint.setColor(Color.BLUE);            // Draw a blue rectangle
canvas.drawRect(rect,mPaint);

Well, rotation can also be superimposed

canvas.rotate(180);
canvas.rotate(20);

Call two rotations, and the actual rotation angle is 180 + 20 = 200 degrees.

In order to demonstrate this effect, I made something that I didn't know was fierce:

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

canvas.drawCircle(0,0,400,mPaint);          // Draw two circles
canvas.drawCircle(0,0,380,mPaint);

for (int i=0; i<=360; i+=10){               // Draw a connecting line between circles
   canvas.drawLine(0,380,0,400,mPaint);
   canvas.rotate(10);
}

(4) skew

Skew is translated here as skew, which is a special type of linear transformation.

Staggered cutting only provides one method:

public void skew (float sx, float sy)

Parameter meaning: float sx: tilt the canvas at the corresponding angle in the x direction, sx the tan value of the tilt angle, float sy: tilt the canvas at the corresponding angle in the y axis direction, sy is the tan value of the tilt angle

After transformation:

X = x + sx * y
Y = sy * x + y

Example:

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,0,200,200);   // Rectangular area

mPaint.setColor(Color.BLACK);           // Draw a black rectangle
canvas.drawRect(rect,mPaint);

canvas.skew(1,0);                       // Horizontal stagger < - 45 degrees

mPaint.setColor(Color.BLUE);            // Draw a blue rectangle
canvas.drawRect(rect,mPaint);

As you think, the staggered cut can also be superimposed, but please note that the drawing results will be different with different call order

// Move the coordinate system origin to the center of the canvas
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,0,200,200);   // Rectangular area

mPaint.setColor(Color.BLACK);           // Draw a black rectangle
canvas.drawRect(rect,mPaint);

canvas.skew(1,0);                       // Horizontal stagger
canvas.skew(0,1);                       // Vertical stagger

mPaint.setColor(Color.BLUE);            // Draw a blue rectangle
canvas.drawRect(rect,mPaint);

(5) snapshot (save) and rollback (restore)

Q: Why do snapshots and rollback exist
A: Canvas operations are irreversible, and many canvas operations will affect subsequent steps. For example, in the first example, two circles are drawn at the coordinate origin, and the actual positions drawn are different due to the movement of the coordinate system. Therefore, some states of the canvas will be saved and rolled back.

Related API s:

Related APIbrief introduction
saveSave the state of the current canvas and put it into a specific stack
saveLayerXxxCreate a new layer and put it into a specific stack
restoreTake out the top canvas state in the stack and restore the current canvas according to this state
restoreToCountPop up the status of the specified location and all above, and restore according to the status of the specified location
getSaveCountGet the number of contents in the stack (i.e. save times)

Some of these concepts and methods are analyzed below:

Status stack:

In fact, I don't know the name of this stack. It's temporarily called the status stack. It looks like the following:

This stack can store canvas state and layer state.

Q: What are canvases and layers?
A: In fact, the canvas we see is composed of multiple layers, as shown in the following figure (the picture is from the network):

In fact, the painting and canvas operations we explained earlier are carried out on the default layer.
In general, the default layer can meet the requirements, but if it is necessary to draw more complex content, such as a map (the map can be superimposed by multiple map layers, such as administrative region layer, road layer and point of interest layer), it is better to draw by layer.
You can think of these layers as layers of glass plates. You draw the content on each layer of glass plates, and then stack these glass plates together to see the final effect.

SaveFlags
namebrief introduction
ALL_SAVE_FLAGBy default, all States are saved
CLIP_SAVE_FLAGSave clip area
CLIP_TO_LAYER_SAVE_FLAGThe clipping area is saved as a layer
FULL_COLOR_LAYER_SAVE_FLAGSaves all color channels of a layer
HAS_ALPHA_LAYER_SAVE_FLAGSaves the alpha (opacity) channel of the layer
MATRIX_SAVE_FLAGSave Matrix information (translate, rotate, scale, skew)
save

There are two ways to save:

// Save all status
public int save ()

// Save part of the status according to the saveFlags parameter
public int save (int saveFlags)

You can see that the second method has one more saveFlags parameter than the first method. Using this parameter can save only part of the state, which is more flexible. For details of this saveFlags parameter, please refer to the table above.

Every time the save method is called, a status message will be added at the top of the stack. Take the above status stack image as an example. Calling save again will add a status message on the fifth time.

saveLayerXxx

saveLayerXxx has many methods:

// No layer alpha channel
public int saveLayer (RectF bounds, Paint paint)
public int saveLayer (RectF bounds, Paint paint, int saveFlags)
public int saveLayer (float left, float top, float right, float bottom, Paint paint)
public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)

// There is a layer alpha (opacity) channel
public int saveLayerAlpha (RectF bounds, int alpha)
public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)

Note: the saveLayerXxx method will make you spend more time rendering the image (the superposition of more layers will lead to the doubling of the calculation). Please be careful before using it. If possible, try to avoid using it.

Using the saveLayerXxx method, the layer state will also be put into the state stack. Similarly, use the restore method to restore.

This is not enough for the time being. If you need to explain it in detail later. (because there are many things in it, too, QAQ)

restore

State rollback is to take a state from the top of the stack and restore it according to the content.

Similarly, take the above state stack image as an example. Call the restore method once to take out the state stack for the fifth time and restore the state according to the saved state.

restoreToCount

Pop up the specified location and all the above states, and recover according to the specified location state.

Take the above status stack picture as an example. If restoreToCount(2) is called, the status of 2, 3, 4 and 5 will pop up and be restored according to the status saved for the second time.

getSaveCount

Get the number of saved times, that is, the number of saved states in the state stack. Take the above state stack picture as an example, and the return value of this function is 5.

However, please note that the minimum return value of this function is 1. Even if all statuses pop up, the return value is still 1, representing the default status.

Common formats

Although there is a lot of verbosity about state saving and rollback, in most cases, you only need to remember the following steps:

save();      //Save status
...          //Specific operation
restore();   //Rollback to previous state

This method is also the simplest and easiest to understand.

3, Summary

As mentioned at the beginning of this article, reasonable use of canvas operations can help you create the effects you want in a more understandable way.

Tags: Android Design Pattern UI

Posted on Fri, 03 Dec 2021 19:57:13 -0500 by valshooter