Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Two finger rotation

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

like image 212
paulot Avatar asked May 21 '12 08:05

paulot


2 Answers

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.

like image 52
leszek.hanusz Avatar answered Sep 20 '22 19:09

leszek.hanusz


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);     } } 
like image 35
aaronmarino Avatar answered Sep 21 '22 19:09

aaronmarino