Android Two finger rotation
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);
}
}
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);
}
}
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
.
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...