preface

In fact, the really advanced part is the three points, rendering, filter, and graph combination. We also need to focus on mastering Canvas, another important object for our graph drawing. So this time, let’s have a deeper study of Canvas. There are two main points

  1. Canvas transform using skills
  2. Canvas state, Canvas Layer

1. Basic concept of Canvas

Face to face means Canvas, which is actually a separate tool class (drawing session, used to communicate with the bottom layer and finally handed over to the bottom layer). A Canvas object has four basic elements:

  • One is a bitmap that holds pixels
  • A Canvas draws on a Bitmap
  • Drawn thing
  • Paint for drawing

2.Canvas transformation operation —- coordinate system concept

There will be a problem when we carry out the canvas operation. When we carry out the translation and rotation operation of the graph, we do not change the original coordinates, but just move the graph directly through some very simple apis. Then what exactly happened in the process? In the previous draw process, we found that in the code below, which I have reduced, a rectangle was generated at the beginning of the draw and it was initialized by the panel

 private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; .final Rect dirty = mDirty;
    if(mSurfaceHolder ! =null) {
        // The app owns the surface, we won't draw.
        dirty.setEmpty();
        if(animating && mScroller ! =null) {
            mScroller.abortAnimation();
        }
        return;
    }

    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        dirty.set(0.0, (int) (mWidth * appScale + 0.5 f), (int) (mHeight * appScale + 0.5 f));
    }

    int xOffset = -mCanvasOffsetX;
    int yOffset = -mCanvasOffsetY + curScrollY;
    final WindowManager.LayoutParams params = mWindowAttributes;
    finalRect surfaceInsets = params ! =null ? params.surfaceInsets : null;
    if(surfaceInsets ! =null) {
        xOffset -= surfaceInsets.left;
        yOffset -= surfaceInsets.top;

        // Offset dirty rect for surface insets.dirty.offset(surfaceInsets.left, surfaceInsets.right); }...if(! drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {return; }}}if (animating) {
        mFullRedrawNeeded = true; scheduleTraversals(); }}Copy the code

In the above code, we can see that at the beginning of drawing, a drawing area is determined at the bottom and the coordinates of the drawing position of canvas are determined, so this is the coordinate system called our canvas to determine the position of the drawing of our canvas. So, when we do

    canvas.translate(50.50);
    canvas.rotate(45);
    canvas.scale(1.5 f.0.5 f);
    canvas.skew(1.73 f.0);
Copy the code

During the operation, our graph drawing will be directly changed. At this time, we need to consider a question: has the green point in the picture moved to the red point? Has the canvas we set just now moved

In fact, many readers will think that the coordinates of our canvas have been moved, but in fact, there are two kinds of coordinate systems involved in canvas: its own coordinate system and drawing coordinate system

2.1 Canvas coordinate system

It’s in the upper left corner of the View, the origin is the positive X axis to the right, the positive Y axis to the bottom, and there’s only one, the only constant and that’s actually the point in the canvas that the surface initialized at the beginning of the drawing

2.2 Plotting Coordinate System

It is not the only constant, it is related to the Matrix of Canvas. When the Matrix changes, the drawing coordinate system will change correspondingly. One of its characteristics is that it is irreversible in this process

So what happens is that when we draw, we have a master panel, the master panel doesn’t move, and when I start drawing, we have a panel that moves all the time, and this panel is our drawing board

So the inner drawing coordinate system is actually represented by a Matrix Matrix, which is similar to the filter Matrix we used before, except that the Matrix of the drawing coordinate system is a 2×2 Matrix and the input value is calculated by our canvas after parsing and giving the desired data to the bottom layer

public void drawRect(@NonNull Rect r, @NonNull Paint paint) {
    throwIfHasHwBitmapInSwMode(paint);
    drawRect(r.left, r.top, r.right, r.bottom, paint);
}
Copy the code

So we can see here that before entering into the underlying native method, the implementation will pass in the underlying data according to the difference of each drawing method, and then calculate the drawing coordinate system of our door (the underlying layer is not to be looked at here, but c is involved, so we can understand this point here).

We simply set Translate, rotate, Scale and Skew to change the position of the drawing graph. His calculation relies on another matrix to change the drawing coordinate system, which is a 3×3 matrix with nine parameters in it

cosX -sinX translateX sinX cosX translateY 0 0 scale

Where sine of x and cosine of x are the sines and cosines of the rotation angles. Note that the positive direction of rotation is clockwise. TranslateX and translateY stand for shifted X and Y. Scale stands for scale.

We can get to this matrix by getMatrix(), and by looking at the underlying source code, I can clearly see that we are calling the underlying matrix directly

@Deprecated
public void getMatrix(@NonNull Matrix ctm) {
    nGetMatrix(mNativeCanvasWrapper, ctm.native_instance);
}
Copy the code

So here I did a set of tests

 RectF r = new RectF(0.0.400.500);
    paint.setColor(Color.GREEN);
    canvas.drawRect(r, paint);
    float[] fs = new float[10];
            canvas.getMatrix().getValues(fs);
    for (int i = 0; i < fs.length; i++){ Log.i("barry"."fs:"+fs[i]);
    }
Copy the code
    / / translation
    canvas.translate(50.50);
    float[] fs2 = new float[10];
    canvas.getMatrix().getValues(fs2);
    for (int i = 0; i < fs2.length; i++){ Log.i("barry"."fs2:"+fs2[i]);
    }

    paint.setColor(Color.BLUE);
    canvas.drawRect(r, paint);
Copy the code

And you can obviously see what happens to the information in this matrix when you shift it. So notice that the coordinate system movement of the drawing matrix is an irreversible state, that is, once the matrix has been moved, it cannot go back to its previous position. The effect is as follows

But there are save and restore methods in our Canvas to save and restore the changes,

    RectF r = new RectF(0.0.400.500);
    paint.setColor(Color.GREEN);
    // After drawing, the drawing coordinate system is positioned here
    canvas.drawRect(r, paint);
    //save Saves the current coordinates
    canvas.save();

    // After translation, the coordinate system changes
    canvas.translate(50.50);
    
    paint.setColor(Color.BLUE);
    canvas.drawRect(r, paint);
    // Use restore to restore to the coordinate system where save was saved
    canvas.restore();
Copy the code

However, if we want to know how these two methods operate, we can be more deeply familiar with the using skills of Canvas. Then we must understand the state stack and Layer stack of Canvas

3. State preservation of Canvas — state stack and Layer stack

3.1 state of the stack

Previously, we mentioned that coordinate system conversion is irreversible, and we can save restore by saving. In fact, during the save operation, the coordinate system saved by us will be saved to a stack in the canvas. And it can be operated on either restore or restoreToCount with a test code to verify that

public class MyView extends View {
private static final String TAG = "BARRY";

private Paint mPaint = null;
private Bitmap mBitmap = null;

public MyView(Context context) {
    this(context, null);
}

public MyView(Context context, AttributeSet attrs) {
    super(context, attires
    mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lsj);
    init();
}

public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

private void init(a) {
    mPaint = new Paint();
    mPaint.setColor(Color.RED);
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setStrokeWidth(10);
}

@Override
protected void onDraw(Canvas canvas) {
    // Save the first time and pass canvas.getSaveCount to the current stack size
    canvas.save();
    Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());

    canvas.translate(400.400);
    RectF rectF = new RectF(0.0.600.600);
        canvas.drawBitmap(mBitmap, null, rectF, mPaint);
    // Save the second time and pass canvas.getSaveCount to the current stack size
    canvas.save();
    Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());

    canvas.rotate(45);

    canvas.drawBitmap(mBitmap, null, rectF, mPaint);
    // Save the third time and pass canvas.getSaveCount to the current stack size
    canvas.save();
    Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());

    canvas.rotate(45);

    canvas.drawBitmap(mBitmap, null, rectF, mPaint);
    // Save the fourth time and pass canvas.getSaveCount to the current stack size
    canvas.save();
    Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());
    // Push the stack to layer 3 state with Canvas. restoreToCount
    canvas.restoreToCount(3);
    Log.i(TAG, "restoreToCount--Current SaveCount = " + canvas.getSaveCount());

    canvas.translate(0.200);

    / / rectF = new rectF,0,600,600 (0);
    canvas.drawBitmap(mBitmap, null, rectF, mPaint);
    // Push the stack to level 1 (the original level) via Canvas. restoreToCount
    canvas.restoreToCount(1);
    Log.i(TAG, "restoreToCount--Current SaveCount = " + canvas.getSaveCount());
    canvas.drawBitmap(mBitmap, null, rectF, mPaint); }}Copy the code

In fact, we can directly understand that each save actually uses a stack to store my drawing coordinate system. This stack is called state stack, and our restore is a process of stack removal. The save and restore methods save and restore the Matrix transformation operation and Clip clipping

3.2 Layer stack

In our canvas, a saveLayer API is provided to create a new layer. Subsequent drawing operations are performed on the new layer and updated to the corresponding layer and canvas when we call Restore or restoreToCount

Let’s verify the current conclusion with the effect of this test code

public class MyView extends View {

Paint mPaint;
float mItemSize = 0;
float mItemHorizontalOffset = 0;
float mItemVerticalOffset = 0;
float mCircleRadius = 0;
float mRectSize = 0;
int mCircleColor = 0xffffcc44;/ / yellow
int mRectColor = 0xff66aaff;/ / blue
float mTextSize = 25;

private static final Xfermode[] sModes = {
        new PorterDuffXfermode(PorterDuff.Mode.CLEAR),
        new PorterDuffXfermode(PorterDuff.Mode.SRC),
        new PorterDuffXfermode(PorterDuff.Mode.DST),
        new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER),
        new PorterDuffXfermode(PorterDuff.Mode.DST_OVER),
        new PorterDuffXfermode(PorterDuff.Mode.SRC_IN),
        new PorterDuffXfermode(PorterDuff.Mode.DST_IN),
        new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT),
        new PorterDuffXfermode(PorterDuff.Mode.DST_OUT),
        new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP),
        new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP),
        new PorterDuffXfermode(PorterDuff.Mode.XOR),
        new PorterDuffXfermode(PorterDuff.Mode.DARKEN),
        new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
        new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY),
        new PorterDuffXfermode(PorterDuff.Mode.SCREEN)
};

private static final String[] sLabels = {
        "Clear"."Src"."Dst"."SrcOver"."DstOver"."SrcIn"."DstIn"."SrcOut"."DstOut"."SrcATop"."DstATop"."Xor"."Darken"."Lighten"."Multiply"."Screen"
};

public MyView(Context context) {
    super(context);
    init(null.0);
}

public MyView(Context context, AttributeSet attrs) {
    super(context, attires
    init(attrs, 0);
}

public MyView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(attrs, defStyle);
}

private void init(AttributeSet attrs, int defStyle) {
    if(Build.VERSION.SDK_INT >= 11){
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setTextSize(mTextSize);
    mPaint.setTextAlign(Paint.Align.CENTER);
    mPaint.setStrokeWidth(2);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // Set the background color
    canvas.drawARGB(255.139.197.186);

    int canvasWidth = canvas.getWidth();
    int canvasHeight = canvas.getHeight();

    for(int row = 0; row < 4; row++){
        for(int column = 0; column < 4; column++){
            canvas.save();
            // Create a new layer
            int layer = canvas.saveLayer(0.0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);
            mPaint.setXfermode(null);
            int index = row * 4 + column;
            float translateX = (mItemSize + mItemHorizontalOffset) * column;
            float translateY = (mItemSize + mItemVerticalOffset) * row;
            canvas.translate(translateX, translateY);
            / / draw text
            String text = sLabels[index];
            mPaint.setColor(Color.BLACK);
            float textXOffset = mItemSize / 2;
            float textYOffset = mTextSize + (mItemVerticalOffset - mTextSize) / 2;
            canvas.drawText(text, textXOffset, textYOffset, mPaint);
            canvas.translate(0, mItemVerticalOffset);
            / / picture frame
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setColor(0xff000000);
            canvas.drawRect(2.2, mItemSize - 2, mItemSize - 2, mPaint);
            mPaint.setStyle(Paint.Style.FILL);
            / / draw circles
            mPaint.setColor(mCircleColor);
            float left = mCircleRadius + 3;
            float top = mCircleRadius + 3;
            canvas.drawCircle(left, top, mCircleRadius, mPaint);
            mPaint.setXfermode(sModes[index]);
            / / draw a rectangle
            mPaint.setColor(mRectColor);
            float rectRight = mCircleRadius + mRectSize;
            float rectBottom = mCircleRadius + mRectSize;
            canvas.drawRect(left, top, rectRight, rectBottom, mPaint);
            mPaint.setXfermode(null);
            //canvas.restore();canvas.restoreToCount(layer); }}}@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, old);)
    mItemSize = w / 4.5 f;
    mItemHorizontalOffset = mItemSize / 6;
    mItemVerticalOffset = mItemSize * 0.426 f;
    mCircleRadius = mItemSize / 3;
    mRectSize = mItemSize * 0.6 f; }}Copy the code

No saveLayer effect

SaveLayer effect

We can see that this code is actually the xfermode demo code in our previous article. In this code, I just used saveLayer to operate. Through the above two results, one is that I added saveLayer, the other is not added. So it can be clearly seen from the middle gate that the pixel output effect of XferMode will directly empty the outer background color when it is not added, but not after it is added. We can clearly see that if we use Layer we are actually creating a new layer on top of the current canvas and when we call Restore or restoreToCount our drawing will be updated to the current layer

So at this point let’s analyze saveLayer’s parameters in detail

canvas.saveLayer(0.0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);
/**
 * Helper version of saveLayer() that takes 4 values rather than a RectF.
 *
 * @deprecated Use {@link #saveLayer(float, float, float, float, Paint)} instead.
 */
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
        @Saveflags int saveFlags) {
    returnnSaveLayer(mNativeCanvasWrapper, left, top, right, bottom, paint ! =null ? paint.getNativeInstance() : 0,
            saveFlags);
}
Copy the code

Appeal way annotation, as well as the code we clearly know that the four parameters, for the up and down or so four points form a layer, Paint brush can also be inherited, and the last parameter is the preservation of our current form, a total of 6 kinds of below, the six models in fact is to tell canvas current save those information

  • MATRIX_SAVE_FLAG: Only saves layers in the Matrix matrix save, saveLayer
  • CLIP_SAVE_FLAG: Saves only size information save, saveLayer
  • HAS_ALPHA_LAYER_SAVE_FLAG: indicates that this layer has transparency, which conflicts with the following flags
  • FULL_COLOR_LAYER_SAVE_FLAG: Retain the color of this layer completely (when merging with the previous layer, clear the overlap area of the previous layer and retain the color of this layer)
  • CLIP_TO_LAYER_SAVE_ : When creating a layer, the canvas (all layers) will be clipped to the range specified by this parameter. If this flag is omitted, the layer will be too expensive (actually the layer is not clipped, it will be the same size as the original layer).
  • ALL_SAVE_FLAG: Save all information save, saveLayer

From the source CODE, I found that several other modes have been removed from the advanced version and only one has been retained. Is our door all_save_flag

  / * *@hide* /
@IntDef(flag = true, value = { ALL_SAVE_FLAG })
@Retention(RetentionPolicy.SOURCE)
public @interface Saveflags {}
Copy the code

So let’s test that out at this point

 public class MyView3 extends View {

public MyView3(Context context) {
    super(context);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    RectF rectF = new RectF(0.0.400.500);
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(10);
    paint.setColor(Color.GREEN);

    canvas.drawRect(rectF, paint);
    canvas.translate(50.50);

    canvas.saveLayer(0.0,canvas.getWidth(),canvas.getHeight(),null,Canvas.ALL_SAVE_FLAG);
    //canvas.save();
    canvas.drawColor(Color.BLUE);// saveLayer creates a new layer by drawColor.
                                // The Demo can verify the new transparent layer by combining Lsn5's 16 Xfermode overlay forms
    paint.setColor(Color.YELLOW);
    canvas.drawRect(rectF,paint);
    //canvas.restore();
    canvas.restore();

    RectF rectF1 = new RectF(10.10.300.400);
    paint.setColor(Color.RED);
    canvas.drawRect(rectF1,paint);

}
Copy the code

Added saveLayer

Don’t add saveLayer

So this code also validates the theory that we appealed, that after saveLayer, the background color is drawn on another layer and there’s a blank section in front of it, and it also leads to an interesting conclusion that it looks like, The pan operation is also inherited, in fact the conclusion here is that saveLayer will carry over some of the previous Canvas state operations. This is done by setting the last parameter before to ALL_SAVE_FLAG. When creating a new layer, he left all the current information state intact.

conclusion

Canvas involves two coordinate systems: Canvas’s own coordinate system and drawing coordinate system

  • The coordinate system of Canvas: it is located in the upper left corner of the View. To the right of the coordinate origin is the positive X-axis, and to the lower is the positive Y-axis. There is only one and only one, and the only one is constant
  • Plotting coordinate system: It is not the only constant, it is related to the Matrix of Canvas. When the Matrix is changed, the drawing coordinate system will be changed accordingly, and this process is irreversible (save and restore methods are used to save and restore the change operation). Matrix is changed by setting Translate, Rotate, Scale and Skew

Canvas state preservation – state stack, Layer stack

  • State stack — Save and restore methods to save and restore Matrix transformation operations and Clip clipping, or restoretoCount can be directly restored to the saved state of the corresponding stack
  • Layer stack – saveLayer creates a transparent Layer (off-screen Bitmap- off-screen buffer) and carries over some of saveLayer’s previous Canvas operations. Subsequent drawing operations are performed on top of the new layer and updated to the corresponding layer and canvas when we call Restore or restoreToCount

Three things to watch ❤️

If you find this article helpful, I’d like to invite you to do three small favors for me:

  1. Like, forward, have your “like and comment”, is the motivation of my creation.
  2. Follow the public account “Xiaoxinchat Android” and share original knowledge from time to time
  3. Also look forward to the follow-up article ing🚀