Home » Android » Wrong scale position after applying zoom effect on Android canvas

Wrong scale position after applying zoom effect on Android canvas

Posted by: admin May 14, 2020 Leave a comment

Questions:

First of all this is a follow up question originally asked here, Pan, Zoom and Scale a custom View for Canvas drawing in Android

Since there was no answer yet, I finally solved my issue using the android-gesture-detectors

After applying the zoom/scale gesture I found that, the canvas drawing coordinates are still pointing to the old position (before applying the zoom) and not drawing on the exact same touch coordinates. Basically, I can’t get the correct canvas coordinates after scaling or dragging the canvas.

Before zooming,

enter image description here

After zooming out the touch points are drawing on the previous location. I want it to draw on the current touch location,

enter image description here

Sample code,

public class DrawingView extends View {

    private void setupDrawing() {

        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());

        mgd = new MoveGestureDetector(ctx, mgl);
        sgd = new ScaleGestureDetector(ctx, sgl);
        rgd = new RotateGestureDetector(ctx, rgl);

}

class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();
            // Don't let the object get too small or too large.
            mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
            invalidate();
            return true;
        }
    }

    MoveGestureDetector.SimpleOnMoveGestureListener mgl = new MoveGestureDetector.SimpleOnMoveGestureListener() {
        @Override
        public boolean onMove(MoveGestureDetector detector) {
            PointF delta = detector.getFocusDelta();
            matrix.postTranslate(delta.x, delta.y);
            invalidate();
            return true;
        }
    };

    ScaleGestureDetector.SimpleOnScaleGestureListener sgl = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float scale = detector.getScaleFactor();
            matrix.postScale(scale, scale, detector.getFocusX(), detector.getFocusY());
            invalidate();
            return true;
        }
    };

    RotateGestureDetector.SimpleOnRotateGestureListener rgl = new RotateGestureDetector.SimpleOnRotateGestureListener() {
        @Override
        public boolean onRotate(RotateGestureDetector detector) {
            matrix.postRotate(-detector.getRotationDegreesDelta(), detector.getFocusX(), detector.getFocusY());
            invalidate();
            return true;
        }
    };

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        //view given size
        super.onSizeChanged(w, h, oldw, oldh);
        canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        drawCanvas = new Canvas(canvasBitmap);
    }

    private void touch_start(float x, float y) {
        undonePaths.clear();
        drawPath.reset();
        drawPath.moveTo(x, y);
        mX = x;
        mY = y;
    }

    private void touch_move(float x, float y, float x2, float y2) {
        float dx = Math.abs(x - mX);
        float dy = Math.abs(y - mY);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
           /* QUad to curves using a quadratic line (basically an ellipse of some sort).
           LineTo is a straight line. QuadTo will smooth out jaggedies where they turn.
          */
            drawPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
            mX = x;
            mY = y;
        }

    }

    private void touch_up() {

            drawPath.lineTo(mX, mY);
            // commit the path to our offscreen
            drawCanvas.drawPath(drawPath, drawPaint);
            // kill this so we don't double draw
            paths.add(drawPath);
            drawPath = new Path();
            drawPath.reset();
            invalidate();
    }

@Override
    public boolean onTouchEvent(MotionEvent event) {

        if (isZoomable) {
            mgd.onTouchEvent(event);
            sgd.onTouchEvent(event);
            rgd.onTouchEvent(event);
        }

        if (!isTouchable) {
            return super.onTouchEvent(event);
        } else {
            //detect user touch
            float x = event.getX();
            float y = event.getY();

            switch (event.getAction() & MotionEvent.ACTION_MASK) {

                case MotionEvent.ACTION_DOWN:
                    if (!isZoomable) {
                        touch_start(x, y);
                    }
                    invalidate();
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (!isZoomable) {
                        //mPositions.add(new Vector2(x - mBitmapBrushDimensions.x / 2, y - mBitmapBrushDimensions.y / 2));
                        if (isCustomBrush && mBitmapBrushDimensions != null) {
                            mPositions = new Vector2(x - mBitmapBrushDimensions.x / 2, y - mBitmapBrushDimensions.y / 2);
                            touch_move(x, y, x - mBitmapBrushDimensions.x / 2, y - mBitmapBrushDimensions.y / 2);
                        } else {
                            touch_move(x, y, 0, 0);
                        }
                    }
                    invalidate();
                    break;

                case MotionEvent.ACTION_UP:
                    if (!isZoomable) {
                        touch_up();
                    }
                    invalidate();
                    break;
            }
            mScaleDetector.onTouchEvent(event);
            return true;
        }
    }

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

        canvas.setMatrix(matrix);

        for (Path p : paths) {
                canvas.drawPath(p, drawPaint);
                drawPaint.setColor(selectedColor);
                drawPaint.setStrokeWidth(brushSize);
                canvas.drawPath(drawPath, drawPaint);
        }
        canvas.restore();
    }
}

PS: MoveGestureDetector(), ScaleGestureDetector() & RotateGestureDetector() are custom classes inherited from android-gesture-detectors

How to&Answers:

Here’s what I did. Basically, you have to find the difference between the “old” and new points. Skip to the bottom for the important lines…

@Override
public boolean onScale(ScaleGestureDetector detector) {

    scaleFactor *= detector.getScaleFactor();

    float xDiff = initialFocalPoints[0] - currentFocalPoints[0];
    float yDiff = initialFocalPoints[1] - currentFocalPoints[1];

    transformMatrix.setScale(scaleFactor, scaleFactor, 
                                 currentFocalPoints[0], currentFocalPoints[1]);
    transformMatrix.postTranslate(xDiff, yDiff);    
    child.setImageMatrix(transformMatrix);

    return true;
}

@Override
public boolean onScaleBegin(ScaleGestureDetector detector){

    float startX = detector.getFocusX() + getScrollX();
    float startY = detector.getFocusY() + getScrollY();

    initialFocalPoints = new float[]{startX, startY};

    if(transformMatrix.invert(inverseTransformMatrix))
    inverseTransformMatrix.mapPoints(currentFocalPoints, initialFocalPoints);
    return true;
}

The lines that made the difference were the following:

float xDiff = initialFocalPoints[0] - currentFocalPoints[0];
float yDiff = initialFocalPoints[1] - currentFocalPoints[1];
transformMatrix.postTranslate(xDiff, yDiff);

The answer was as simple as figuring out the difference between the two points and translating the imageview everytime the image is scaled.

Answer:

To apply any transformation you have to understand the rules of mathematics. It works both for 2dim and 3 dimensions graphics.
That is, if you work with translation (T), rotation (R), scale (S) matrices to apply any transformation, you have scale object at first (multiply coordinates xyz by this matrix S) then rotate (mult. by R) then shift the object by T.
So, you apply rotation over some point you have to move the object to zero and scale then return to base point.
That is namely in your case, before applying scale, you have to shift (decrease) all coordinates by touch position then apply scale matrix by multiplication then shift by increasing all positions by this touch against.