35

Can we use scale gesture detector for pinch zoom in Android?

Zankhna
  • 4,570
  • 9
  • 62
  • 103
user562237
  • 775
  • 5
  • 15
  • 24

6 Answers6

29

You can create a reusable class that implements OnTouchListener to accomplish this.

public class MyScaleGestures implements OnTouchListener, OnScaleGestureListener {       
    private View view;
    private ScaleGestureDetector gestureScale;
    private float scaleFactor = 1;  
    private boolean inScale = false;

    public MyScaleGestures (Context c){ gestureScale = new ScaleGestureDetector(c, this); }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        this.view = view; 
        gestureScale.onTouchEvent(event);
        return true;
    }   

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        scaleFactor *= detector.getScaleFactor();
        scaleFactor = (scaleFactor < 1 ? 1 : scaleFactor); // prevent our view from becoming too small //
        scaleFactor = ((float)((int)(scaleFactor * 100))) / 100; // Change precision to help with jitter when user just rests their fingers //
        view.setScaleX(scaleFactor);
        view.setScaleY(scaleFactor);
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        inScale = true;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) { inScale = false; }
}

Then assign it as your View's OnTouchListener like so.

myView.setOnTouchListener(new MyScaleGestures(context));

If you want to add a scrolling ability to the View you will need to implement onScroll from the OnGestureListener interface. You can add this override to the MyScaleGestures class to accomplish this.

@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2, float x, float y) {
    float newX = view.getX();
    float newY = view.getY();
    if(!inScale){
        newX -= x;
        newY -= y;
    }
    WindowManager wm = (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE);
    Display d = wm.getDefaultDisplay();
    Point p = new Point();
    d.getSize(p);

    if (newX > (view.getWidth() * scaleFactor - p.x) / 2){
        newX = (view.getWidth() * scaleFactor - p.x) / 2;
    } else if (newX < -((view.getWidth() * scaleFactor - p.x) / 2)){
        newX = -((view.getWidth() * scaleFactor - p.x) / 2);
    }

    if (newY > (view.getHeight() * scaleFactor - p.y) / 2){
        newY = (view.getHeight() * scaleFactor - p.y) / 2;
    } else if (newY < -((view.getHeight() * scaleFactor - p.y) / 2)){
        newY = -((view.getHeight() * scaleFactor - p.y) / 2);
    }

    view.setX(newX);
    view.setY(newY);

    return true;
}

The end result of doing all of the above will give you class like this one:

public class StandardGestures implements OnTouchListener, OnGestureListener, OnDoubleTapListener, OnScaleGestureListener {
    private View view;
    private GestureDetector gesture;
    private ScaleGestureDetector gestureScale;
    private float scaleFactor = 1;
    private boolean inScale;

    public StandardGestures(Context c){
        gesture = new GestureDetector(c, this);
        gestureScale = new ScaleGestureDetector(c, this);
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        this.view = view;
        gesture.onTouchEvent(event);
        gestureScale.onTouchEvent(event);
        return true;
    }

    @Override
    public boolean onDown(MotionEvent event) {
        return true;
    }

    @Override
    public boolean onFling(MotionEvent event1, MotionEvent event2, float x, float y) {
        return true;
    }

    @Override
    public void onLongPress(MotionEvent event) {
    }

    @Override
    public boolean onScroll(MotionEvent event1, MotionEvent event2, float x, float y) {
        float newX = view.getX();
        float newY = view.getY();
        if(!inScale){
            newX -= x;
            newY -= y;
        }
        WindowManager wm = (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE);
        Display d = wm.getDefaultDisplay();
        Point p = new Point();
        d.getSize(p);

        if (newX > (view.getWidth() * scaleFactor - p.x) / 2){
            newX = (view.getWidth() * scaleFactor - p.x) / 2;
        } else if (newX < -((view.getWidth() * scaleFactor - p.x) / 2)){
            newX = -((view.getWidth() * scaleFactor - p.x) / 2);
        }

        if (newY > (view.getHeight() * scaleFactor - p.y) / 2){
            newY = (view.getHeight() * scaleFactor - p.y) / 2;
        } else if (newY < -((view.getHeight() * scaleFactor - p.y) / 2)){
            newY = -((view.getHeight() * scaleFactor - p.y) / 2);
        }

        view.setX(newX);
        view.setY(newY);

        return true;
    }

    @Override
    public void onShowPress(MotionEvent event) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent event) {
        return true;
    }

    @Override
    public boolean onDoubleTap(MotionEvent event) {
        view.setVisibility(View.GONE);
        return true;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent event) {
        return true;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent event) {
        return true;
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {

        scaleFactor *= detector.getScaleFactor();
        scaleFactor = scaleFactor < 1 ? 1 : scaleFactor; // prevent our image from becoming too small
        scaleFactor = (float) (int) (scaleFactor * 100) / 100; // Change precision to help with jitter when user just rests their fingers //
        view.setScaleX(scaleFactor);
        view.setScaleY(scaleFactor);
        onScroll(null, null, 0, 0); // call scroll to make sure our bounds are still ok //
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        inScale = true;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        inScale = false;
        onScroll(null, null, 0, 0); // call scroll to make sure our bounds are still ok //
    }
}
Chris Stillwell
  • 10,266
  • 10
  • 67
  • 77
  • 1
    This is a great answer, since the code does not involve creating a new class deriving from `ImageView`. Unfortunately, the constuctor and the class name do not match, the boolean inScale is missing and do only have meaning if the `onScroll()` method is present. Bur more importantly than all that above, the `onScroll()`method does not override anything (should the class derive from another listener?), and thus does not work. Or maybe I'm missing something big. Can this answer be improved? – Baltasarq Oct 18 '18 at 13:17
  • 1
    @Baltasarq Thanks for pointing those out. The class in the answer is using code from a much larger class I use. As you discovered I cut out a little too much trying to make it apply to the answer. I've updated my answer to correct the errors you found. Thanks for the help in catching them and helping me improve it. – Chris Stillwell Oct 18 '18 at 14:34
  • thank you for the quick response and fix. Another issue, I think it is possible to substitute `inScale` by `this.gestureScale.isInProgress()`. Is that true or is it better to use maintain your own boolean? – Baltasarq Oct 19 '18 at 08:01
  • And yet another issue. By implementing `GestureDector.OnGestureListener`, I had to provide `onLongPress()`, `onDown()`, `onFling()`, `onShowPress()` and `onSingleTapUp()`. – Baltasarq Oct 19 '18 at 08:33
  • Okay, scroll does not work. I guess I have to set up another listener in the image or in the `ScaleGestureDetector`... – Baltasarq Oct 19 '18 at 14:15
  • 1
    @Baltasarq, I'm not sure if this will help or not, but it might be worth trying. In the `onDown()`, `onFling()` and `onSingleTapUp()` I am returning `true` and I have a comment in my code that says returning `true` is was required to not block input in those events. I've attached the full class to my answer, in case there is something else I missed. – Chris Stillwell Oct 19 '18 at 15:04
  • Thanks, will try and let you know. – Baltasarq Oct 19 '18 at 19:10
  • Nope, sorry, it did not work. I've made it work by managing the appropriate messages in the `onTouch()`. – Baltasarq Oct 21 '18 at 20:29
28

You can use this

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;

public class MyImageView extends View {

private static final int INVALID_POINTER_ID = -1;

    private Drawable mImage;
    private float mPosX;
    private float mPosY;

    private float mLastTouchX;
    private float mLastTouchY;
    private int mActivePointerId = INVALID_POINTER_ID;

    private ScaleGestureDetector mScaleDetector;
    private float mScaleFactor = 1.f;

    public MyImageView(Context context) {
        this(context, null, 0);
    mImage=act.getResources().getDrawable(context.getResources().getIdentifier("imag­ename", "drawable", "packagename"));

        mImage.setBounds(0, 0, mImage.getIntrinsicWidth(), mImage.getIntrinsicHeight());
    }

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

    public MyImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Let the ScaleGestureDetector inspect all events.
        mScaleDetector.onTouchEvent(ev);

        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
            final float x = ev.getX();
            final float y = ev.getY();

            mLastTouchX = x;
            mLastTouchY = y;
            mActivePointerId = ev.getPointerId(0);
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            final int pointerIndex = ev.findPointerIndex(mActivePointerId);
            final float x = ev.getX(pointerIndex);
            final float y = ev.getY(pointerIndex);

            // Only move if the ScaleGestureDetector isn't processing a gesture.
            if (!mScaleDetector.isInProgress()) {
                final float dx = x - mLastTouchX;
                final float dy = y - mLastTouchY;

                mPosX += dx;
                mPosY += dy;

                invalidate();
            }

            mLastTouchX = x;
            mLastTouchY = y;

            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);
            }
            break;
        }
        }

        return true;
    }

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

        canvas.save();
        Log.d("DEBUG", "X: "+mPosX+" Y: "+mPosY);
        canvas.translate(mPosX, mPosY);
        canvas.scale(mScaleFactor, mScaleFactor);
        mImage.draw(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;
        }
    }

}

to call this in your activity.setContentView(new MyImageView(this));

Flexo
  • 87,323
  • 22
  • 191
  • 272
10

ScaleGestureDetector is available starting in Android 2.2 (aka Froyo, API level 8). See: http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html

In 2.0/2.1, you don't have ScaleGestureDetector, but you can provide pinch-to-zoom using the ZDNet blog entry by Ed Burnette that Pieter888 linked to above: http://www.zdnet.com/blog/burnette/how-to-use-multi-touch-in-android-2-part-6-implementing-the-pinch-zoom-gesture/1847

Dalbergia
  • 1,699
  • 1
  • 17
  • 26
3

actually there is a library that uses this class just for the zooming of images.

it's called "TouchImageView"

Dmitry Zaytsev
  • 23,650
  • 14
  • 92
  • 146
android developer
  • 114,585
  • 152
  • 739
  • 1,270
3

TouchImageView

public class TouchImageView extends ImageView {
    Matrix matrix;
    // We can be in one of these 3 states
    static final int NONE = 0;
    static final int DRAG = 1;
    static final int ZOOM = 2;

    int mode = NONE;

    // Remember some things for zooming
    PointF last = new PointF();
    PointF start = new PointF();
    float minScale = 1f;
    float maxScale = 3f;
    float[] m;
    int viewWidth, viewHeight;

    static final int CLICK = 3;

    float saveScale = 1f;

    protected float origWidth, origHeight;

    int oldMeasuredWidth, oldMeasuredHeight;

    ScaleGestureDetector mScaleDetector;

    Context context;

    public TouchImageView(Context context) {
        super(context);
        sharedConstructing(context);
    }

    public TouchImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        sharedConstructing(context);
    }

    private void sharedConstructing(Context context) {

        super.setClickable(true);

        this.context = context;

        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());

        matrix = new Matrix();

        m = new float[9];

        setImageMatrix(matrix);

        setScaleType(ScaleType.MATRIX);

        setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {

                mScaleDetector.onTouchEvent(event);

                PointF curr = new PointF(event.getX(), event.getY());

                switch (event.getAction()) {

                    case MotionEvent.ACTION_DOWN:

                        last.set(curr);

                        start.set(last);

                        mode = DRAG;

                        break;

                    case MotionEvent.ACTION_MOVE:

                        if (mode == DRAG) {

                            float deltaX = curr.x - last.x;

                            float deltaY = curr.y - last.y;

                            float fixTransX = getFixDragTrans(deltaX, viewWidth, origWidth * saveScale);

                            float fixTransY = getFixDragTrans(deltaY, viewHeight, origHeight * saveScale);

                            matrix.postTranslate(fixTransX, fixTransY);

                            fixTrans();

                            last.set(curr.x, curr.y);

                        }

                        break;

                    case MotionEvent.ACTION_UP:

                        mode = NONE;

                        int xDiff = (int) Math.abs(curr.x - start.x);

                        int yDiff = (int) Math.abs(curr.y - start.y);

                        if (xDiff < CLICK && yDiff < CLICK)

                            performClick();

                        break;

                    case MotionEvent.ACTION_POINTER_UP:

                        mode = NONE;

                        break;

                }

                setImageMatrix(matrix);

                invalidate();

                return true; // indicate event was handled
            }
        });
    }

    public void setMaxZoom(float x) {

        maxScale = x;
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {

            mode = ZOOM;

            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {

            float mScaleFactor = detector.getScaleFactor();

            float origScale = saveScale;

            saveScale *= mScaleFactor;

            if (saveScale > maxScale) {

                saveScale = maxScale;

                mScaleFactor = maxScale / origScale;

            } else if (saveScale < minScale) {

                saveScale = minScale;

                mScaleFactor = minScale / origScale;

            }

            if (origWidth * saveScale <= viewWidth || origHeight * saveScale <= viewHeight)

                matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2, viewHeight / 2);

            else

                matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY());

            fixTrans();

            return true;
        }
    }

    void fixTrans() {

        matrix.getValues(m);

        float transX = m[Matrix.MTRANS_X];

        float transY = m[Matrix.MTRANS_Y];

        float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale);

        float fixTransY = getFixTrans(transY, viewHeight, origHeight * saveScale);

        if (fixTransX != 0 || fixTransY != 0)

            matrix.postTranslate(fixTransX, fixTransY);
    }

    float getFixTrans(float trans, float viewSize, float contentSize) {

        float minTrans, maxTrans;

        if (contentSize <= viewSize) {

            minTrans = 0;

            maxTrans = viewSize - contentSize;

        } else {

            minTrans = viewSize - contentSize;

            maxTrans = 0;

        }

        if (trans < minTrans)

            return -trans + minTrans;

        if (trans > maxTrans)

            return -trans + maxTrans;

        return 0;
    }

    float getFixDragTrans(float delta, float viewSize, float contentSize) {

        if (contentSize <= viewSize) {

            return 0;

        }

        return delta;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        viewWidth = MeasureSpec.getSize(widthMeasureSpec);

        viewHeight = MeasureSpec.getSize(heightMeasureSpec);

        //
        // Rescales image on rotation
        //
        if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight

                || viewWidth == 0 || viewHeight == 0)

            return;

        oldMeasuredHeight = viewHeight;

        oldMeasuredWidth = viewWidth;

        if (saveScale == 1) {

            //Fit to screen.

            float scale;

            Drawable drawable = getDrawable();

            if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0)

                return;

            int bmWidth = drawable.getIntrinsicWidth();

            int bmHeight = drawable.getIntrinsicHeight();

            Log.e("bmSize", "bmWidth: " + bmWidth + " bmHeight : " + bmHeight);

            float scaleX = (float) viewWidth / (float) bmWidth;

            float scaleY = (float) viewHeight / (float) bmHeight;

            scale = Math.min(scaleX, scaleY);

            matrix.setScale(scale, scale);

            // Center the image

            float redundantYSpace = (float) viewHeight - (scale * (float) bmHeight);

            float redundantXSpace = (float) viewWidth - (scale * (float) bmWidth);

            redundantYSpace /= (float) 2;

            redundantXSpace /= (float) 2;

            matrix.postTranslate(redundantXSpace, redundantYSpace);

            origWidth = viewWidth - 2 * redundantXSpace;

            origHeight = viewHeight - 2 * redundantYSpace;

            setImageMatrix(matrix);

        }

        fixTrans();
    }
}

MainActivity.java

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);


        TouchImageView img = new TouchImageView(this);
        img.setImageResource(R.drawable.ic_launcher);
        img.setMaxZoom(4f);
        setContentView(img);
    }
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Maradiya Krupa
  • 213
  • 1
  • 6
1

yes we can here is the sample code where onPinch() and onZoom() are actions to be implement on your own

public class simpleOnScaleGestureListener extends
        SimpleOnScaleGestureListener {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        startScale = detector.getScaleFactor();
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        endScale = detector.getScaleFactor();



        if (startScale > endScale) {
            Log.i("onScaleEnd", "Pinch Dection");
            onPinch();
        } else if (startScale < endScale) {
            Log.i("onScaleEnd", "Zoom Dection");
            onZoom();
        }


    }

}
AMD
  • 1,662
  • 18
  • 39
  • 2
    I think you're leaving out way too much for this answer to be useful. This is only your ScaleGestureListener, what about the rest? – Simon Forsberg Jun 23 '13 at 12:49
  • i have called onpinch() and onzoom() which has implementation of pinchzoom on my customeview you can implement your own – AMD Jun 24 '13 at 06:02