ZoomLayout icon indicating copy to clipboard operation
ZoomLayout copied to clipboard

Double tap to zoom

Open natario1 opened this issue 8 years ago • 13 comments

It's common to react to double taps to control zoom. This should be a opt-in feature in ZoomEngine, enabled by default in ZoomImageView.

natario1 avatar Sep 28 '17 23:09 natario1

@natario1

For now, I have just implemented this functionality by my self with custom Gesture Listener class. Here is my code snippet if anyone need it.

private GestureDetector gestureDetector; 
private View.OnTouchListener touchListener;
private ZoomImageView selectedImage;

then initialize these variables:

    gestureDetector = new GestureDetector(mContext, new MyGestureListener());

        touchListener = new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // pass the events to the gesture detector
                // a return value of true means the detector is handling it
                // a return value of false means the detector didn't
                // recognize the event
                selectedImage = (ZoomImageView) v;
                return gestureDetector.onTouchEvent(event);

            }
        };

then assign your touchlistener to zoomimageview object:

    ZoomImageView image = layout.findViewById(R.id.imageViewItemImageSlider);
    image.setOnTouchListener(touchListener);

and here is MyGestureListener class:

    class MyGestureListener extends GestureDetector.SimpleOnGestureListener {

//        @Override
//        public boolean onDown(MotionEvent event) {
//            Log.d("TAG","onDown: ");
//
//            // don't return false here or else none of the other
//            // gestures will work
//            return true;
//        }
//
//        @Override
//        public boolean onSingleTapConfirmed(MotionEvent e) {
//            Log.i("TAG", "onSingleTapConfirmed: ");
//            return true;
//        }
//
//        @Override
//        public void onLongPress(MotionEvent e) {
//            Log.i("TAG", "onLongPress: ");
//        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            Log.i("TAG", "onDoubleTap: ");
            if((selectedImage.getEngine().getZoom() >= 2.75F)) {
                selectedImage.getEngine().zoomTo(1F, true);
            } else if((selectedImage.getEngine().getZoom() < 1F)) {
                selectedImage.getEngine().zoomTo(1F, true);
            }  else {
                selectedImage.getEngine().zoomBy(2F, true);
            }

            return true;
        }

//        @Override
//        public boolean onScroll(MotionEvent e1, MotionEvent e2,
//                                float distanceX, float distanceY) {
//            Log.i("TAG", "onScroll: ");
//            return true;
//        }

//        @Override
//        public boolean onFling(MotionEvent event1, MotionEvent event2,
//                               float velocityX, float velocityY) {
//            Log.d("TAG", "onFling: ");
//            return true;
//        }
    }

Please do on override onScroll (MUST) and other method (Optional in my case) which you don't have anything to deal with as it will conflict with ZoomImageView's touch event and create an issue.

Hope this will help you.

HardikMaru avatar Feb 20 '18 11:02 HardikMaru

I'm currently reacting to double-tap to center the screen where the event was done.

Simply by adding this:

    private val simpleGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {


        //here is the method for double tap


        override fun onDoubleTap(e: MotionEvent): Boolean {

            Log.d("OnDoubleTapListener", "onDoubleTap")
            centerElement(e.x.absoluteValue,e.y.absoluteValue)
            return true
        }

    })

    fun centerElement(clickedX: Float, clickedY: Float) {
        val offsetX = (width.absoluteValue) / 2
        val offsetY = (height.absoluteValue) / 2
        val displacedX = engine.panX.absoluteValue
        val displacedY = engine.panY.absoluteValue
        val x = (displacedX + clickedX / engine.realZoom) - (offsetX / engine.realZoom)
        val y = (displacedY + clickedY / engine.realZoom) - (offsetY / engine.realZoom)
        val desiredX = if (x > 0) -x else 0f
        val desiredY = if (y > 0) -y else 0f
        engine.moveTo(engine.zoom, desiredX, desiredY, true)
    }

Now I'm trying to move and zoom correctly. I can open a pull request if you want to make it work.

AlvaroFalcon avatar May 21 '18 12:05 AlvaroFalcon

@AlvaroFalcon that would be cool! I can give you some tips.

  • We already have a gesture detector in ZoomEngine that reacts to scroll and fling
  • I think you can reuse 99% of the logic inside onScale(). It works with coordinates of the center of the scale gesture and applies a zoom factor. You would have to assign a zoom factor yourself (I think zoomIn() uses 1.3) but it's pretty much the same task
  • This should be configurable through an XML attribute (doubleTapBehavior?) which should have at least two options ("none" and "zoom"). Let's use none as a default so we don't change this for who is already using the lib.

natario1 avatar May 21 '18 12:05 natario1

@natario1 Cool! I'll do it when I have some time...

I'm currently working in my project with a class that extends your zoomlayout so I add my logic there. I'm trying to make zoom with movement at the same time using that, but no luck for the moment.

Thanks for the tips!

AlvaroFalcon avatar May 21 '18 12:05 AlvaroFalcon

Could an action be added in the moveTo() method? To be called after it finishes, I think it would be more flexible that way so you can do multiple things.

Well, to moveTo, zoomBy, zoomTo, etc...

It could be like:

moveTo(zoom, x, y, action : ()->Unit={})

AlvaroFalcon avatar May 21 '18 14:05 AlvaroFalcon

@AlvaroFalcon In Kotlin I would do that but this is still pure Java. We will move to Kotlin in the future though. For now, there is onIdle() which anyone can access.

natario1 avatar May 21 '18 14:05 natario1

@AlvaroFalcon were you able to achieve zoom and pan at the same time? mind sharing your code?

msamyatl avatar May 01 '19 04:05 msamyatl

I have implemented both double tap and pinch to zoom. The key to the working solution is to attach tap/double tap gesture detector to the content view inside zoom layout and configure zoom layout to have clickable children to true. Then in the onTouchEvent of the content view return true to consume touch events and pass them to the gesture detector. That way both pinch, zoom, and double tap work at the same time. Zooming in on double tap is no problem then.

However, I have also tried to zoom to the area that was double tapped, and given how ZoomEngine.moveTo is implemented, the simultaneous interpolation of zoom and pan does not behave correctly and the image is sliding around different paths to reach the destination zoom and pan. It does not look visually pleasing, yet I'm unable to figure out what is happening. I think the interpolation algorithm should differently compute the actual pan based on what the actual zoom is, not just fraction of the animation.

mman avatar May 12 '19 18:05 mman

zoom to the area that was double tapped

Yes, that's exactly what I've been trying to do. Any suggestions as to how to do this @natario1?

msamyatl avatar May 13 '19 21:05 msamyatl

Hi @mman

We have also implemented a way to automatically zoom when the user taps the screen and the animation for zooming in kind of swirling toward the end position.

It would be very good, if we could control this animation - do you or others have suggestions for how to go about that?

Thanks in advance :)

nikfalstie avatar May 29 '19 12:05 nikfalstie

I managed to do it with a dirty hack. I am tricking the zoom engine into thinking that a pinch is taking place.

class ReflectionHelper {
    @Nullable
    static ScaleGestureDetector.OnScaleGestureListener getScaleDetectorListenerUsingReflection(ZoomEngine zoomEngine) {
        try {
            Field mScaleDetector = zoomEngine.getClass().getDeclaredField("mScaleDetector");
            mScaleDetector.setAccessible(true);
            ScaleGestureDetector detector = (ScaleGestureDetector) mScaleDetector.get(zoomEngine);
            Field mListener = detector.getClass().getDeclaredField("mListener");
            mListener.setAccessible(true);
            return (ScaleGestureDetector.OnScaleGestureListener) mListener.get(detector);
        } catch (Exception e) {
            return null;
        }
    }
}

private fun simulatePinch(fromScale: Float, toScale: Float, focusX: Float, focusY: Float) {
    class MockDetector(var mockScaleFactor: Float, var mockFocusX: Float, var mockFocusY: Float) : ScaleGestureDetector(context, null) {
        override fun getScaleFactor() = mockScaleFactor
        override fun getFocusX() = mockFocusX
        override fun getFocusY() = mockFocusY
    }

    val mockDetector = MockDetector(fromScale, focusX, focusY)
    ValueAnimator.ofFloat(fromScale, toScale).apply {
        addUpdateListener { animation ->
            mockDetector.mockScaleFactor = animation.animatedValue as Float
            zoomEngineScaleListener?.onScale(mockDetector)
        }
        doOnEnd { zoomEngineScaleListener?.onScaleEnd(mockDetector) }
        start()
    }
}

msamyatl avatar Jun 06 '19 03:06 msamyatl

Hi,

Thanks for the effort! :)

That definitely seems like a bit of a hacky way of achieving it. I think we will however choose to live with the very animating animation for now.

Thanks, Nikolaj

nikfalstie avatar Aug 01 '19 14:08 nikfalstie

Hi,

Any news on how to achieve zooming + panning on a double tap?

Thanks, Christophe

cdongieux avatar Nov 03 '20 07:11 cdongieux