I have a SurfaceView that is resposible for drawing a Bitmap as a background and another one that will be used as an overlay. So I’ve decided to do all transformations using a Matrix that can be used for both bitmaps as it is (I think) one of the fastest ways to do it without using OpenGL.
I’ve been able to implement panning around and zooming but I have some problems with what I’ve came with:
- I wasn’t able to find a way how to focus on the center of the two
fingers while zooming, the image always resets to its initial state
(that is, without panning nor scalling) before the new scale being
applied. Besides looking wrong, that doesn’t allow the user to zoom
out to see the whole image and then zoom in on the part that is
important. - After the scalling operation the image won’t be at the
same place after the new draw pass because the translation value will
be different.
Is there a way to achieve that using a Matrix or is there another solution?
Code is below (I use a SurfaceHolder in a separate thread do lock the SurfaceView canvas and call its doDraw method):
public class MapSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
public void doDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(mBitmap, mTransformationMatrix, mPaintAA);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN: {
if (event.getPointerCount() == 2) {
mOriginalDistance = MathUtils.distanceBetween(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
mScreenMidpoint = MathUtils.midpoint(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
mImageMidpoint = MathUtils.midpoint((mXPosition+event.getX(0))/mScale, (mXPosition+event.getX(1))/mScale, (mYPosition+event.getY(0))/mScale, (mYPosition+event.getY(1))/mScale);
mOriginalScale = mScale;
}
}
case MotionEvent.ACTION_DOWN: {
mOriginalTouchPoint = new Point((int)event.getX(), (int)event.getY());
mOriginalPosition = new Point(mXPosition, mYPosition);
break;
}
case MotionEvent.ACTION_MOVE: {
if (event.getPointerCount() == 2) {
final double currentDistance = MathUtils.distanceBetween(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
if (mIsZooming || currentDistance - mOriginalDistance > mPinchToZoomTolerance || mOriginalDistance - currentDistance > mPinchToZoomTolerance) {
final float distanceRatio = (float) (currentDistance / mOriginalDistance);
float tempZoom = mOriginalScale * distanceRatio;
mScale = Math.min(10, Math.max(Math.min((float)getHeight()/(float)mBitmap.getHeight(), (float)getWidth()/(float)mBitmap.getWidth()), tempZoom));
mScale = (float) MathUtils.roundToDecimals(mScale, 1);
mIsZooming = true;
mTransformationMatrix = new Matrix();
mTransformationMatrix.setScale(mScale, mScale);//, mImageMidpoint.x, mImageMidpoint.y);
} else {
System.out.println("Dragging");
mIsZooming = false;
final int deltaX = (int) ((int) (mOriginalTouchPoint.x - event.getX()));
final int deltaY = (int) ((int) (mOriginalTouchPoint.y - event.getY()));
mXPosition = mOriginalPosition.x + deltaX;
mYPosition = mOriginalPosition.y + deltaY;
validatePositions();
mTransformationMatrix = new Matrix();
mTransformationMatrix.setScale(mScale, mScale);
mTransformationMatrix.postTranslate(-mXPosition, -mYPosition);
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
mIsZooming = false;
validatePositions();
mTransformationMatrix = new Matrix();
mTransformationMatrix.setScale(mScale, mScale);
mTransformationMatrix.postTranslate(-mXPosition, -mYPosition);
}
}
return true;
}
private void validatePositions() {
// Lower right corner
mXPosition = Math.min(mXPosition, (int)((mBitmap.getWidth() * mScale)-getWidth()));
mYPosition = Math.min(mYPosition, (int)((mBitmap.getHeight() * mScale)-getHeight()));
// Upper left corner
mXPosition = Math.max(mXPosition, 0);
mYPosition = Math.max(mYPosition, 0);
// Image smaller than the container, should center it
if (mBitmap.getWidth() * mScale <= getWidth()) {
mXPosition = (int) -((getWidth() - (mBitmap.getWidth() * mScale))/2);
}
if (mBitmap.getHeight() * mScale <= getHeight()) {
mYPosition = (int) -((getHeight() - (mBitmap.getHeight() * mScale))/2);
}
}
}
Instead of resetting the transformation matrix every time using new Matrix(), try updating it using post*(). This way, you do only operations relative to the screen. It is easier to think in terms: “zoom to this point on the screen”.
Now some code. Having calculated mScale in zooming part:
It might be even better to recalculate mScreenMidpoint on each zooming touch event. This would allow user to change the focus point a bit while zooming. For me, it is more natural than having the focus point frozen after first two finger touch.
During dragging, you translate using deltaX and deltaY instead of absolute points:
Of course now you have to change your validatePositions() method to:
I will describe the second method, as it is more flexible and allows to validate zooming as well.
We calculate how much image is off screen and then move it using those values: