Search code examples
androidrotationgesture-recognition

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


Solution

  • 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.