Canvas Pinch-Zoom to Point Within Bounds

10,901

Here is the code I use to implement pinch zoom in an ImageView using ScaleGestureDetector. With little or no modification you should be able to use it too, since you can use transformation marices too, to draw on a Canvas.

@Override
public boolean onScale(ScaleGestureDetector detector) {
    float mScaleFactor = (float) Math.min(
        Math.max(.8f, detector.getScaleFactor()), 1.2);
    float origScale = saveScale;
    saveScale *= mScaleFactor;
    if (saveScale > maxScale) {
        saveScale = maxScale;
        mScaleFactor = maxScale / origScale;
    } else if (saveScale < minScale) {
        saveScale = minScale;
        mScaleFactor = minScale / origScale;
    }
    right = width * saveScale - width
            - (2 * redundantXSpace * saveScale);
    bottom = height * saveScale - height
            - (2 * redundantYSpace * saveScale);
    if (origWidth * saveScale <= width
            || origHeight * saveScale <= height) {
        matrix.postScale(mScaleFactor, mScaleFactor, width / 2, height / 2);
        if (mScaleFactor < 1) {
            matrix.getValues(m);
            float x = m[Matrix.MTRANS_X];
            float y = m[Matrix.MTRANS_Y];
            if (mScaleFactor < 1) {
                if (Math.round(origWidth * saveScale) < width) {
                    if (y < -bottom)
                        matrix.postTranslate(0, -(y + bottom));
                    else if (y > 0)
                        matrix.postTranslate(0, -y);
                } else {
                    if (x < -right)
                        matrix.postTranslate(-(x + right), 0);
                    else if (x > 0)
                        matrix.postTranslate(-x, 0);
                }
            }
        }
    } else {
        matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY());
        matrix.getValues(m);
        float x = m[Matrix.MTRANS_X];
        float y = m[Matrix.MTRANS_Y];
        if (mScaleFactor < 1) {
            if (x < -right)
                matrix.postTranslate(-(x + right), 0);
            else if (x > 0)
                matrix.postTranslate(-x, 0);
            if (y < -bottom)
                matrix.postTranslate(0, -(y + bottom));
            else if (y > 0)
                matrix.postTranslate(0, -y);
        }
    }
    return true;
}

In my case, I computed the neccesary values in the onMeasure() method of the View, you might want to do this somewhere else in your SurfaceView

width = MeasureSpec.getSize(widthMeasureSpec); // Change this according to your screen size
height = MeasureSpec.getSize(heightMeasureSpec); // Change this according to your screen size

// Fit to screen.
float scale;
float scaleX = (float) width / (float) bmWidth;
float scaleY = (float) height / (float) bmHeight;
scale = Math.min(scaleX, scaleY);
matrix.setScale(scale, scale);
setImageMatrix(matrix);
saveScale = 1f;
scaleMappingRatio = saveScale / scale;

// Center the image
redundantYSpace = (float) height - (scale * (float) bmHeight);
redundantXSpace = (float) width - (scale * (float) bmWidth);
redundantYSpace /= (float) 2;
redundantXSpace /= (float) 2;

matrix.postTranslate(redundantXSpace, redundantYSpace);

origWidth = width - 2 * redundantXSpace;
origHeight = height - 2 * redundantYSpace;
right = width * saveScale - width - (2 * redundantXSpace * saveScale);
bottom = height * saveScale - height - (2 * redundantYSpace * saveScale);
setImageMatrix(matrix);

A little explanation:

saveScale is the current scale ratio of the Bitmap

mScaleFactor is the factor you have to multiply your scale ratio with.

maxScale and minScale can be constant values.

width and height are the dimensions of the screen.

redundantXSpace and redundantYSpace are the empty between the image borders and screen borders since the image is centered when in it is smaller then the screen

origHeight and origWidth are the sizes of the bitmap

matrix is the current transformation matrix used to draw the bitmap

The trick is, that when I first scaled and centered the image on initialization, I picked that scale ratio to be 1 and with scaleMappingRatio I mapped the actual scale values of the image relative to that.

Share:
10,901

Related videos on Youtube

Cat
Author by

Cat

I write code, and I love artwork, writing, and languages. Make sure you're accepting answers to questions you ask! It encourages us to help give you the best answers possible and helps other people find answers to questions you ask.

Updated on June 04, 2022

Comments

  • Cat
    Cat almost 2 years

    I've been stuck on this problem for eight hours, so I figured it was time to get some help.

    Before I begin my problem, I'll just let it be known that I've been through this site and Google, and none of the answers I've found have helped. (This is one, another, and another.)

    Here's the deal: I have a class that extends SurfaceView (let's call it MySurface) and overrides many methods in it. Normally, it draws several squares and text boxes, which is all fine. As soon as a user starts touching, it converts to a Bitmap, then draws each frame that until the user releases.

    Here's the rub: I want to implement such a functionality that the user can place two fingers on the screen, pinch to zoom, and also pan around (but ONLY with two fingers down).

    I found a few implementations of pinch-to-zoom and adapted them to my Canvas object in MySurface via the following:

    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        canvas.save();
    
        canvas.scale(mScaleVector.z, mScaleVector.z); // This is the scale factor as seen below
        canvas.translate(mScaleVector.x, mScaleVector.y); // These are offset values from 0,0, both working fine
    
        // Start draw code
    
        // ...
    
        // End draw code
    
        canvas.restore();
    }
    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float factor = detector.getScaleFactor();
            if (Math.abs(factor - 1.0f) >= 0.0075f) {
                mScaleVector.z *= factor;
                mScaleVector.z = Math.max(MIN_ZOOM, Math.min(mScaleVector.z, MAX_ZOOM));
            }
    
            // ...
    
            invalidate();
    
            return true;
        }
    }
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction() & MotionEvent.ACTION_MASK;
        int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
        if (event.getPointerCount() == 2) {
            if (action == MotionEvent.ACTION_POINTER_DOWN && pointerIndex == 1) {
                // The various pivot coordinate codes would belong here
            }
        }
    
        detector.onTouchEvent(event); // Calls the Scale Gesture Detector
        return true;
    }
    

    While both elements work fine--the scrolling back and forth and the pinch-to-zoom--there is one large problem. The pinch-to-zoom, when used, zooms into the point 0,0, instead of zooming into the finger point.

    I've tried a lot of ways to fix this:

    • Using canvas.scale(mScaleVector.z, mScaleVector.z, mScaleVector.x, mScaleVector.y);; obviously, this produces unwanted results as the mScaleVector x and y values are 0-offsets.
    • Managing a "pivot" coordinate that uses the same offset as the translate() method, but this produces either the same 0,0 issue, or jumping around when the view is touched.
    • Numerous other things... I've done a lot with the aforementioned pivot coordinate, trying to base its location on the user's first touch, and moving it relative to that touch each successive gesture.

    Additionally, this canvas must be bounded, so the user cannot scroll forever. However, when I use the .scale(sx, sy, px, py) method, it pushes things beyond any bounds I set in .translate().

    I'm... pretty much open to anything at this point. I know this functionality can be added, as it is seen in the Android 4.0 gallery (when viewing a single image). I've tried to track down the source code that handles this, to no avail.

  • Cat
    Cat almost 12 years
    Oh man, after reviewing and thoroughly testing aspects of your code, I discovered those matrix transformations were the key! I'd buy you a beer if I could! A+, good sir.
  • Adam Monos
    Adam Monos almost 12 years
    You are welcome, I spent a lot of time figuring out the right way too, when I started writing the code above, I thought it would worth sharing.
  • user2288580
    user2288580 over 6 years
    Here is a library that wil do this. stackoverflow.com/questions/6578320/…