• After 15+ years, we've made a big change: Android Forums is now Early Bird Club. Learn more here.

Apps Bug in Rotating ImageView Android with two fingers

AssetFX

Lurker
Aug 2, 2012
3
0
Hi,
in the following code I have setup a few gestures for an ImageView.
All was well until I got to implementing rotation functionality with two fingers. Before I added this a two fingers gesture was used to pan and scale the image using the focus point as its center.


Now adding another level of complexity to it, with the rotation, it has come undone.
- The first bug is that the pan is losing movement if at a scale larger than 1 and gaining more movement if less than one.
- The second bug is that if you scale the image then lift your fingers, place them somewhere else the image jumps.


Obviously it is to do with the scale but I am having issues trying to figure the resolution.
The piece where I recalculate mPosX and mPosY in RotatePoint2D is the one causing the headache but it may be the figures I throwing in as well.


Any help would greatly be appreciated

Code:
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.ImageView;

public class MyImageView extends ImageView {
    
    private static final int INVALID_POINTER_ID = -1;

    private float mPosX;
    private float mPosY;

    private float mLastTouchX;
    private float mLastTouchY;
    private float mLastGestureX;
    private float mLastGestureY;
    private float mRotationDegrees = 0;
    private int mActivePointerId = INVALID_POINTER_ID;
    private int mActivePointerId2 = INVALID_POINTER_ID;
    float oL1X1, oL1Y1, oL1X2, oL1Y2;

    private ScaleGestureDetector mScaleDetector;
    private GestureDetector mGestureDetector;
    
    private float mScaleFactor = 1.f;
    
    //The following variable control the fling gesture
    private Interpolator animateInterpolator;
    private long startTime;
    private long endTime;
    private float totalAnimDx;
    private float totalAnimDy;
    private float lastAnimDx;
    private float lastAnimDy;

    public MyImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
        mGestureDetector = new GestureDetector(getContext(), new GestureListener());
    }

    public MyImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
        mGestureDetector = new GestureDetector(context, new GestureListener());
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Let the ScaleGestureDetector inspect all events.
        mScaleDetector.onTouchEvent(ev);
        mGestureDetector.onTouchEvent(ev);

        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                if (!mScaleDetector.isInProgress()) {
                    final float x = ev.getX();
                    final float y = ev.getY();
                    
                    mLastTouchX = x;
                    mLastTouchY = y;
                    mActivePointerId = ev.getPointerId(0);
                }
                break;
            }
            case MotionEvent.ACTION_POINTER_DOWN: {
                if (mScaleDetector.isInProgress()) {
                    mActivePointerId2 = ev.getPointerId(1);
                    
                    mLastGestureX = mScaleDetector.getFocusX();
                    mLastGestureY = mScaleDetector.getFocusY();
                    
                    oL1X1 = ev.getX(ev.findPointerIndex(mActivePointerId));
                    oL1Y1 = ev.getY(ev.findPointerIndex(mActivePointerId));
                    oL1X2 = ev.getX(ev.findPointerIndex(mActivePointerId2));
                    oL1Y2 = ev.getY(ev.findPointerIndex(mActivePointerId2));
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                
                // Only move if the ScaleGestureDetector isn't processing a gesture.
                if (!mScaleDetector.isInProgress()) {
                    final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(pointerIndex);
                    final float y = ev.getY(pointerIndex);
                    
                    final float dx = x - mLastTouchX;
                    final float dy = y - mLastTouchY;
                    
                    mPosX += dx;
                    mPosY += dy;
                    
                    invalidate();
                    
                    mLastTouchX = x;
                    mLastTouchY = y;
                }
                else{
                    final float gx = mScaleDetector.getFocusX();
                    final float gy = mScaleDetector.getFocusY();
                    
                    final float gdx = gx - mLastGestureX;
                    final float gdy = gy - mLastGestureY;
                    
                    mPosX += gdx;
                    mPosY += gdy;
                    
                    float nL2X1, nL2Y1, nL2X2, nL2Y2;
                    nL2X1 = ev.getX(ev.findPointerIndex(mActivePointerId));
                    nL2Y1 = ev.getY(ev.findPointerIndex(mActivePointerId));
                    nL2X2 = ev.getX(ev.findPointerIndex(mActivePointerId2));
                    nL2Y2 = ev.getY(ev.findPointerIndex(mActivePointerId2));
                    
                    float angleDifference = angleBetween2Lines(oL1X1, oL1Y1, oL1X2, oL1Y2, nL2X1, nL2Y1, nL2X2, nL2Y2);
                    RotatePoint2D(gx, gy, mPosX, mPosY, angleDifference);
                    mRotationDegrees += angleDifference;
                    
                    oL1X1 = nL2X1;
                    oL1Y1 = nL2Y1;
                    oL1X2 = nL2X2;
                    oL1Y2 = nL2Y2;
                    
                    invalidate();

                    mLastGestureX = gx;
                    mLastGestureY = gy;
                }
                
                break;
            }
            case MotionEvent.ACTION_UP: {
                mActivePointerId = INVALID_POINTER_ID;
                
                break;
            }
            case MotionEvent.ACTION_CANCEL: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }
            case MotionEvent.ACTION_POINTER_UP: {
                
                final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) 
                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int pointerId = ev.getPointerId(pointerIndex);
                if (pointerId == mActivePointerId) {
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mLastTouchX = ev.getX(newPointerIndex);
                    mLastTouchY = ev.getY(newPointerIndex);
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                }
                else{
                    final int tempPointerIndex = ev.findPointerIndex(mActivePointerId);
                    mLastTouchX = ev.getX(tempPointerIndex);
                    mLastTouchY = ev.getY(tempPointerIndex);
                }
                
                break;
            }
        }

        return true;
    }

    @Override
    public void onDraw(Canvas canvas) {
        canvas.save();
        canvas.translate(mPosX, mPosY);
        if (mScaleDetector.isInProgress()) {
            canvas.rotate(mRotationDegrees);
            canvas.scale(mScaleFactor, mScaleFactor, mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
        }
        else{
            canvas.rotate(mRotationDegrees);
            canvas.scale(mScaleFactor, mScaleFactor, mLastGestureX, mLastGestureY);
        }
        super.onDraw(canvas);
        canvas.restore();
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();

            // Don't let the object get too small or too large.
            mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));

            invalidate();
            return true;
        }
    }

    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            
            if (e1 == null || e2 == null){
                return false;
            }
            final float distanceTimeFactor = 0.4f;
            final float totalDx = (distanceTimeFactor * velocityX/2);
            final float totalDy = (distanceTimeFactor * velocityY/2);
            
            onAnimateMove(totalDx, totalDy, (long) (1000 * distanceTimeFactor));
            return true;
        }
    }
    
    public void onAnimateMove(float dx, float dy, long duration) {
        animateInterpolator = new DecelerateInterpolator();
        startTime = System.currentTimeMillis();
        endTime = startTime + duration;
        totalAnimDx = dx;
        totalAnimDy = dy;
        lastAnimDx = 0;
        lastAnimDy = 0;
        
        post(new Runnable() {
            @Override
            public void run() {
                onAnimateStep();
            }
        });
    }
    
    private void onAnimateStep() {
        long curTime = System.currentTimeMillis();
        float percentTime = (float) (curTime - startTime) / (float) (endTime - startTime);
        float percentDistance = animateInterpolator.getInterpolation(percentTime);
        float curDx = percentDistance * totalAnimDx;
        float curDy = percentDistance * totalAnimDy;
        
        float diffCurDx = curDx - lastAnimDx;
        float diffCurDy = curDy - lastAnimDy;
        lastAnimDx = curDx;
        lastAnimDy = curDy;
        
        doAnimation(diffCurDx, diffCurDy);
        
        if (percentTime < 1.0f) {
            post(new Runnable() {
                @Override
                public void run() {
                    onAnimateStep();
                }
            });
        }
    }
    
    public void doAnimation(float diffDx, float diffDy) {
        mPosX += diffDx;
        mPosY += diffDy;
        
        invalidate();
    }
    
    public float angleBetween2Lines(float L1X1, float L1Y1, float L1X2, float L1Y2, float L2X1, float L2Y1, float L2X2, float L2Y2)
    {
        float angle1 = (float) Math.atan2(L1Y1 - L1Y2, L1X1 - L1X2);
        float angle2 = (float) Math.atan2(L2Y1 - L2Y2, L2X1 - L2X2);
        
        float angleDelta = findAngleDelta( (float)Math.toDegrees(angle1), (float)Math.toDegrees(angle2));
        return -angleDelta;
    }
    
    private float findAngleDelta( float angle1, float angle2 )
    {
        return angle1 - angle2;
    }
    
    private void RotatePoint2D(float ox, float oy, float px, float py, float angle)   
    {
        double thetaAngle = Math.toRadians(angle);
        
        //If you rotate point (px, py) around point (ox, oy) by angle theta you'll get:
        mPosX = (float)(Math.cos(thetaAngle) * (px-ox) - Math.sin(thetaAngle) * (py-oy) + ox);
        mPosY = (float)(Math.sin(thetaAngle) * (px-ox) + Math.cos(thetaAngle) * (py-oy) + oy);
        //Log.d("","ox: " + ox + " oy: " + oy + " px: " + px + " py: " + py + " angle: " + angle + " thetaAngle: " + thetaAngle + " mPosX: " + mPosX + " mPosY: " + mPosY);
    }
}
 

BEST TECH IN 2023

We've been tracking upcoming products and ranking the best tech since 2007. Thanks for trusting our opinion: we get rewarded through affiliate links that earn us a commission and we invite you to learn more about us.

Smartphones