This article is the original article, if you need to reprint, please indicate the source, thank you!

In the recent project, the function of whiteboard doodle was added. The requirement is to draw smooth curves when you slide your finger on the screen, and you can switch colors, select pen width, switch on and off brushes, undo strokes and empty the drawing board. Many online sketchboards are implemented using View. I personally feel that the processing of Canvas by View is not as convenient as SurfaceView, and the performance of SurfaceView is better than that of View under the condition of frequent drawing. So I chose to inherit SurfaceView to implement the artboard function.

Let’s take a look at the effect

Related to knowledge

  • Use of the View onTouchEvent method
  • Basic use of SurfaceView
  • Basic use of Path

If you want to understand the principle of SurfaceView, “double buffering, drawing mechanism balabala… , to see the principle of the great God written analysis ~

Implementation approach

1. Override the onTouchEvent method

@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); float x = event.getX(); float y = event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: mPrevX = x; mPrevY = y; mPath = new Path(); mPath.moveTo(x, y); // Set the starting coordinate of Path to break when the finger presses the screen; case MotionEvent.ACTION_MOVE: Canvas canvas = mSurfaceHolder.lockCanvas(); restorePreAction(canvas); QuadTo (mPrevX, mPrevY, (x + mPrevX) / 2, (y + mPrevY) / 2); // Draw a Bezier curve, i.e., a smooth curve. If you use lineTo here, the curve will be prevx = x; mPrevY = y; canvas.drawPath(mPath, mPaint); mSurfaceHolder.unlockCanvasAndPost(canvas); break; case MotionEvent.ACTION_UP: break; } return true; }Copy the code

There is one method in this code, restorePreAction, which is given later in this code. Canvas can only draw the content once every time and will not save it for us. If we use View to realize the drawing board, we also need to cache the content drawn before with Bitmap, while using SurfaceView simplifies our processing of canvas.

Then let’s talk briefly about the mSurfaceHolder. First mSurfaceHolder is during initialization through getHolder () method for instance, then you need to call mSurfaceHolder. AddCallback (this) method, add listening to SurfaceHolder, specific monitoring content is as follows

Public void surfaceCreated(SurfaceHolder) {public void surfaceCreated(SurfaceHolder) {public void surfaceCreated(SurfaceHolder) SurfaceChanged (SurfaceHolder holder, int format, int width, int height) { } @override public void surfaceDestroyed(SurfaceHolder holder) {// Called when SurfaceView is destroyed, For example, if you hit the home button and your app goes into the background, you call this method}Copy the code

And then a little bit about the SurfaceView double buffering mechanism, which basically means that the SurfaceView manages two canvases, one is the front canvas, which is the one we see in front, and the other is the back canvas, which is the one that we buffer, and everything that we draw is going to be on the back, After drawing, we call unlockCanvasAndPost(canvas), which will change the back canvas to front, so that the newly drawn content will be displayed in front. Then the front will change to back and continue waiting for the lockCanvas call.

2. Optimize the onTouchEvent method

Now consider a problem: “In onTouchEvent, we directly operate on Path, which restricts the graph drawn. If the requirement is extended to draw circles and squares in the future, the code will need to be directly modified, which violates the object-oriented design principle.” Then how to solve the problem?

Solution is abstract, regardless of the painting circle or line drawing, graphics are in the picture, a step deep thinking, in dealing with the actual onTouchEvent is our finger movements, so we only need to use an abstract action to deal with the coordinate is ok, as for specific what to draw, how to deal with the coordinate can to subclass. So I abstracted a class called DoodleAction to handle coordinates. The following code

public abstract class DoodleAction { protected int color; protected float strokeWidth; DoodleAction() { } public int getColor() { return color; } public void setColor(int color) { this.color = color; } public float getStrokeWidth() { return strokeWidth; } public void setStrokeWidth(float strokeWidth) { this.strokeWidth = strokeWidth; } @Override public String toString() { return "DoodleAction{" + ", color=" + color + ", strokeWidth=" + strokeWidth + '}'; } /** * draw the current action ** @param canvas new canvas */ public void draw(canvas); ** @param x * @param y */ public void move(float x, float y); }Copy the code

There are two core abstract methods in this class:

  • Draw method: Draw different graphics from the passed canvas
  • Move method: used to record the finger across the coordinate, and corresponding processing

The optimized code looks like this

@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); float x = event.getX(); float y = event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: if (! mIsDoodleEnabled) return false; // If the current setting does not draw directly return false does not consume this event mDownX = x; mDownY = y; setCurDoodleAction(x, y); break; case MotionEvent.ACTION_MOVE: Canvas canvas = mSurfaceHolder.lockCanvas(); restorePreAction(canvas); McUraction.move (x, y); mCurAction.draw(canvas); / / draw the current Action mSurfaceHolder. UnlockCanvasAndPost (canvas); break; case MotionEvent.ACTION_UP: If (x == mDownX &&y == mDownY) {// Currently ACTION_DOWN -> ACTION_UP does nothing, } else {mdoodleActionList.add (mCurAction);} else {mdoodleActionList.add (mCurAction); } mCurAction = null; // The object should be set to null break after each action; } return true; }Copy the code

The setCurDoodleAction method is first executed in ACTION_DOWN

Private void setCurDoodleAction(float startX, float startX, float startX, float startX, float startX, float startX, float startX, float startX, float startX, float startX, float startX, float startX, float float float startY) { switch (mType) { case Path: mCurAction = new DoodlePath(startX, startY); break; Case Oval: //TODO add Oval break; } mCurAction.setColor(mCurColor); mCurAction.setStrokeWidth(mCurStrokeWidth); }Copy the code

This method initializes the action that we need, and mType is an enum type that I defined for students to expand.

Then execute the Move Draw method in ACTION_MOVE. Here we use abstract types to interact with the SurfaceView, making it easier to maintain and extend functionality later.

Finally, a special processing is made in ACTION_UP, which is ACTION_DOWN – > ACTION_UP immediately when the finger touches the screen. This operation is easy to misoperate when it is really used, the specific reason is not explained here. If you need to handle this function, you can add a callback here. Finally, mDoodleActionList is an ArrayList that manages each action, which I’ll show you in a second.

DoodlePath is a class that inherits from DoodleAction. The code is relatively simple and is posted directly

/** * extends DoodleAction {private Path mPath; private float mPrevX; private float mPrevY; private Paint mPaint; DoodlePath() {this(0, 0, 0, 10.0f); } DoodlePath(float startX, float startY) {this(startX, startY, 0, 10.0f); } DoodlePath(float startX, float startY, int color, float strokeWidth) { this.color = color; this.strokeWidth = strokeWidth; mPath = new Path(); mPath.moveTo(startX, startY); mPrevX = startX; mPrevY = startY; initPaint(); } private void initPaint() { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setDither(true); mPaint.setColor(color); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(strokeWidth); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeJoin(Paint.Join.ROUND); } @Override public void setColor(int color) { super.setColor(color); mPaint.setColor(color); } @Override public void setStrokeWidth(float strokeWidth) { super.setStrokeWidth(strokeWidth); mPaint.setStrokeWidth(strokeWidth); } @Override public void draw(Canvas canvas) { if (canvas ! = null) { canvas.drawPath(mPath, mPaint); } } @Override public void move(float x, float y) { mPath.quadTo(mPrevX, mPrevY, (x + mPrevX) / 2, (y + mPrevY) / 2); mPrevX = x; mPrevY = y; } public void moveTo(float startX, float startY) { mPath.moveTo(startX, startY); mPrevX = startX; mPrevY = startY; }}Copy the code

3. Management DoodleAction

In the code above, we add an object to the List for DoodleAction management every time we draw. The restorePreAction method we have not explained before is to draw all the existing actions by iterating through the List.

Private void restorePreAction(canvas canvas) {if (canvas == null) {return; } canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); If (mDoodleActionList! = null && mDoodleActionList.size() > 0) { for (DoodleAction action : mDoodleActionList) { action.draw(canvas); }}}Copy the code

You need to clear the artboard before iterating through the List, otherwise the interface will repeat the previous drawing.

In addition, using List we can easily implement the need to undo and clear the artboard. Now let’s look at these two methods:

public void undoAction() { int size = mDoodleActionList == null? 0 : mDoodleActionList.size(); if (size > 0) { mDoodleActionList.remove(size - 1); Canvas canvas = mSurfaceHolder.lockCanvas(); restorePreAction(canvas); mSurfaceHolder.unlockCanvasAndPost(canvas); }}Copy the code

Undo is simple, just remove the last object in the List and redraw the contents.

It is easier to clear the List and then clear the artboard. The code is as follows

public void cleanWhiteBoard() {
    if (mDoodleActionList != null && mDoodleActionList.size() > 0) {
        mDoodleActionList.clear();
        Canvas canvas = mSurfaceHolder.lockCanvas();
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        mSurfaceHolder.unlockCanvasAndPost(canvas);
    }
}Copy the code

4. Communication of graffiti data

The above introduction can already implement a stand-alone version of the sketchpad, what if you need to encapsulate the doodle data and send it to other terminals over the network? Since the background and front end can be implemented in many ways, I’ll just give you the general idea.

You first need to design an object to hold the graffiti data. The properties of the object may include

  1. PaintColor paintColor
  2. paintStrokeWidth
  3. The coordinate collection pointList
  4. User Id userId

After the object is designed, it can communicate. Here is what the front end does, which is divided into sender and receiver.

  • Sender: Create the transfer object in ACTION_DOWN, initialize the brush information, collect the coordinates in ACTION_MOVE, and finally add a callback in ACTION_UP to pass the object, and then make the network request.

  • Receiver: If the data transfer format is JSON, parse the JSON into an object, then connect the coordinate set in the object via Path, set the brush information, and display it on the SurfaceView

conclusion

This article does not involve the explanation of the principle, just to explain the core idea of my drawing board through SurfaceView, if you want to know more about the principle can refer to the following article oh! “In the history of the finest Path” www.jianshu.com/p/b872b064d… “Old detailed analysis of the SurfaceView” blog.csdn.net/luoshengyan…

If there is something wrong in the article, please let me know in time! Because I am also a beginner, hope everybody great god gives directions a lot!

If you need to see the source code, you can go to my Github Clone, welcome to submit an issue, if you can give a star, I would be very grateful!