Android Two finger rotation
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:
- Put the above class in a separate file
RotationGestureDetector.java
- create a private field
mRotationDetector
of typeRotationGestureDetector
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 theonRotation
method (here theactivity = this
). - In the method
onTouchEvent
, send the touch events received to the gesture detector with 'mRotationDetector.onTouchEvent(event);
' - Implements
RotationGestureDetector.OnRotationGestureListener
in your activity and add the method 'public void OnRotation(RotationGestureDetector rotationDetector)
' in the activity. In this method, get the angle withrotationDetector.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
.
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); }}
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);}}