28

Please see each section below for a description of my problem described in three separate ways. Hopefully should help people to answer.

Problem: How do you find a pair of coordinate expressed in canvas/userspace when you only have the coordinate expressed in terms of a zoomed image, given the original scale point & scale factor?

Problem in practice:

I'm currently trying to replicate the zoom functionality used in apps such as the gallery / maps, when you can pinch to zoom/zoom out with the zoom moving towards the midpoint of the pinch.

On down I save the centre point of the zoom (which is in X,Y coordinates based on the current screen). I then have this function act when a "scale" gesture is detected:

class ImageScaleGestureDetector extends SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        if(mScaleAllowed) 
            mCustomImageView.scale(detector.getScaleFactor(), mCenterX, mCenterY);
        return true;
    }
}   

The scale function of the CustomImageView look like this:

public boolean scale(float scaleFactor, float focusX, float focusY) {
    mScaleFactor *= scaleFactor;

    // Don't let the object get too small or too large.
    mScaleFactor = Math.max(MINIMUM_SCALE_VALUE, Math.min(mScaleFactor, 5.0f));

    mCenterScaleX = focusX;
    mCenterScaleY = focusY;

    invalidate();

    return true;
}

The drawing of the scaled image is achieved by overriding the onDraw method which scales the canvas around the centre ands draw's the image to it.

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

    canvas.save();
    canvas.translate(mCenterScaleX, mCenterScaleY);
    canvas.scale(mScaleFactor, mScaleFactor);
    canvas.translate(-mCenterScaleX, -mCenterScaleY);
    mIcon.draw(canvas);
    canvas.restore();
}

This all works fine when scaling from ScaleFactor 1, this is because the initial mCenterX and mCenterY are coordinates which are based on the device screen. 10, 10 on the device is 10, 10 on the canvas.

After you have already zoomed however, then next time you click position 10, 10 it will no longer correspond to 10, 10 in the canvas because of the scaling & transforming that has already been performed.

Problem in abstraction:

The image below is an example of a zoom operation around centre point A. Each box represents the position and size of the view when at that scale factor (1, 2, 3, 4, 5).

Example

In the example if you scaled by a factor of 2 around A then you clicked on position B, the X, Y reported as B would be based on the screen position - not on the position relative to 0,0 of the initial canvas.

I need to find a way of getting the absolute position of B.

Graeme
  • 25,714
  • 24
  • 124
  • 186
  • The standard View scaling of Android + setting the pivot point won't do the job? – js- Nov 18 '11 at 09:59
  • Not sure what you mean by "standard view scaling and setting of pivot point" ? If you could expand on that it would be great. – Graeme Nov 18 '11 at 10:45
  • 1
    What i meant is using `setScaleX()` and `setScaleY(()` for the scaling and `setPivotX()` and `setPivotY()` for setting the center of the scaling. They are all methods of a View but I just read that the minSdkVersion of those methods is 11 (Honeycomb), so maybe this information is useless to you if you target earlier versions. – js- Nov 18 '11 at 10:52
  • Unfortunately on 2.3 :( Thanks for suggestion though! Hopefully will help someone in future! – Graeme Nov 18 '11 at 10:56
  • 1
    Sony-Ericson has a tut on zooming which may help. http://blogs.sonyericsson.com/wp/2010/05/18/android-one-finger-zoom-tutorial-part-1/ – J.G.Sebring Nov 18 '11 at 16:02
  • This might help. http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html – Glitch Nov 19 '11 at 05:10

3 Answers3

14

So, after redrawing the problem I've found the solution I was looking for. It's gone through a few iteration's but here's how I worked it out:

Solution Description

B - Point, Center of the scale operation

A1, A2, A3 - Points, equal in user-space but different in canvas-space.

You know the values for Bx and By because they are always constant no matter what the scale factor (You know this value in both canvas-space and in user-space).

You know Ax & Ay in user-space so you can find the distance between Ax to Bx and Ay to By. This measurement is in user-space, to convert it to a canvas-space measurement simply divide it by the scale factor. (Once converted to canvas-space, you can see these lines in red, orange and yellow).

As point B is constant, the distance between it and the edges are constant (These are represented by Blue Lines). This value is equal in user-space and canvas-space.

You know the width of the Canvas in canvas-space so by subtracting these two canvas space measurements (Ax to Bx and Bx to Edge) from the total width you are left with the coordinates for point A in canvas-space:

public float[] getAbsolutePosition(float Ax, float Ay) {
    
    float fromAxToBxInCanvasSpace = (mCenterScaleX - Ax) / mScaleFactor;
    float fromBxToCanvasEdge = mCanvasWidth - Bx;
    float x = mCanvasWidth - fromAxToBxInCanvasSpace - fromBxToCanvasEdge;
    
    float fromAyToByInCanvasSpace = (mCenterScaleY - Ay) / mScaleFactor;
    float fromByToCanvasEdge = mCanvasHeight - By;
    float y = mCanvasHeight - fromAyToByInCanvasSpace - fromByToCanvasEdge;
    
    return new float[] { x, y };
}

The above code and image describe when you're clicking to the top left of the original centre. I used the same logic to find A no matter which quadrant it was located in and refactored to the following:

public float[] getAbsolutePosition(float Ax, float Ay) {

    float x = getAbsolutePosition(mBx, Ax);
    float y = getAbsolutePosition(mBy, Ay); 

    return new float[] { x, y };
}

private float getAbsolutePosition(float oldCenter, float newCenter, float mScaleFactor) {
    if(newCenter > oldCenter) {
        return oldCenter + ((newCenter - oldCenter) / mScaleFactor);
    } else {
        return oldCenter - ((oldCenter - newCenter) / mScaleFactor);
    }
}
Community
  • 1
  • 1
Graeme
  • 25,714
  • 24
  • 124
  • 186
  • Hey Graeme I am facing similar kind of issue? and this is eating my head from last 1 month. Finally I found one question which had same problem which i am facing. I need your help to crack the solution ? I understood your above logic little bit but need your more help ...where should I contact you ? Thanks in advance – user1169079 Mar 29 '12 at 03:03
  • I'm one of the owners of the `Android` chat room - I'll be there. – Graeme Mar 29 '12 at 10:01
  • But in chat only users nominated by owner can chat ? Please do accept the request..thanks – user1169079 Apr 03 '12 at 03:21
  • http://stackoverflow.com/questions/9989170/clickable-area-after-scaling-with-respect-to-positions-of-touch-event please check out this since I could not get in to chatroom i posted this question – user1169079 Apr 03 '12 at 07:49
  • Hi @Graeme, can you explain to me the purpose of the last piece of code? For example, in the first method, don't you call the same method here: "float x = getAbsolutePosition(mBx, Ax);"? I also didn't get the relation to the first method that you explained above – Sergio Carneiro Dec 17 '13 at 00:45
  • @SergioCarneiro - the second set of `getAbsolutePosition()` is a re-factored and condensed version of the first method (It also takes account of the quadrant clicked - that's what the `if()` statement is doing). – Graeme Dec 17 '13 at 16:55
  • I think I understood what was confusing me... In the first method you call a `getAbsolutePosition(...)` with 2 args, but it was supposed to call the second one that has 3. So, `float mScaleFactor` is not supposed to be an argument of it :) – Sergio Carneiro Dec 17 '13 at 18:35
2

Here is my solution based on Graeme's answer:

public float[] getAbsolutePosition(float Ax, float Ay) {
    MatrixContext.drawMatrix.getValues(mMatrixValues);

    float x = mWidth - ((mMatrixValues[Matrix.MTRANS_X] - Ax) / mMatrixValues[Matrix.MSCALE_X])
            - (mWidth - getTranslationX());

    float y = mHeight - ((mMatrixValues[Matrix.MTRANS_Y] - Ay) / mMatrixValues[Matrix.MSCALE_X])
            - (mHeight - getTranslationY());

    return new float[] { x, y };
}

the parameters Ax and Ay are the points which user touch via onTouch(), I owned my static matrix instance in MatrixContext class to hold the previous scaled/translated values.

Keanu Pang
  • 21
  • 2
1

Really sorry this is a brief answer, in a rush. But I've been looking at this recently too - I found http://code.google.com/p/android-multitouch-controller/ to do what you want (I think - I had to skim read your post). Hope this helps. I'll have a proper look tonight if this doesn't help and see if I can help further.

Martyn
  • 16,432
  • 24
  • 71
  • 104
  • Have to excuse me because i'm pretty tired at this point, but in this guys source code he uses and interface called "MultiTouchObjectCanvas". In this interface there is a method called "getPositionAndScale" described as "Get the screen coords of the dragged object's origin, and scale multiplier to convert screen coords to obj coords." which is exactly what I want - unfortunately I can't see where this interface is implemented.... – Graeme Nov 18 '11 at 16:32
  • Have a look through his demo - http://code.google.com/p/android-multitouch-controller/source/browse/trunk/demo/MTPhotoSortr/src/org/metalev/multitouch/photosortr/PhotoSortrView.java – Martyn Nov 21 '11 at 08:36