React-native-gesture-handler: ScrollView breaks RefreshControl on Android

Created on 12 May 2020  路  13Comments  路  Source: software-mansion/react-native-gesture-handler

When using ScrollView from rngh RefreshControl no longer works. This happens because on Android RefreshControl works by wrapping the ScrollView with a SwipeRefreshLayout component. This component should interact with the rngh gesture system since it needs to be able to interrupt and recognize simultaneously the ScrollView it wraps.

This can be kind of accomplished by wrapping RefreshControl with createNativeWrapper on Android. This is the first part of the hack patch I have to fix this:

GestureComponents.js

     });
   },
+  get RefreshControl() {
+    if (Platform.OS === 'android') {
+      return memoizeWrap(ReactNative.RefreshControl, {
+        disallowInterruption: true,
+        shouldCancelWhenOutside: false,
+      });
+    } else {
+      return ReactNative.RefreshControl;
+    }
+  },
   get Switch() {
     return memoizeWrap(ReactNative.Switch, {

The problem then is that ScrollView sets disallowInterruption to true, which means it cannot get interrupted or recognize with another gesture handler. Setting disallowInterruption to false makes RefreshControl work but causes other issues like nested ScrollView will both scroll at the same time.

I tried playing with adding simultaneousHandlers or waitFor to the RefreshControl associated with the ScrollView but wasn't able to get it working. Seems like disallowInterruption takes priority over that.

So at this point I was mostly looking for a hack to get it working so I came up with:

NativeViewGestureHandler.java

 import android.view.ViewGroup;

+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
 public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHandler> {

   private boolean mShouldActivateOnStart;
@@ -48,7 +50,7 @@ public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHa
       }
     }

-    boolean canBeInterrupted = !mDisallowInterruption;
+    boolean canBeInterrupted = !shouldDisallowInterruptionBy(handler);
     int state = getState();
     int otherState = handler.getState();

@@ -62,9 +64,19 @@ public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHa
     return state == STATE_ACTIVE && canBeInterrupted;
   }

+  private boolean shouldDisallowInterruptionBy(GestureHandler handler) {
+    if (handler.getView() instanceof SwipeRefreshLayout) {
+      return false;
+    }
+    return mDisallowInterruption;
+  }
+
   @Override
   public boolean shouldBeCancelledBy(GestureHandler handler) {
-    return !mDisallowInterruption;
+    if (handler.getView() instanceof SwipeRefreshLayout) {
+      return true;
+    }
+    return !shouldDisallowInterruptionBy(handler);
   }

   @Override

Basically it just special cases when the other handler is SwipeRefreshLayout so that even if mDisallowInterruption is true it will treat it like it was false.

Not sure how this could be properly fix, will keep using this hack for now.

Android Cross platform inconsistency

Most helpful comment

I had the same issue with the scrollview from react-native-gesture-handler tried so many methods finally i just switched to scrollview from react-native to solve this issue

All 13 comments

@janicduplessis

I got it working by putting a ScrollView inside a ScrollView
so the code looks like this:

<PanGestureHandler>
 <ScrollView>
  <ScrollView refreshControl={<RefreshControl />}>
   // rest
  </ScrollView>
 </ScrollView>
</PanGestureHandler>

What side effects should I expect ?

@jakub-gonet do you maybe have time to prioritize this one?

@janicduplessis, could please describe what is broken here (expected/current behavior)? I used this code as a repro example
and it seems alright to me:

import React, { Component, useCallback } from 'react';
import { SafeAreaView, View, Text, RefreshControl } from 'react-native';

import { ScrollView } from 'react-native-gesture-handler';

const wait = timeout => {
  return new Promise(resolve => {
    setTimeout(resolve, timeout);
  });
};
export default function App() {
  const [refreshing, setRefreshing] = React.useState(false);

  const onRefresh = useCallback(() => {
    setRefreshing(true);

    wait(1500).then(() => setRefreshing(false));
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView
        contentContainerStyle={styles.scrollView}
        refreshControl={
          <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
        }>
        <Text>Pull down to see RefreshControl indicator</Text>
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = {
  container: {
    flex: 1,
  },
  scrollView: {
    flex: 1,
    backgroundColor: 'pink',
    alignItems: 'center',
    justifyContent: 'center',
  },
};

android-ios

EDIT: I was running this example on fresh RN 0.63.2 installation

Hmm strange, I can repro using the same code in my app by replacing everything in App.js, but can't repro in snack. Issue might be related to the react-native version I'm using in my app, will investigate more.

@jakub-gonet I was able to reproduce with https://github.com/janicduplessis/rnghtest

I had to make the scrollview scrollable to reproduce. It is just a simple react-native init project and added rngh. Still doesn't repro in snack so it might only happen on more recent RN versions.

Tested on Pixel 3 API 29 emulator

import React, {useCallback} from 'react';
import {View, Text, RefreshControl} from 'react-native';

import {ScrollView} from 'react-native-gesture-handler';

const wait = (timeout) => {
  return new Promise((resolve) => {
    setTimeout(resolve, timeout);
  });
};
export default function App() {
  const [refreshing, setRefreshing] = React.useState(false);

  const onRefresh = useCallback(() => {
    setRefreshing(true);

    wait(1500).then(() => setRefreshing(false));
  }, []);

  return (
    <ScrollView
      style={{flex: 1}}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }>
      <View
        style={{
          height: 2000,
          paddingTop: 64,
          backgroundColor: 'pink',
          alignItems: 'center',
        }}>
        <Text>Pull down to see RefreshControl indicator</Text>
      </View>
    </ScrollView>
  );
}

On 0.62.2 setting the the refresh control on Android the content of the default RN ScrollView is no longer rendered.
Maybe that's related?

@janicduplessis, I tried to reproduce it and seems like it's not working. After replacing ScrollView with RN one it still doesn't work. It's a problem with RNGH though, I'll try to deep deeper into that.

I had the same issue with the scrollview from react-native-gesture-handler tried so many methods finally i just switched to scrollview from react-native to solve this issue

I ran into this issue today as well. A RNGH scrollview with content that is large enough to scroll. Adding a RefreshControl. And it doesn't work. If the content is small enough that no scrolling is needed it does work. Im 100% sure this worked a few months ago when i build these screens.

Any idea on how to fix this properly?
And what are the downsides of using the RN scrollview? this was never 100% clear to me (why the RNGH scrollview is better).

edit: @jakub-gonet can you comment on this?

I'm still getting this issue in RNGH v1.9.0 and RN v0.63.3. Testing the above code from @janicduplessis take me to this issue on Android, but it doesn't happen on iOS.

facing same issue, swipe up on android emulator causing refresh controll trigger, works all good on ios

Same here. I tried multiple times, and few of the times it worked. Is it the same with you that randomly it works?

@jakub-gonet Any update on this?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jacobrosenskold picture jacobrosenskold  路  3Comments

Agoujil2saad picture Agoujil2saad  路  3Comments

rt2zz picture rt2zz  路  4Comments

radex picture radex  路  3Comments

brentvatne picture brentvatne  路  5Comments