Rough Book

random musings of just another computer nerd

Implementing pinch-zoom and pan/drag in an Android view on the canvas

As far as what I’ve covered so far, here is the code its entirety; I hope you find it useful:

public class ZoomView extends View {

    //These two constants specify the minimum and maximum zoom
    private static float MIN_ZOOM = 1f;
    private static float MAX_ZOOM = 5f;

    private float scaleFactor = 1.f;
    private ScaleGestureDetector detector;

    //These constants specify the mode that we're in
    private static int NONE = 0;
    private static int DRAG = 1;
    private static int ZOOM = 2;

    private int mode;

    //These two variables keep track of the X and Y coordinate of the finger when it first
    //touches the screen
    private float startX = 0f;
    private float startY = 0f;

    //These two variables keep track of the amount we need to translate the canvas along the X
    //and the Y coordinate
    private float translateX = 0f;
    private float translateY = 0f;

    //These two variables keep track of the amount we translated the X and Y coordinates, the last time we
    //panned.
    private float previousTranslateX = 0f;
    private float previousTranslateY = 0f;    

    public ZoomView(Context context) {
        super(context);
        detector = new ScaleGestureDetector(getContext(), new ScaleListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

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

            case MotionEvent.ACTION_DOWN:
                mode = DRAG;

                //We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
                //amount for each coordinates This works even when we are translating the first time because the initial
                //values for these two variables is zero.
                startX = event.getX() - previousTranslateX;
                startY = event.getY() - previousTranslateY;
                break;

            case MotionEvent.ACTION_MOVE:
                translateX = event.getX() - startX;
                translateY = event.getY() - startY;

                //We cannot use startX and startY directly because we have adjusted their values using the previous translation values.
                //This is why we need to add those values to startX and startY so that we can get the actual coordinates of the finger.
                double distance = Math.sqrt(Math.pow(event.getX() - (startX + previousTranslateX), 2) +
                                            Math.pow(event.getY() - (startY + previousTranslateY), 2)
                                           );

                if(distance > 0) {
                   dragged = true;
                }               

                break;

            case MotionEvent.ACTION_POINTER_DOWN:
                mode = ZOOM;
                break;

            case MotionEvent.ACTION_UP:
                mode = NONE;
                dragged = false;

                //All fingers went up, so let's save the value of translateX and translateY into previousTranslateX and
                //previousTranslate
                previousTranslateX = translateX;
                previousTranslateY = translateY;
                break;

            case MotionEvent.ACTION_POINTER_UP:
                mode = DRAG;

                //This is not strictly necessary; we save the value of translateX and translateY into previousTranslateX
                //and previousTranslateY when the second finger goes up
                previousTranslateX = translateX;
                previousTranslateY = translateY;
                break;
        }

        detector.onTouchEvent(event);

        //We redraw the canvas only in the following cases:
        //
        // o The mode is ZOOM
        //        OR
        // o The mode is DRAG and the scale factor is not equal to 1 (meaning we have zoomed) and dragged is
        //   set to true (meaning the finger has actually moved)
        if ((mode == DRAG && scaleFactor != 1f && dragged) || mode == ZOOM) {
            invalidate();
        }

        return true;
    }

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

        canvas.save();

        //We're going to scale the X and Y coordinates by the same amount
        canvas.scale(scaleFactor, scaleFactor);

        //If translateX times -1 is lesser than zero, let's set it to zero. This takes care of the left bound
        if((translateX * -1) < 0) {
           translateX = 0;
        }

        //This is where we take care of the right bound. We compare translateX times -1 to (scaleFactor - 1) * displayWidth.
        //If translateX is greater than that value, then we know that we've gone over the bound. So we set the value of
        //translateX to (1 - scaleFactor) times the display width. Notice that the terms are interchanged; it's the same
        //as doing -1 * (scaleFactor - 1) * displayWidth
        else if((translateX * -1) > (scaleFactor - 1) * displayWidth) {
           translateX = (1 - scaleFactor) * displayWidth;
        }

        if(translateY * -1 < 0) {
           translateY = 0;
        }

        //We do the exact same thing for the bottom bound, except in this case we use the height of the display
        else if((translateY * -1) > (scaleFactor - 1) * displayHeight) {
           translateY = (1 - scaleFactor) * displayHeight;
        }

        //We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
        //because the translation amount also gets scaled according to how much we've zoomed into the canvas.
        canvas.translate(translateX / scaleFactor, translateY / scaleFactor);

        /* The rest of your canvas-drawing code */
        canvas.restore();
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            scaleFactor *= detector.getScaleFactor();
            scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
            return true;
        }
    }
}

Popularity: 28% [?]

Pages: 1 2 3 4 5 6 7 8

December 4, 2011 - Posted by | Android, Java, Operating Systems, Programming and Development, Software | , , , , , , , , , , ,

17 Comments »

  1. Thank you for the great tutorial, you helped me greatly figuring out the zooming part!
    I do have one problem though, I cant seem to get the panning to work. I believe the canvas is translating but line that I drew on the canvas does not seem to be. I think my mistake is actually the place where im drawing it, but I cant be sure. I was hoping you could help me out with this problem I have, thank you!

    This is my onDraw portion of the code.

    public void onDraw(Canvas canvas)
    {
    super.onDraw(canvas);
    canvas.save();
    canvas.scale(scaleFactor, scaleFactor);

    // My canvas code…
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); // Creating a paint object
    p.setColor(Color.CYAN); // Sets the color to red
    canvas.drawPaint(p);
    p.setColor(Color.RED); // Sets the color to red
    p.setStrokeWidth(10);
    canvas.drawLine(100, 100, 120, 120, p); // Drawing the lines
    // My canvas code…

    canvas.save();

    //We’re going to scale the X and Y coordinates by the same amount
    canvas.scale(scaleFactor, scaleFactor);

    //We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
    //because the translation amount also gets scaled according to how much we’ve zoomed into the canvas.
    canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
    canvas.restore();
    }

    ReplyReply

    Comment by Dan | January 1, 2012

  2. I figured it out already, I put the draw codes in the wrong area, it was drawing in the wrong place, I put it after the canvas translate and it worked already, thanks for the tutorial vivin !

    ReplyReply

    Comment by Dan | January 3, 2012

  3. Your tutorial is easy to follow and helped me a lot in implementing a nice zoom-and-drag-feature for my drawing application.
    Thanks a lot for your effort, I really appreciate your decision to share your experience with the world.

    ReplyReply

    Comment by Roland | January 23, 2012

  4. @Dan: @Roland:

    Glad that you guys found the tutorial helpful!

    ReplyReply

    Comment by vivin | January 23, 2012

  5. Hi thanks a lot for this tutorial, i’m 14 and starting to try and develop for android and this has helped me a lot to better understand how i can put this into effect on an app of my own.

    ReplyReply

    Comment by BiggsyStudios | February 16, 2012

  6. @BiggsyStudios:

    Glad this was of help to you! Good luck with your coding!

    ReplyReply

    Comment by vivin | February 17, 2012

  7. Thank you for this! But where was displayWidth and displayHeight initialized?

    ReplyReply

    Comment by CP | February 22, 2012

  8. In invalidate()a function that is already predefined within java/eclipse?

    ReplyReply

    Comment by Shannon | February 29, 2012

  9. Added the following code to get it to compile:

    private boolean dragged = false;
    private float displayWidth;
    private float displayHeight;

    And inside the constructor:
    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = wm.getDefaultDisplay();

    displayWidth = display.getWidth();
    displayHeight = display.getHeight();

    This code is not tested yet :)

    ReplyReply

    Comment by Richard | March 11, 2012

  10. @Richard: Good catch! Thanks, I’ll update the post as soon as I can!

    ReplyReply

    Comment by vivin | March 12, 2012

  11. Hi guy!
    One question: how to use this class?
    I need to show in an Activity an image bigger than the screen, and i would like to give to the user the pan and zoom function.
    To add an image usually I use ImageView, with your class what should i do?
    Thanks!

    ReplyReply

    Comment by Giovanni | March 20, 2012

  12. Hi,
    very informative tut i m using ur code but one prob is thr still i can drag image towards left top corner infinitely.

    ReplyReply

    Comment by cv | March 23, 2012

  13. Hi,
    ssorry to bug u guys again but how to keep the image in centre????????

    ReplyReply

    Comment by cv | March 23, 2012

  14. I have the same problem as CV. When zooming it always goes to 0,0. If I change canvas.scale(scaleFactor, scaleFactor); to canvas.scale(scaleFactor, scaleFactor, detector.getx(), detector.gety()); it will zoom to the correct coordinates, but the scrolling/panning is very messed up and trying to compensate for that diff in the left, right, top, bottom calculations doesn’t seem to be working. Granted my math could be wrong. Any ideas?

    ReplyReply

    Comment by so | March 29, 2012

  15. @cv: That seems strange. Perhaps you aren’t checking the top-left boundary?

    @so, @cv:

    When I zoom it seems to zoom fine (i.e., it doesn’t zoom towards 0,0). How does the scrolling/panning get messed up when you use detector.getX() and detector.getY()?

    ReplyReply

    Comment by vivin | March 30, 2012

  16. great article.
    I have a problem, how to zoom at finger click position? thanks

    ReplyReply

    Comment by rain@morning | April 19, 2012

  17. Hi,
    Very nice tutorial.It is very help full and easy to understand. thanks a lot.
    I have one issue it works fine but speed is very slow. how does solve it.
    please help me

    ReplyReply

    Comment by Latha | April 21, 2012


Leave a Comment

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

 
All original content on these pages is fingerprinted and certified by Digiprove