oblador / react-native-pinchable

Instagram like pinch to zoom for React Native
MIT License
224 stars 23 forks source link

Disable background touch events, until images gets settled to its original position. #5

Open siddharth-kt opened 2 years ago

siddharth-kt commented 2 years ago

Great library !

It would be great if background touch/scroll etc.. events can be stopped until image gets settled down to its original position. Because in few case I have found that user tries to scroll away fast (till that time image was not settled properly to its actual position) and since the actual position of that image gets lost, that image get stuck on the screen. Then user needs to restart the app.

Note : I also confirm this issue exists on android

siddharth-kt commented 2 years ago

@oblador Kindly sort this issue, its getting worse on pressing the key which navigates to another screen. Image gets stuck on the screen and app needs restart to be used again.

JayantJoseph commented 2 years ago

Facing the same issue. Can confirm issue is present in android devices.

I have used the Pinchable component like the example below:

<FlatList
     data={[
       'https://thumbs.dreamstime.com/z/random-square-multicolor-pattern-24853666.jpg',
       'https://cdn4.vectorstock.com/i/1000x1000/51/38/random-square-pattern-seamless-background-vector-25695138.jpg',
       'https://c8.alamy.com/comp/K0D99R/square-background-random-black-and-white-colored-abstract-digital-K0D99R.jpg',
       'https://picsum.photos/800'
     ]}
     renderItem={({ item }) => (
      <Pinchable>
         <Image
           resizeMode="cover"
           source={{
             uri: item
           }}
           style={{
                height: dimension.screenWidth,
                width: dimension.screenWidth
}}
         />
       </Pinchable>
     )}
     ItemSeparatorComponent={() => (<View style={{ width: '100%', height: 10, backgroundColor: 'red' }} />)}
   />

Issue screen record given below

https://user-images.githubusercontent.com/38125115/139681820-6363eefa-27a2-4cac-aad0-5463629b6aa9.mp4

From the moment the issue happens, the image will be present there even after navigating to other pages. Only by closing the app completely the image is dismissed. @oblador

siddharth-kt commented 2 years ago

Yeah same

siddharth-kt commented 2 years ago

@oblador kindly reply!

siddharth-kt commented 2 years ago

@Dein1 ??

JayantJoseph commented 2 years ago

@oblador is there any updates? is there any possibility to get a prop to know if pinch is happening, so we can disable the scroll looking into it, or something like that

siddharth-kt commented 2 years ago

@oblador ?

davidmrp commented 2 years ago

Same issue here!

FelipeSSantos1 commented 2 years ago

Are you using the library with something like react-native-pager-view lib as showed here?

I am using it, and that issue happen just if I move line 10 to after the line 12 inside of component, if I declare it as the example shows, declaring AnimatedPagerView outside of component it works like a charm.

hristowwe commented 2 years ago

Are you using the library with something like react-native-pager-view lib as showed here?

I am using it, and that issue happen just if I move line 10 to after the line 12 inside of component, if I declare it as the example shows, declaring AnimatedPagerView outside of component it works like a charm.

Hello, can you show simple example?

pierroo commented 1 year ago

Has there been any progress on this issue?

frodriguez-hu commented 1 year ago

When the animation start I am trying to turn off the user interactions on the activity with: ctx.getCurrentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);

It works good in most cases but when I keep just one finger it starts to move things from the activity that should be blocked, I am trying to check what is going on

frodriguez-hu commented 1 year ago

This patch fix the issue:

diff --git a/node_modules/react-native-pinchable/android/src/main/java/com/oblador/pinchable/PinchableView.java b/node_modules/react-native-pinchable/android/src/main/java/com/oblador/pinchable/PinchableView.java
index 9f22074..841967c 100644
--- a/node_modules/react-native-pinchable/android/src/main/java/com/oblador/pinchable/PinchableView.java
+++ b/node_modules/react-native-pinchable/android/src/main/java/com/oblador/pinchable/PinchableView.java
@@ -8,6 +8,8 @@ import android.graphics.drawable.BitmapDrawable;
 import android.graphics.Color;
 import android.graphics.PointF;
 import android.graphics.Bitmap;
+import android.os.Handler;
+import android.os.Looper;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnTouchListener;
@@ -15,8 +17,11 @@ import android.view.View.OnTouchListener;
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.view.ViewParent;
+import android.view.WindowManager;
 import android.view.animation.DecelerateInterpolator;

+import com.facebook.react.uimanager.ThemedReactContext;
 import com.facebook.react.views.view.ReactViewGroup;

 public class PinchableView extends ReactViewGroup implements OnTouchListener {
@@ -33,9 +38,10 @@ public class PinchableView extends ReactViewGroup implements OnTouchListener {
     private ValueAnimator currentAnimator = null;
     private ColorDrawable backdrop = null;
     private BitmapDrawable clone = null;
-
+    private ThemedReactContext ctx = null;
     public PinchableView(Context context) {
         super(context);
+        this.ctx = (ThemedReactContext) context;
         this.setOnTouchListener(this);
     }

@@ -102,6 +108,8 @@ public class PinchableView extends ReactViewGroup implements OnTouchListener {
     }

     private void startGesture(View v, MotionEvent event) {
+        ctx.getCurrentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
+                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
         if (currentAnimator != null) {
             currentAnimator.cancel();
         }
@@ -155,14 +163,12 @@ public class PinchableView extends ReactViewGroup implements OnTouchListener {

     private void endGesture(MotionEvent event) {
         active = false;
-        this.getParent().requestDisallowInterceptTouchEvent(false);
         if (currentAnimator != null) {
             currentAnimator.cancel();
         }
-
         ValueAnimator animator = ValueAnimator.ofFloat(1, 0);
         animator.setDuration(animationDuration);
-        animator.setInterpolator(new DecelerateInterpolator(1.5f));
+        animator.setInterpolator(new DecelerateInterpolator(5f));
         animator.addUpdateListener(updatedAnimation -> {
             float animatedValue = (float)updatedAnimation.getAnimatedValue();
             setCloneTransforms(translation.x * animatedValue, translation.y * animatedValue, 1 + (scale - 1) * animatedValue);
@@ -171,13 +177,22 @@ public class PinchableView extends ReactViewGroup implements OnTouchListener {
         animator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
                 endAnimation();
             }

             @Override
             public void onAnimationCancel(Animator animation) {
+                super.onAnimationCancel(animation);
                 endAnimation();
             }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                super.onAnimationStart(animation);
+                ctx.getCurrentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
+                        WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
+            }
         });
         animator.start();
         currentAnimator = animator;
@@ -195,6 +210,15 @@ public class PinchableView extends ReactViewGroup implements OnTouchListener {
             clone = null;
         }
         setVisibility(View.VISIBLE);
+        ViewParent parent = this.getParent();
+        final Handler handler = new Handler(Looper.getMainLooper());
+        handler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                ctx.getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
+                parent.requestDisallowInterceptTouchEvent(false);
+            }
+        }, 100);
     }

     public void setMinimumZoomScale(float minimumZoomScale) {

This is the entire PinchableView.java File:

package com.oblador.pinchable;

import java.lang.Math;

import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.view.ViewParent;
import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator;

import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.view.ReactViewGroup;

public class PinchableView extends ReactViewGroup implements OnTouchListener {
    private final int animationDuration = 400;
    private float minScale = 1f;
    private float maxScale = 3f;
    private boolean active = false;
    private int sourceLocation[] = new int[2];
    private int sourceDimensions[] = new int[2];
    private PointF initialTouchPoint = new PointF();
    private float initialSpacing = 0f;
    private PointF translation = new PointF();
    private float scale = 1;
    private ValueAnimator currentAnimator = null;
    private ColorDrawable backdrop = null;
    private BitmapDrawable clone = null;
    private ThemedReactContext ctx = null;
    public PinchableView(Context context) {
        super(context);
        this.ctx = (ThemedReactContext) context;
        this.setOnTouchListener(this);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // Block touch events on children
        return true;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_POINTER_DOWN:
                if (!active && event.getPointerCount() >= 2) {
                    startGesture(v, event);
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                if (active && event.getPointerCount() < 2) {
                    endGesture(event);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (active) {
                    if (event.getPointerCount() < 2) {
                        endGesture(event);
                    } else {
                        moveGesture(event);
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (active) {
                    endGesture(event);
                }
                break;
            default:
                break;
        }

        return true;
    }

    private float spacing(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);
    }

    private void midPoint(PointF point, MotionEvent event)  {
        float x = event.getX(0) + event.getX(1);
        float y = event.getY(0) + event.getY(1);
        point.set(x / 2, y / 2);
    }

    private void setCloneTransforms(float translateX, float translateY, float scale) {
        int x = (int) (sourceLocation[0] + translateX - sourceDimensions[0] * (scale - 1) / 2);
        int y = (int) (sourceLocation[1] + translateY - sourceDimensions[1] * (scale - 1) / 2);
        int width = (int) (sourceDimensions[0] * scale);
        int height = (int) (sourceDimensions[1] * scale);
        clone.setBounds(x, y, x + width, y + height);
        backdrop.setAlpha((int) (255 * Math.min(scale - 1, 0.7)));
    }

    private void startGesture(View v, MotionEvent event) {
        ctx.getCurrentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
        if (currentAnimator != null) {
            currentAnimator.cancel();
        }
        View rootView = this.getRootView();
        if (clone != null) {
            rootView.getOverlay().remove(clone);
            clone = null;
        }
        Bitmap snapshot = null;
        v.setDrawingCacheEnabled(true);
        v.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_AUTO);
        try {
            snapshot = Bitmap.createBitmap(v.getDrawingCache(true));
            clone = new BitmapDrawable(this.getContext().getResources(), snapshot);
        } catch(Exception ex) {
            ex.printStackTrace();
            return;
        }

        active = true;

        if (backdrop == null) {
            backdrop = new ColorDrawable(Color.BLACK);
            backdrop.setAlpha(0);
            backdrop.setBounds(0, 0, rootView.getWidth(), rootView.getHeight());
            rootView.getOverlay().add(backdrop);
        }

        v.getLocationInWindow(sourceLocation);
        sourceDimensions[0] = v.getWidth();
        sourceDimensions[1] = v.getHeight();
        setCloneTransforms(0, 0, 1);
        rootView.getOverlay().add(clone);
        setVisibility(View.INVISIBLE);
        midPoint(initialTouchPoint, event);
        initialSpacing = spacing(event);
        translation.set(0, 0);
        scale = 1;
        this.getParent().requestDisallowInterceptTouchEvent(true);
    }

    private void moveGesture(MotionEvent event) {
        PointF current = new PointF();
        midPoint(current, event);
        float deltaX = current.x - initialTouchPoint.x;
        float deltaY = current.y - initialTouchPoint.y;
        scale = Math.min(maxScale, Math.max(minScale, spacing(event) / initialSpacing));
        setCloneTransforms(deltaX, deltaY, scale);
        translation.set(deltaX, deltaY);
    }

    private void endGesture(MotionEvent event) {
        active = false;
        if (currentAnimator != null) {
            currentAnimator.cancel();
        }
        ValueAnimator animator = ValueAnimator.ofFloat(1, 0);
        animator.setDuration(animationDuration);
        animator.setInterpolator(new DecelerateInterpolator(5f));
        animator.addUpdateListener(updatedAnimation -> {
            float animatedValue = (float)updatedAnimation.getAnimatedValue();
            setCloneTransforms(translation.x * animatedValue, translation.y * animatedValue, 1 + (scale - 1) * animatedValue);
        });

        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                endAnimation();
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
                endAnimation();
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                ctx.getCurrentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                        WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
            }
        });
        animator.start();
        currentAnimator = animator;
    }

    private void endAnimation() {
        currentAnimator = null;
        View rootView = this.getRootView();
        if (backdrop != null) {
            rootView.getOverlay().remove(backdrop);
            backdrop = null;
        }
        if (clone != null) {
            rootView.getOverlay().remove(clone);
            clone = null;
        }
        setVisibility(View.VISIBLE);
        ViewParent parent = this.getParent();
        final Handler handler = new Handler(Looper.getMainLooper());
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                ctx.getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
                parent.requestDisallowInterceptTouchEvent(false);
            }
        }, 100);
    }

    public void setMinimumZoomScale(float minimumZoomScale) {
        minScale = minimumZoomScale;
    }

    public void setMaximumZoomScale(float maximumZoomScale) {
        maxScale = maximumZoomScale;
    }
}

I will create a pr when I have some time

frodriguez-hu commented 1 year ago

This pr fix the issue:

https://github.com/oblador/react-native-pinchable/pull/14

@siddharth-kt @oblador @pierroo