From 30989dd24ac8a7754b4ecd5c3d976065da478cf7 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Tue, 27 Sep 2016 04:22:59 -0700 Subject: [PATCH] Fix ReactSwipeRefreshLayout Summary: `ReactSwipeRefreshLayout` extends `SwipeRefreshLayout` which does not play nice with Android's touch handling system. There are two problems: 1. `SwipeRefreshLayout` overrides and swallows `requestDisallowInterceptTouchEvent`, which means that Views underneath the `SwipeRefreshLayout` will not interact correctly with parent Views of `SwipeRefreshLayout`. We've seen this in practice by H-ScrollViews having their touches intercepted by an enclosing ViewPager. This is fixed by passing `requestDisallowInterceptTouchEvent` up to the parents of `SwipeRefreshLayout`. 2. `SwipeRefreshLayout` overrides `onInterceptTouchEvent` and never calls `super.onInterceptTouchEvent`, therefore ignoring the value of `disallowIntercept`. That means that it will intercept some touches when it shouldn't. One such case is again the H-ScrollView, which should receive all horizontal scrolls and stop `SwipeRefreshLayout` from intercepting any touch events after scrolling. Currently, after the H-ScrollView starts scrolling, it is still possible to get the `SwipeRefreshLayout` to detect and emit refresh events. This is fixed by checking and blocking on horizontal scrolls. Reviewed By: foghina Differential Revision: D3929893 fbshipit-source-id: e6f8050fb554e53318a7ca564c49c20cb5137df9 --- .../swiperefresh/ReactSwipeRefreshLayout.java | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java index d16e58de1b8..9f8ede6537c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java @@ -11,10 +11,11 @@ package com.facebook.react.views.swiperefresh; import android.support.v4.widget.SwipeRefreshLayout; import android.view.MotionEvent; +import android.view.ViewConfiguration; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.uimanager.events.NativeGestureUtil; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.events.NativeGestureUtil; /** * Basic extension of {@link SwipeRefreshLayout} with ReactNative-specific functionality. @@ -24,13 +25,15 @@ public class ReactSwipeRefreshLayout extends SwipeRefreshLayout { private static final float DEFAULT_CIRCLE_TARGET = 64; private boolean mDidLayout = false; - private boolean mRefreshing = false; private float mProgressViewOffset = 0; - + private int mTouchSlop; + private float mPrevTouchX; + private boolean mIntercepted; public ReactSwipeRefreshLayout(ReactContext reactContext) { super(reactContext); + mTouchSlop = ViewConfiguration.get(reactContext).getScaledTouchSlop(); } @Override @@ -71,12 +74,50 @@ public class ReactSwipeRefreshLayout extends SwipeRefreshLayout { } } + /** + * {@link SwipeRefreshLayout} overrides {@link ViewGroup#requestDisallowInterceptTouchEvent} and + * swallows it. This means that any component underneath SwipeRefreshLayout will now interact + * incorrectly with Views that are above SwipeRefreshLayout. We fix that by transmitting the call + * to this View's parents. + */ + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (getParent() != null) { + getParent().requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - if (super.onInterceptTouchEvent(ev)) { + if (shouldInterceptTouchEvent(ev) && super.onInterceptTouchEvent(ev)) { NativeGestureUtil.notifyNativeGestureStarted(this, ev); return true; } return false; } + + /** + * {@link SwipeRefreshLayout} completely bypasses ViewGroup's "disallowIntercept" by overriding + * {@link ViewGroup#onInterceptTouchEvent} and never calling super.onInterceptTouchEvent(). + * This means that horizontal scrolls will always be intercepted, even though they shouldn't, so + * we have to check for that manually here. + */ + private boolean shouldInterceptTouchEvent(MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + mPrevTouchX = ev.getX(); + mIntercepted = false; + break; + + case MotionEvent.ACTION_MOVE: + final float eventX = ev.getX(); + final float xDiff = Math.abs(eventX - mPrevTouchX); + + if (mIntercepted || xDiff > mTouchSlop) { + mIntercepted = true; + return false; + } + } + return true; + } }