30

I am trying to implement two finger rotation in android however, it is not quite working as expected. The goal is to implement rotation like Google Earth does (two-finger rotating the image around the focal point). Currently my rotation listener looks like this:

 private class RotationGestureListener {
    private static final int INVALID_POINTER_ID = -1;
    private float fX, fY, sX, sY, focalX, focalY;
    private int ptrID1, ptrID2;

    public RotationGestureListener(){
        ptrID1 = INVALID_POINTER_ID;
        ptrID2 = INVALID_POINTER_ID;
    }

    public boolean onTouchEvent(MotionEvent event){
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                sX = event.getX();
                sY = event.getY();
                ptrID1 = event.getPointerId(0);
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                fX = event.getX();
                fY = event.getY();
                focalX = getMidpoint(fX, sX);
                focalY = getMidpoint(fY, sY);
                ptrID2 = event.getPointerId(event.getActionIndex());
                break;
            case MotionEvent.ACTION_MOVE:

                if(ptrID1 != INVALID_POINTER_ID && ptrID2 != INVALID_POINTER_ID){
                    float nfX, nfY, nsX, nsY;
                    nfX = event.getX(event.findPointerIndex(ptrID1));
                    nfY = event.getY(event.findPointerIndex(ptrID1));
                    nsX = event.getX(event.findPointerIndex(ptrID2));
                    nsY = event.getY(event.findPointerIndex(ptrID2));
                    float angle = angleBtwLines(fX, fY, nfX, nfY, sX, sY, nsX, nsY);
                    rotateImage(angle, focalX, focalY);
                    fX = nfX;
                    fY = nfY;
                    sX = nfX;
                    sY = nfY;
                }
                break;
            case MotionEvent.ACTION_UP:
                ptrID1 = INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_POINTER_UP:
                ptrID2 = INVALID_POINTER_ID;
                break;
        }
        return false;
    }

    private float getMidpoint(float a, float b){
        return (a + b) / 2;
    }
    private float angleBtwLines (float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2){
        float angle1 = (float) Math.atan2(fy1 - fy2, fx1 - fx2);
        float angle2 = (float) Math.atan2(sy1 - sy2, sx1 - sx2);
        return (float) Math.toDegrees((angle1-angle2));
    }
}

However whenever I rotate the angle of rotation is much larger and it sometimes it rotates to the wrong side. Any ideas on how to fix this?

By the way I am testing it on a Motorola Atrix, so it does not have the touchscreen bug.

Thanks

paulot
  • 1,258
  • 2
  • 12
  • 24
  • Note some older devices (e.g. HTC Desire) will not correctly detect the positions of the two fingers—it’s liable to confuse their X- and Y-coordinates. – Lawrence D'Oliveiro May 21 '12 at 10:56
  • I would look into those classes: [GestureDetector](http://developer.android.com/reference/android/view/GestureDetector.html) ([source](http://omapzoom.org/?p=platform/frameworks/base.git;a=blob;f=core/java/android/view/GestureDetector.java;h=5c8b23639ba4e3a073577a5a064d17b02a7ab333;hb=HEAD)) [ScaleGestureDetector](http://developer.android.com/reference/android/view/ScaleGestureDetector.html) ([source](http://omapzoom.org/?p=platform/frameworks/base.git;a=blob;f=core/java/android/view/ScaleGestureDetector.java;h=bbb5adef5f0ffc51c2437ae85e703784f63a795a;hb=HEAD)) They contain some nice tricks to – MeTTeO May 21 '12 at 09:10
  • ScaleGestureDetector can only be used for scaling (which I already implemented using the class) and GestureDetector can only be used for single touch gestures. Android does not have a default rotate gesture detector. – paulot May 21 '12 at 09:36
  • You have a problem with angles, not with GestureDection and handling. The symptoms you report are exactly the ones expected with the wrong Angle difference calculation. – Viktor Latypov May 21 '12 at 10:10
  • _pretomba_, I just suggested that you could look into those implementations of those built in classes and see how events from two fingers can be nicely handled. _Viktor_, Java has remainder operator '%' so you don't need to use method and while loop for that... – MeTTeO May 21 '12 at 10:33
  • Thanks, added your suggestion to the and of my answer. – Viktor Latypov May 21 '12 at 10:41

6 Answers6

65

Improvements of the class:

  • angle returned is total since rotation has begun
  • removing unnecessary functions
  • simplification
  • get position of first pointer only after second pointer is down
public class RotationGestureDetector {
    private static final int INVALID_POINTER_ID = -1;
    private float fX, fY, sX, sY;
    private int ptrID1, ptrID2;
    private float mAngle;

    private OnRotationGestureListener mListener;

    public float getAngle() {
        return mAngle;
    }

    public RotationGestureDetector(OnRotationGestureListener listener){
        mListener = listener;
        ptrID1 = INVALID_POINTER_ID;
        ptrID2 = INVALID_POINTER_ID;
    }

    public boolean onTouchEvent(MotionEvent event){
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                ptrID1 = event.getPointerId(event.getActionIndex());
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                ptrID2 = event.getPointerId(event.getActionIndex());
                sX = event.getX(event.findPointerIndex(ptrID1));
                sY = event.getY(event.findPointerIndex(ptrID1));
                fX = event.getX(event.findPointerIndex(ptrID2));
                fY = event.getY(event.findPointerIndex(ptrID2));
                break;
            case MotionEvent.ACTION_MOVE:
                if(ptrID1 != INVALID_POINTER_ID && ptrID2 != INVALID_POINTER_ID){
                    float nfX, nfY, nsX, nsY;
                    nsX = event.getX(event.findPointerIndex(ptrID1));
                    nsY = event.getY(event.findPointerIndex(ptrID1));
                    nfX = event.getX(event.findPointerIndex(ptrID2));
                    nfY = event.getY(event.findPointerIndex(ptrID2));

                    mAngle = angleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);

                    if (mListener != null) {
                        mListener.OnRotation(this);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                ptrID1 = INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_POINTER_UP:
                ptrID2 = INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_CANCEL:
                ptrID1 = INVALID_POINTER_ID;
                ptrID2 = INVALID_POINTER_ID;
                break;
        }
        return true;
    }

    private float angleBetweenLines (float fX, float fY, float sX, float sY, float nfX, float nfY, float nsX, float nsY)
    {
        float angle1 = (float) Math.atan2( (fY - sY), (fX - sX) );
        float angle2 = (float) Math.atan2( (nfY - nsY), (nfX - nsX) );

        float angle = ((float)Math.toDegrees(angle1 - angle2)) % 360;
        if (angle < -180.f) angle += 360.0f;
        if (angle > 180.f) angle -= 360.0f;
        return angle;
    }

    public static interface OnRotationGestureListener {
        public void OnRotation(RotationGestureDetector rotationDetector);
    }
}

How to use it:

  1. Put the above class in a separate file RotationGestureDetector.java
  2. create a private field mRotationDetector of type RotationGestureDetector in your activity class and create a new instance of the detector during the initialization (onCreate method for example) and give as parameter a class implementing the onRotation method (here the activity = this).
  3. In the method onTouchEvent, send the touch events received to the gesture detector with 'mRotationDetector.onTouchEvent(event);'
  4. Implements RotationGestureDetector.OnRotationGestureListener in your activity and add the method 'public void OnRotation(RotationGestureDetector rotationDetector)' in the activity. In this method, get the angle with rotationDetector.getAngle()

Example:

public class MyActivity extends Activity implements RotationGestureDetector.OnRotationGestureListener {
    private RotationGestureDetector mRotationDetector;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mRotationDetector = new RotationGestureDetector(this);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        mRotationDetector.onTouchEvent(event);
        return super.onTouchEvent(event);
    }

    @Override
    public void OnRotation(RotationGestureDetector rotationDetector) {
        float angle = rotationDetector.getAngle();
        Log.d("RotationGestureDetector", "Rotation: " + Float.toString(angle));
    }

}

Note:

You can also use the RotationGestureDetector class in a View instead of an Activity.

Asif Patel
  • 1,666
  • 1
  • 19
  • 26
leszek.hanusz
  • 4,418
  • 2
  • 34
  • 53
  • 1
    This works well for me. I added some custom filtering for my own specific needs, but this saved me a lot of time. Thanks. – KnucklesTheDog Mar 12 '14 at 14:07
  • dragging my finger in one direction i get 178.33331, -177.07356, -177.14868, 131.9633 I mean values that make no sense at all. bogus – Tyler Davis Aug 26 '14 at 23:47
  • Anyway to implement this in a custom view class? that extends view? – Coova Sep 24 '14 at 02:50
  • 1
    Tyler Davis, I was having the same problem. See my answer below for a solution. – aaronmarino Nov 13 '14 at 14:03
  • 2
    just to be clear, this doesn't actually handle the rotation right? It just gets the angle that the object has been rotated so that it can be used in like a Matrix.postRotate call to actually rotate the view – BigBoy1337 Jun 03 '15 at 02:23
  • Great solution! One optimization that can be applied is to calculate the initial angle only when gesture is started and cache it. You can avoid calculating `(float) Math.atan2( (fY - sY), (fX - sX) );` every move this way. – Alexander Vakrilov Nov 18 '15 at 14:54
  • 1
    It is notable that angleBetweenLines() provides the accumulative angle. If you need delta angle, you should write (inside onTouchEvent()) mDeltaAngle = mAngle - mPreviousAngle and return mDeltaAngle. Also write mPreviousAngle = mAngle inside the "if (mListener != null)" block. Don't forget to initialized it to zero and also make it zero in other 'cases' in the switch block. – Kamran Bigdely Jul 28 '16 at 19:30
  • With this solution, can we get correct touch x,y coordinates? Because this solution does not use any Metrix. – isuru Mar 08 '17 at 10:50
16

Here's my improvement on Leszek's answer. I found that his didn't work for small views as when a touch went outside the view the angle calculation was wrong. The solution is to get the raw location instead of just getX/Y.

Credit to this thread for getting the raw points on a rotatable view.

public class RotationGestureDetector {

    private static final int INVALID_POINTER_ID = -1;
    private PointF mFPoint = new PointF();
    private PointF mSPoint = new PointF();
    private int mPtrID1, mPtrID2;
    private float mAngle;
    private View mView;

    private OnRotationGestureListener mListener;

    public float getAngle() {
        return mAngle;
    }

    public RotationGestureDetector(OnRotationGestureListener listener, View v) {
        mListener = listener;
        mView = v;
        mPtrID1 = INVALID_POINTER_ID;
        mPtrID2 = INVALID_POINTER_ID;
    }

    public boolean onTouchEvent(MotionEvent event) {


        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_OUTSIDE:
                Log.d(this, "ACTION_OUTSIDE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.v(this, "ACTION_DOWN");
                mPtrID1 = event.getPointerId(event.getActionIndex());
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                Log.v(this, "ACTION_POINTER_DOWN");
                mPtrID2 = event.getPointerId(event.getActionIndex());

                getRawPoint(event, mPtrID1, mSPoint);
                getRawPoint(event, mPtrID2, mFPoint);

                break;
            case MotionEvent.ACTION_MOVE:
                if (mPtrID1 != INVALID_POINTER_ID && mPtrID2 != INVALID_POINTER_ID) {
                    PointF nfPoint = new PointF();
                    PointF nsPoint = new PointF();

                    getRawPoint(event, mPtrID1, nsPoint);
                    getRawPoint(event, mPtrID2, nfPoint);

                    mAngle = angleBetweenLines(mFPoint, mSPoint, nfPoint, nsPoint);

                    if (mListener != null) {
                        mListener.onRotation(this);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                mPtrID1 = INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_POINTER_UP:
                mPtrID2 = INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_CANCEL:
                mPtrID1 = INVALID_POINTER_ID;
                mPtrID2 = INVALID_POINTER_ID;
                break;
            default:
                break;
        }
        return true;
    }

    void getRawPoint(MotionEvent ev, int index, PointF point) {
        final int[] location = { 0, 0 };
        mView.getLocationOnScreen(location);

        float x = ev.getX(index);
        float y = ev.getY(index);

        double angle = Math.toDegrees(Math.atan2(y, x));
        angle += mView.getRotation();

        final float length = PointF.length(x, y);

        x = (float) (length * Math.cos(Math.toRadians(angle))) + location[0];
        y = (float) (length * Math.sin(Math.toRadians(angle))) + location[1];

        point.set(x, y);
    }

    private float angleBetweenLines(PointF fPoint, PointF sPoint, PointF nFpoint, PointF nSpoint) {
        float angle1 = (float) Math.atan2((fPoint.y - sPoint.y), (fPoint.x - sPoint.x));
        float angle2 = (float) Math.atan2((nFpoint.y - nSpoint.y), (nFpoint.x - nSpoint.x));

        float angle = ((float) Math.toDegrees(angle1 - angle2)) % 360;
        if (angle < -180.f) angle += 360.0f;
        if (angle > 180.f) angle -= 360.0f;
        return -angle;
    }

    public interface OnRotationGestureListener {
        void onRotation(RotationGestureDetector rotationDetector);
    }
}
Alex
  • 838
  • 2
  • 6
  • 21
aaronmarino
  • 2,447
  • 19
  • 30
  • This is great! Thanks! The only problem I'm getting, though, is that after the first rotation, every rotation resets the angle to 0 first. I've been unable to fix this issue for awhile. Any ideas on how to implement delta-anglebetweenlines? – Erwin Lengkeek Dec 15 '14 at 15:28
  • 3
    I managed to fix it myself by saving the old angle in ACTOIN_POINTER_DOWN and adding it to mAngle in ACTION_MOVE (and using modulo 360 on that value). I hope this might be useful for someone that tries to do the same as me. – Erwin Lengkeek Dec 15 '14 at 20:49
10

I tried a combination of answers that are here but it still didn't work perfectly so I had to modify it a little bit.

This code gives you the delta angle on each rotation, it works perfectly to me, I'm using it to rotate an object in OpenGL.

public class RotationGestureDetector {
private static final int INVALID_POINTER_ID = -1;
private float fX, fY, sX, sY, focalX, focalY;
private int ptrID1, ptrID2;
private float mAngle;
private boolean firstTouch;

private OnRotationGestureListener mListener;

public float getAngle() {
    return mAngle;
}

public RotationGestureDetector(OnRotationGestureListener listener){
    mListener = listener;
    ptrID1 = INVALID_POINTER_ID;
    ptrID2 = INVALID_POINTER_ID;
}


public boolean onTouchEvent(MotionEvent event){
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            sX = event.getX();
            sY = event.getY();
            ptrID1 = event.getPointerId(0);
            mAngle = 0;
            firstTouch = true;
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            fX = event.getX();
            fY = event.getY();
            focalX = getMidpoint(fX, sX);
            focalY = getMidpoint(fY, sY);
            ptrID2 = event.getPointerId(event.getActionIndex());
            mAngle = 0;
            firstTouch = true;
            break;
        case MotionEvent.ACTION_MOVE:

            if(ptrID1 != INVALID_POINTER_ID && ptrID2 != INVALID_POINTER_ID){
                float nfX, nfY, nsX, nsY;
                nsX = event.getX(event.findPointerIndex(ptrID1));
                nsY = event.getY(event.findPointerIndex(ptrID1));
                nfX = event.getX(event.findPointerIndex(ptrID2));
                nfY = event.getY(event.findPointerIndex(ptrID2));
                if (firstTouch) {
                    mAngle = 0;
                    firstTouch = false;
                } else {
                    mAngle = angleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);
                }

                if (mListener != null) {
                    mListener.OnRotation(this);
                }
                fX = nfX;
                fY = nfY;
                sX = nsX;
                sY = nsY;
            }
            break;
        case MotionEvent.ACTION_UP:
            ptrID1 = INVALID_POINTER_ID;
            break;
        case MotionEvent.ACTION_POINTER_UP:
            ptrID2 = INVALID_POINTER_ID;
            break;
    }
    return true;
}

private float getMidpoint(float a, float b){
    return (a + b) / 2;
}

float findAngleDelta( float angle1, float angle2 )
{
    float From = ClipAngleTo0_360( angle2 );
    float To   = ClipAngleTo0_360( angle1 );

    float Dist  = To - From;

    if ( Dist < -180.0f )
    {
        Dist += 360.0f;
    }
    else if ( Dist > 180.0f )
    {
        Dist -= 360.0f;
    }

    return Dist;
}

float ClipAngleTo0_360( float Angle ) { 
    return Angle % 360.0f; 
}

private float angleBetweenLines (float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2)
{
       float angle1 = (float) Math.atan2( (fy1 - fy2), (fx1 - fx2) );
       float angle2 = (float) Math.atan2( (sy1 - sy2), (sx1 - sx2) );

       return findAngleDelta((float)Math.toDegrees(angle1),(float)Math.toDegrees(angle2));
}

public static interface OnRotationGestureListener {
    public boolean OnRotation(RotationGestureDetector rotationDetector);
}
}
Jorge Garcia
  • 680
  • 6
  • 12
6

There are still some mistakes, here is the solution that worked perfect for me...

instead of

float angle = angleBtwLines(fX, fY, nfX, nfY, sX, sY, nsX, nsY);

you need to write

float angle = angleBtwLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);

And angleBetweenLines should be

private float angleBetweenLines (float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2)
{
       float angle1 = (float) Math.atan2( (fy1 - fy2), (fx1 - fx2) );
       float angle2 = (float) Math.atan2( (sy1 - sy2), (sx1 - sx2) );

        return findAngleDelta((float)Math.toDegrees(angle1),(float)Math.toDegrees(angle2));
}

Then the angle you get is the angle you should rotate the image by...

ImageAngle += angle...
par
  • 16,065
  • 4
  • 61
  • 77
Nir Hartmann
  • 1,279
  • 12
  • 14
5

You have a problem here:

private float angleBtwLines (float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2){
    float angle1 = (float) Math.atan2(fy1 - fy2, fx1 - fx2);
    float angle2 = (float) Math.atan2(sy1 - sy2, sx1 - sx2);
    return (float) Math.toDegrees((angle1-angle2));
}

You must clip the angles to the [0..2*Pi] range and than carefully calculate the angular difference in the (-Pi..+Pi) range.

Here's the code for 0..360 angle range

float FindAngleDelta( float angle1, float angle2 )
{
    float From = ClipAngleTo0_360( angle2 );
    float To   = ClipAngleTo0_360( angle1 );

    float Dist  = To - From;

    if ( Dist < -180.0f )
    {
        Dist += 360.0f;
    }
    else if ( Dist > 180.0f )
    {
        Dist -= 360.0f;
    }

    return Dist;
}

In C++ I would code the ClipAngleTo0_360 as

float ClipAngleTo0_360( float Angle ) { return std::fmod( Angle, 360.0f ); }

where the std::fmod return the floating-point remainder.

In java you may use something like

float ClipAngleTo0_360( float Angle )
{
    float Res = Angle;
    while(Angle < 0) { Angle += 360.0; }
    while(Angle >= 360.0) { Angle -= 360.0; }
    return Res;
}

Yeah, careful floating-point arithmetics is much better than the obvious while() loop.

As MeTTeO mentioned (java reference, 15.17.3), you can use the '%' operator instead of C++'s std::fmod:

float ClipAngleTo0_360( float Angle ) { return Angle % 360.0; }
Viktor Latypov
  • 13,683
  • 3
  • 36
  • 53
1

I tried a lot of examples. But only this one works good.:

public class RotationGestureDetector {

  public interface RotationListener {
    public void onRotate(float deltaAngle);
  }

  protected float mRotation;
  private RotationListener mListener;

  public RotationGestureDetector(RotationListener listener) {
    mListener = listener;
  }

  private float rotation(MotionEvent event) {
    double delta_x = (event.getX(0) - event.getX(1));
    double delta_y = (event.getY(0) - event.getY(1));
    double radians = Math.atan2(delta_y, delta_x);
    return (float) Math.toDegrees(radians);
  }

  public void onTouch(MotionEvent e) {
    if (e.getPointerCount() != 2) {
      return;
    }

    if (e.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
      mRotation = rotation(e);
    }

    float rotation = rotation(e);
    float delta = rotation - mRotation;
    mRotation += delta;

    if (mListener != null) {
      mListener.onRotate(delta);}
    }
  }
}

In your callback:

view.setRotation(view.getRotation() + deltaAngle));
m02ph3u5
  • 2,735
  • 6
  • 34
  • 45
vihkat
  • 695
  • 9
  • 24