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

by vivin

We don’t need to make any changes to onDraw(…) because we have appropriately adjust the values of translateX and translateY by taking into account the previous translation values when we set startX and startY. So now we need to solve the problem of indefinite panning. Let’s tackle the easier aspect of that problem first: stopping panning past the top and left edges of the canvas (i.e., where the X and Y coordinates are zero):

@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);


    //I multiplied translateX and translateY by -1 because it made more sense to me to see if they were lesser than 0. If they are lesser
    //than zero, we set their values to 0. Otherwise, we leave them as is. This ensures that we never pan past the top of left edge of the
    //zoomed-in canvas.
    translateX = (translateX * -1) < 0 ? 0 : translateX;
    translateY = (translateY * -1) < 0 ? 0 : translateY;

    //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();
}
&#91;/sourcecode&#93;

Now let's tackle the more difficult aspect of the indefinite-panning problem: panning past the bottom and right edges of the canvas. This part was hard for me. The solution seems pretty obvious now and so it might not actually be all that difficult; it took me a little time to figure it out however! The height of my display is 320px. I noticed that when I had zoomed in by a factor of 2, the value for <span class="code-snippet">translateY</span> was 320. At a zoom factor of 3, the value was 640px and so on. So basically the limit seemed to be the scale factor minus one, times the height of the display. I'm sure if I had spent more time I could have proved why that is, but at the time I was more concerned with getting this to work:


@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);
    canvas.restore();
}