Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fix pinch zoom focal point in a custom view?

For my question I have prepared a very simple test app at Github.

For simplicity I have removed flinging, scroll constraints and edge effects (which actually work well in my real app):

test app screenshot

So the custom view in my test app only supports scrolling:

mGestureDetector = new GestureDetector(context,
        new GestureDetector.SimpleOnGestureListener() {
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float dX, float dY) {
        mBoardScrollX -= dX;
        mBoardScrollY -= dY;

        ViewCompat.postInvalidateOnAnimation(MyView.this);
        return true;
    }
});

and pinch zooming with 2 fingers (the focus is broken though!):

mScaleDetector = new ScaleGestureDetector(context,
        new ScaleGestureDetector.SimpleOnScaleGestureListener() {
    @Override
    public boolean onScale(ScaleGestureDetector scaleDetector) {
        float focusX = scaleDetector.getFocusX();
        float focusY = scaleDetector.getFocusY();
        float factor = scaleDetector.getScaleFactor();

        mBoardScrollX = mBoardScrollX + focusX * (1 - factor) * mBoardScale;
        mBoardScrollY = mBoardScrollY + focusY * (1 - factor) * mBoardScale;
        mBoardScale *= factor;

        ViewCompat.postInvalidateOnAnimation(MyView.this);
        return true;
    }
});

Finally, here the code drawing the scalled and offsetted game board Drawable:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.scale(mBoardScale, mBoardScale);
    canvas.translate(mBoardScrollX / mBoardScale, mBoardScrollY / mBoardScale);
    mBoard.draw(canvas);
    canvas.restore();
}

If you try running my app, you will notice that while scaling the game board with two finger pinch zoom gesture works, the zoom focal point jumps around.

My understanding is that while scaling the Drawable I need to pan it by adjusting the mBoardScrollX and mBoardScrollY values - so that the focus point stays at the same point in the game board coordinates. So my calculation is -

The old position of that point is:

-mBoardScrollX + focusX * mBoardScale
-mBoardScrollY + focusY * mBoardScale

and the new position would be at:

-mBoardScrollX + focusX * mBoardScale * factor
-mBoardScrollY + focusY * mBoardScale * factor

By solving these 2 linear equations I get:

mBoardScrollX = mBoardScrollX + focusX * (1 - factor) * mBoardScale;
mBoardScrollY = mBoardScrollY + focusY * (1 - factor) * mBoardScale;

However that does not work!

To eliminate any errors I have even tried hardcoding the focal point to the middle of my custom view - and still the game board center sways around while scaling:

float focusX = getWidth() / 2f;
float focusY = getHeight() / 2f;

I think I am missing something minor, please help me.

I would prefer to find a solution without using Matrix, because I believe that something really minor is missing in the above calculations. And yes, I have already studied a lot of comparable code, including the PhotoView by Chris Banes and the InteractiveChart example by Google.

DOUBLE TAP UPDATE:

The Matrix-based solution by pskink works very well, however I still have one issue with a wrong focal point -

I have tried to add code to the custom view to increase zoom by 100% on a double tap gesture:

public boolean onDoubleTap(final MotionEvent e) {
    float[] values = new float[9];
    mMatrix.getValues(values);
    float scale = values[Matrix.MSCALE_X];
    ValueAnimator animator = ValueAnimator.ofFloat(scale, 2f * scale);
    animator.setDuration(3000);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animator){
            float scale = (float) animator.getAnimatedValue();
            mMatrix.setScale(scale, scale, e.getX(), e.getY());
            ViewCompat.postInvalidateOnAnimation(MyView.this);
        }
    });
    animator.start();
    return true;
}

And while the zoom changes correctly, the focal point is wrong again - even though the focal point coordinates are passed to each setScale call.

For example when I double tap in the middle of the screen, the result will be panned too far right and down:

emulator screenshot

like image 748
Alexander Farber Avatar asked Mar 20 '19 09:03

Alexander Farber


People also ask

How do you implement pinch to zoom?

You can use the pinch to zoom feature with videos and live streams playing in full screen. Pinch to zoom in on content: In full screen mode, touch the video with two fingers. Move your fingers away from one another, while touching the video screen.

Does pinch zoom change image?

Pinch-to-zoom on all devices may use algorithms, but only to scale the image — it doesn't change the content itself.

How do I pinch zoom on Android emulator?

Tap anywhere on the screen, except the keyboard or navigation bar. Drag 2 fingers to move around the screen. Pinch with 2 fingers to adjust zoom.


1 Answers

Using Matrix is a really better idea - the code is much more simple and you dont have to prove your math skills ;-), see how Matrix#postTranslate and Matrix#postScale methods are used:

class MyView extends View {
    private static final String TAG = "MyView";

    private final ScaleGestureDetector mScaleDetector;
    private final GestureDetector mGestureDetector;

    private final Drawable mBoard;
    private final float mBoardWidth;
    private final float mBoardHeight;
    private Matrix mMatrix;

    public MyView(Context context) {
        this(context, null, 0);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mBoard = ResourcesCompat.getDrawable(context.getResources(), R.drawable.chrome, null);
        mBoardWidth = mBoard.getIntrinsicWidth();
        mBoardHeight = mBoard.getIntrinsicHeight();
        mBoard.setBounds(0, 0, (int) mBoardWidth, (int) mBoardHeight);

        mMatrix = new Matrix();

        mScaleDetector = new ScaleGestureDetector(context, scaleListener);
        mGestureDetector = new GestureDetector(context, listener);
    }

    ScaleGestureDetector.OnScaleGestureListener scaleListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
        @Override
        public boolean onScale(ScaleGestureDetector scaleDetector) {
            float factor = scaleDetector.getScaleFactor();
            mMatrix.postScale(factor, factor, getWidth() / 2f, getHeight() / 2f);
            ViewCompat.postInvalidateOnAnimation(MyView.this);
            return true;
        }
    };

    GestureDetector.OnGestureListener listener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float dX, float dY) {
            mMatrix.postTranslate(-dX, -dY);
            ViewCompat.postInvalidateOnAnimation(MyView.this);
            return true;
        }
    };

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        float scale = Math.max(w / mBoardWidth, h / mBoardHeight);
        mMatrix.setScale(scale, scale);
        mMatrix.postTranslate((w - scale * mBoardWidth) / 2f, (h - scale * mBoardHeight) / 2f);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        canvas.concat(mMatrix);
        mBoard.draw(canvas);
        canvas.restore();
    }

    @Override
    @SuppressLint("ClickableViewAccessibility")
    public boolean onTouchEvent(MotionEvent e) {
        mGestureDetector.onTouchEvent(e);
        mScaleDetector.onTouchEvent(e);
        return true;
    }
}
like image 196
pskink Avatar answered Sep 20 '22 08:09

pskink