Zoomlayout: Double tap to zoom

Created on 29 Sep 2017  ·  13Comments  ·  Source: natario1/ZoomLayout

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.

enhancement long term

Most helpful comment

Hi,

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

Thanks,
Christophe

All 13 comments

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

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 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 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!

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

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

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.

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?

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 :)

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()
    }
}

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

Hi,

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

Thanks,
Christophe

Was this page helpful?
0 / 5 - 0 ratings