preface

In front of the “Android graffiti sketchpad principle detailed explanation – from elementary to advanced (A)”, about the graffiti principle of the elementary and intermediate applications, now explain advanced applications. If you have not read the previous article, I suggest you go to have a look.

To prepare

Advanced graffiti involves image operation, including zooming and zooming, graffiti, etc., which involves matrix transformation. For the knowledge of Matrix transformation, please check my other article “Talk about Matrix transformation – Matrix”. According to the introduction of the paper, the next use of transformation coordinate system spatial imagination to understand the matrix transformation involved in graffiti.

Senior graffiti

Advanced doodle supports doodling of pictures and can be moved to zoom in and out of pictures. Here’s the idea:

  1. Create a custom View: AdvancedDoodleView, passed in a Bitmap image object when created externally.
  2. Initialize in the onSizeChanged() callback when the View is resized, and calculate the parameters required for the image to be centered, such as the image zoom multiple and offset values.
  3. Define the PathItem class that encapsulates the scribbled track, including information such as Path and offset values.
class PathItem {
    Path mPath = new Path(); // Doodle the track
    float mX, mY; // Track offset value
}
Copy the code
  1. To determine whether a doodle is clicked, Path provides the interface computeBounds() to calculate the rectangular range of the current graph by determining whether the clicked point is within the rectangular range. Use TouchGestureDetector to identify click and swipe gestures. (TouchGestureDetector in my other project Androids requires importing dependencies to use)

  2. In the sliding process, it is necessary to judge whether there is currently selected graffiti. If there is, the graffiti will be moved and the offset value will be recorded in the PathItem. If not, draw new graffiti tracks.

  3. Listen for two-finger zooming gestures and calculate the multiples of zooming.

(Touch coordinates involved in 4-6 should be converted into coordinates in the corresponding picture coordinate system, which will be explained in detail later)

  1. In the onDraw method of AdvancedDoodleView, the picture is drawn according to the zoom and offset value of the picture; Move the canvas based on the offset value before drawing each PathItem.

Coordinate mapping

Choose canvas and picture to share the same coordinate system. After understanding the position information of picture, the last thing to deal with is the mapping between screen coordinate system and picture (canvas) coordinate system, that is, the sliding track on the screen is projected into the picture.

From the analysis of the above figure, we can get the following mapping:

Picture coordinate x = (screen coordinate x- The offset of the picture on the x axis of the screen coordinate system)/Picture scaling factor Picture coordinate Y = (screen coordinate Y - the offset of the picture on the y axis of the screen coordinate system)/Picture scaling factorCopy the code

(Note that the image is zoomed in on the top left corner)

Corresponding code:

/** * Convert the screen touch coordinate x to the coordinate x */ public finalfloat toX(float touchX) {
    return(touchX - mBitmapTransX) / mBitmapScale; } /** * convert screen touch coordinates y to coordinates y */ public finalfloat toY(float touchY) {
    return (touchY - mBitmapTransY) / mBitmapScale;
}
Copy the code

As you can see, when the screen coordinates are projected onto the image, we have to subtract the offset, because the position of the image is always the same, so we’re offsetting the image, which is actually offsetting the canvas of the View.

The final result is as follows:

The code is as follows:

public class AdvancedDoodleView extends View {

    private final static String TAG = "AdvancedDoodleView";

    private Paint mPaint = new Paint();
    private List<PathItem> mPathList = new ArrayList<>(); // Save a collection of doodle tracks
    private TouchGestureDetector mTouchGestureDetector; // Touch gestures listen
    private float mLastX, mLastY;
    private PathItem mCurrentPathItem; // The current doodle trajectory
    private PathItem mSelectedPathItem; // The selected doodle track

    private Bitmap mBitmap;
    private float mBitmapTransX, mBitmapTransY, mBitmapScale = 1;

    public AdvancedDoodleView(Context context, Bitmap bitmap) {
        super(context);
        mBitmap = bitmap;

        // Set the brush
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(20);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        // Gestures are handled by gesture recognizers
        mTouchGestureDetector = new TouchGestureDetector(getContext(), new TouchGestureDetector.OnTouchGestureListener() {

            RectF mRectF = new RectF();

            // Zoom gestures are related
            Float mLastFocusX;
            Float mLastFocusY;
            float mTouchCentreX, mTouchCentreY;

            @Override
            public boolean onScaleBegin(ScaleGestureDetectorApi27 detector) {
                Log.d(TAG, "onScaleBegin: ");
                mLastFocusX = null;
                mLastFocusY = null;
                return true;
            }

            @Override
            public void onScaleEnd(ScaleGestureDetectorApi27 detector) {
                Log.d(TAG, "onScaleEnd: ");
            }

            @Override
            public boolean onScale(ScaleGestureDetectorApi27 detector) { // Double finger zooming
                Log.d(TAG, "onScale: ");
                // Focus on the screen
                mTouchCentreX = detector.getFocusX();
                mTouchCentreY = detector.getFocusY();

                if(mLastFocusX ! =null&& mLastFocusY ! =null) { // Focus changes
                    float dx = mTouchCentreX - mLastFocusX;
                    float dy = mTouchCentreY - mLastFocusY;
                    // Move the image
                    mBitmapTransX = mBitmapTransX + dx;
                    mBitmapTransY = mBitmapTransY + dy;
                }

                // Zoom the image
                mBitmapScale = mBitmapScale * detector.getScaleFactor();
                if (mBitmapScale < 0.1 f) {
                    mBitmapScale = 0.1 f;
                }
                invalidate();

                mLastFocusX = mTouchCentreX;
                mLastFocusY = mTouchCentreY;

                return true;
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) { // Click select
                float x = toX(e.getX()), y = toY(e.getY());
                boolean found = false;
                for (PathItem path : mPathList) { // Draw the graffiti trace
                    path.mPath.computeBounds(mRectF, true); // Calculate the rectangle range of graffiti trace
                    mRectF.offset(path.mX, path.mY); // Add offset
                    if (mRectF.contains(x, y)) { // Check whether the dot is within the rectangle of the doodle track
                        found = true;
                        mSelectedPathItem = path;
                        break; }}if(! found) {// No doodles
                    mSelectedPathItem = null;
                }
                invalidate();
                return true;
            }

            @Override
            public void onScrollBegin(MotionEvent e) { // Slide to start
                Log.d(TAG, "onScrollBegin: ");
                float x = toX(e.getX()), y = toY(e.getY());
                if (mSelectedPathItem == null) {
                    mCurrentPathItem = new PathItem(); // New doodle
                    mPathList.add(mCurrentPathItem); // Add to the collection
                    mCurrentPathItem.mPath.moveTo(x, y);
                }
                mLastX = x;
                mLastY = y;
                invalidate(); / / refresh
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { / / slide
                Log.d(TAG, "onScroll: " + e2.getX() + "" + e2.getY());
                float x = toX(e2.getX()), y = toY(e2.getY());
                if (mSelectedPathItem == null) { // No selected doodle
                    mCurrentPathItem.mPath.quadTo(
                            mLastX,
                            mLastY,
                            (x + mLastX) / 2,
                            (y + mLastY) / 2); // Use bezier curves to make the graffiti trail smoother
                } else { // Move the selected doodle
                    mSelectedPathItem.mX = mSelectedPathItem.mX + x - mLastX;
                    mSelectedPathItem.mY = mSelectedPathItem.mY + y - mLastY;
                }
                mLastX = x;
                mLastY = y;
                invalidate(); / / refresh
                return true;
            }

            @Override
            public void onScrollEnd(MotionEvent e) { // The slide ends
                Log.d(TAG, "onScrollEnd: ");
                float x = toX(e.getX()), y = toY(e.getY());
                if (mSelectedPathItem == null) {
                    mCurrentPathItem.mPath.quadTo(
                            mLastX,
                            mLastY,
                            (x + mLastX) / 2,
                            (y + mLastY) / 2); // Use bezier curves to make the graffiti trail smoother
                    mCurrentPathItem = null; // End of trajectory
                }
                invalidate(); / / refresh}});// Gesture parameter Settings for doodling
        // The spacing of the following two lines should be set to greater than or equal to 1 in the painting scene, otherwise set to 0
        mTouchGestureDetector.setScaleSpanSlop(1); // The minimum distance between the two fingers before the gesture is recognized as the pinch gesture
        mTouchGestureDetector.setScaleMinSpan(1); // The minimum distance between two fingers identified as the pinch gesture during zooming
        mTouchGestureDetector.setIsLongpressEnabled(false);
        mTouchGestureDetector.setIsScrollAfterScaled(false);
    }

    @Override
    protected void onSizeChanged(int width, int height, int oldw, int oldh) { // The size of the view is fixed when the view is drawn
        super.onSizeChanged(width, height, oldw, oldh);
        int w = mBitmap.getWidth();
        int h = mBitmap.getHeight();
        float nw = w * 1f / getWidth();
        float nh = h * 1f / getHeight();
        float centerWidth, centerHeight;
        // 1. Calculate the zoom value to center the image
        if (nw > nh) {
            mBitmapScale = 1 / nw;
            centerWidth = getWidth();
            centerHeight = (int) (h * mBitmapScale);
        } else {
            mBitmapScale = 1 / nh;
            centerWidth = (int) (w * mBitmapScale);
            centerHeight = getHeight();
        }
        // 2. Calculate the offset to center the image
        mBitmapTransX = (getWidth() - centerWidth) / 2f;
        mBitmapTransY = (getHeight() - centerHeight) / 2f;
        invalidate();
    }

    /** * Convert the screen touch coordinate x to the coordinate */ in the image
    public final float toX(float touchX) {
        return (touchX - mBitmapTransX) / mBitmapScale;
    }

    /** * Convert the screen touch coordinate y to the coordinate in the image */
    public final float toY(float touchY) {
        return (touchY - mBitmapTransY) / mBitmapScale;
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean consumed = mTouchGestureDetector.onTouchEvent(event); // Gestures are handled by gesture recognizers
        if(! consumed) {return super.dispatchTouchEvent(event);
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // Canvas and image share the same coordinate system, only need to deal with the screen coordinate system to the image (canvas) coordinate system mapping (toX toY)
        canvas.translate(mBitmapTransX, mBitmapTransY);
        canvas.scale(mBitmapScale, mBitmapScale);

        // Draw a picture
        canvas.drawBitmap(mBitmap, 0.0.null);

        for (PathItem path : mPathList) { // Draw the graffiti trace
            canvas.save();
            canvas.translate(path.mX, path.mY); // According to the offset value of the graffiti track, offset the canvas so that it is painted in the corresponding position
            if (mSelectedPathItem == path) {
                mPaint.setColor(Color.YELLOW); // The dots are yellow
            } else {
                mPaint.setColor(Color.RED); // Others are red} canvas.drawPath(path.mPath, mPaint); canvas.restore(); }}/** * encapsulates the doodle track object */
    private static class PathItem {
        Path mPath = new Path(); // Doodle the track
        float mX, mY; // Track offset value}}Copy the code

Add to the parent container with the following code:

// Advanced doodle ViewGroup advancedContainer = findViewById(r.i.C.ontainer_advanced_doodle); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thelittleprince2); AdvancedDoodleView advancedDoodleView = new AdvancedDoodleView(this, bitmap); advancedContainer.addView(advancedDoodleView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));Copy the code

subsequent

This is the core of doodling, and I hope you can understand it. As for how to add text pictures or other similar graffiti in the picture, it is the same as the PathItem defined in the code represents the graffiti track. We can encapsulate the new graffiti type with a new class, save the relevant information, and finally draw it on the canvas.

This is the end of the doodle principle series! Thank you for your attention and support! Thanks!!

The above code in my open source framework Demo >>>>Doodle principle tutorial code.

Finally, please support my project >>>> open source project Doodle! A powerful, customizable and extensible doodle framework.