2

In an Android photo viewing app, I want to allow users to:

  • Zoom into a photo, drag to see its details, unzoom.
  • Swipe to go to the next photo.

Implementation attempt using ZoomableDraweeView in Facebook's Fresco library:

private fun init(imageUri: Uri?) {
    val hierarchy = GenericDraweeHierarchyBuilder.newInstance(resources)
        .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
        .setProgressBarImage(ProgressBarDrawable())
        .setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
        .build()
    zoomableDraweeView!!.hierarchy = hierarchy
    zoomableDraweeView!!.setAllowTouchInterceptionWhileZoomed(false)
    zoomableDraweeView!!.setIsLongpressEnabled(false)
    zoomableDraweeView!!.setTapListener(DoubleTapGestureListener(zoomableDraweeView))
    val controller: DraweeController = Fresco.newDraweeControllerBuilder()
        .setUri(imageUri)
        .setControllerListener(loadingListener)
        .build()
    zoomableDraweeView!!.controller = controller

Problem: When I zoom in, lift the fingers, then try to unzoom, this gets misinterpreted as a swipe and I am randomly sent to the next picture.

What am I doing wrong? How to disable swipes when zoomed in (or any better UX solution)?

  • I specifically call setAllowTouchInterceptionWhileZoomed(false), whose javadoc says: "If this is set to true, parent views can intercept touch events while the view is zoomed. For example, this can be used to swipe between images in a view pager while zoomed."
  • I tried to execute the swipe action only when zoomableDraweeView.getZoomableController().isIdentity() is false, but that does not always prevent unintended swipe. In particular, when I fully zoom out, often swipe accidentally happens, maybe because isIdentity() has been updated by the time I release all fingers. getScaleFactor() has the same issue. Another issue is that this solution only allows me to drag the zoomed picture with two fingers, dragging with one finger has no effect.
  • I thought about writing my own DraweeController and surface the zoom level (and ignore swipes when zoomed) but the base classes do not seem to contain any zoom level information.

By the way, here is how I detect swipes:

open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
    private inner class GestureListener :
                        GestureDetector.SimpleOnGestureListener() {
        override fun onFling(
            event1: MotionEvent,
            event2: MotionEvent,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            try {
                val diffX: Float = event2.x - event1.x
                if (abs(diffX) > abs(diffY)) {
                    if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) >
                        SWIPE_VELOCITY_THRESHOLD) {
                        if (diffX > 0) {
                            goToTheNextPhoto() // Swipe detected.

(full code on GitHub if needed)

Nicolas Raoul
  • 58,567
  • 58
  • 222
  • 373

1 Answers1

1

The issue is not in the Fresco library or in the ZoomableDraweeView. The problem is in the orchestrating of the overall UX in your activity. It is not hard though, and you were on the right track with the zoomableDraweeView.getZoomableController().isIdentity() and setAllowTouchInterceptionWhileZoomed.

There are several ways to overcome it, starting from the custom ZoomableController and ending by exchanging your swipes detector with a proper ViewPager2(RecyclerView based)(or even two as you have vertical and horizontal swipes) with snapping and its own callbacks and animations. But the solution will be similar in all the cases - using everything where(and when) it is supposed to be used. It may require a bit of refactoring of your overall approach.

The first way to fix it is via setAllowTouchInterceptionWhileZoomed, which is not working for you because you apply your swipe detector directly to the ZoomableDraweeView rather than to its parent. Thus your view parent cannot handle swipes because you told it not to via setAllowTouchInterceptionWhileZoomed, but your ZoomableDraweeView can, and it does. Thus making you ZoomableDraweeView parent handle swipes rather than the view itself should do the trick.

Secondly, the ZoomableController is AnimatedZoomableController thus, it performs animation, and the animation has a duration(in this case, it depends on the pinch gesture velocity) - in the case when you were zooming out, and your swipe detector was changing the image - the animation was still ongoing, but the transformation matrix was already back to identity - thus the issue.

To fix this, you have to consider animation duration. I think even a simple 200ms delay should fix it in most cases. Also, I would suggest having an onTransformChanged method of the ZoomableDraweeView view(making a custom view) overridden, checking for the identity matrix in it, and exposing it to the activity in some way - this way, you will be sure that the transformation has already happened(the animation delay is still relevant in this case), and you can easily enable your own swipe detection.

The first method is preferable and more "clean," and the second one is "hacky," but its minimal implementation should be very fast.

I haven't tried to refactor it so I assume some additional work may be required.

Hope it helps.

Pavlo Ostasha
  • 14,527
  • 11
  • 35