14

I have a ScrollView inside an animated View that has panning.

<Animated.View {...this.panResponder.panHandlers}>
    <ScrollView>
    ...
    </ScrollView>
<Animated.View>

This is a sample view of my screen:

enter image description here

A user should be able to swipe upwards and the draggable area should snap upwards, as shown below:

enter image description here

Now my problem is the Scrollview. I want a user to be able to scroll the content inside.

After the user has finished viewing the content inside and scrolls all the way up(by performing downward swiping motion) and tries to swipe further, the draggable area should move downwards to its original position.

I have tried various methods, mainly focusing on disabling and enabling the scrolling of the ScrollView, to prevent its interference with the panning.

My current solution is not ideal.

My main issue is these 2 methods:

onStartShouldSetPanResponder 
onStartShouldSetPanResponderCapture

Not sure if my assumption is correct but these methods decide if the View should capture the touch event. I either allow panning or let the ScrollView capture the event instead.

My problem is I need to somehow know what the user intends to do before the other pan handlers kick in. But I cant know that until the user has moved, either downwards or upwards. To know the direction I need the event to pass on to the onPanResponderMove handler.

So in essence, I need to decide if I should allow my view to be dragged before even knowing the direction the user is swiping. Currently that is not possible.

Hopefully Im missing something simple here.

EDIT: Found a similar question(with no answer): Drag up a ScrollView then continue scroll in React Native

Gilbert Nwaiwu
  • 687
  • 2
  • 13
  • 37
  • I'm still waiting for some more info but for now I've thought up a different approach. I will be scrapping the ScrollView in the draggable area and dealing with a single long View and PanResponder exclusively. Downside is there wont be any scroll bar indicator but i can live with that – Gilbert Nwaiwu Mar 12 '19 at 07:27
  • If possible, please put your answer here. Also, this library kind of helped me but I wasn't able to scroll within the nestedscrollview https://www.npmjs.com/package/react-native-collapsing-toolbar – Shubham Bisht Mar 12 '19 at 07:32
  • I will as soon as I can get to it. I have implemented the functionality of the library you posted before. Its quite straightforward. Unfortunately it doesnt support some panning functionality im looking for. – Gilbert Nwaiwu Mar 12 '19 at 10:10
  • take a look at https://github.com/kmagiera/react-native-gesture-handler, it's more flexible than what comes with react-native itself – vonovak Mar 16 '19 at 16:24
  • I can't even make any reponder callback to work on ScrollView. Can you? – diogenesgg Mar 19 '19 at 05:26
  • I took a different route. Its somewhat problematic controlling ScrollView behaviour with a PanResponder. Now I just use an absolute positioned View with variable transform for vertical location to simulate a sticky ScrollView – Gilbert Nwaiwu Mar 19 '19 at 14:25
  • There is a library called react-native-interactable. It has some examples regarding your use case. All the interactions are performed at 60FPS. – Thakur Karthik Jun 13 '19 at 11:15

3 Answers3

9

Apparently the problem is in the Native layer.

https://github.com/facebook/react-native/issues/9545#issuecomment-245014488

I found that onterminationrequest not being triggerred is caused by Native Layer.

Modify react-native\ReactAndroid\src\main\java\com\facebook\react\views\scroll\ReactScrollView.java , comment Line NativeGestureUtil.notifyNativeGestureStarted(this, ev); and then build from the source, you will see your PanResponder outside ScrollView takes the control as expected now.

PS: I couldn't build from source yet. Building from source apparently is way harder than I thought.

EDIT 1:

Yes, it worked. I deleted the react-native folder from node_modules, then git cloned the react-native repository directly into node_modules. And checked out to version 0.59.1. Then, followed this instructions. For this example, I didn't have to set any PanReponder or Responder to the ScrollView.

However, it doesn't work as expected, of course. I had to hold the press gesture up and down. If you scroll all the way up, then try to move it down, it will panResponde to snap the blue area down. The content will remain up.

Conclusion: Even after removing the strong locking from ScrollView, it's quite complex to implement the full desired behaviour. Now we have to combine the onMoveShouldSetPanResponder to the ScrollView's onScroll, and deal with the initial press event, to take the delta Y so that we can finally move the parent view properly, once it's reached top.

enter image description here

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow
 */

import React, { Component } from 'react';
import { Platform, StyleSheet, Text, View, Dimensions, PanResponder, Animated, ScrollView } from 'react-native';

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
  android:
    'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

export default class App extends Component {

  constructor(props) {
    super(props);
    
    const {height, width} = Dimensions.get('window');

    const initialPosition = {x: 0, y: height - 70}
    const position = new Animated.ValueXY(initialPosition);

    const parentResponder = PanResponder.create({
      onMoveShouldSetPanResponderCapture: (e, gestureState) => {
        return false
      },
      onStartShouldSetPanResponder: () => false,
      onMoveShouldSetPanResponder: (e, gestureState) =>  {
        if (this.state.toTop) {
          return gestureState.dy > 6
        } else {
          return gestureState.dy < -6
        }
      },
      onPanResponderTerminationRequest: () => false,
      onPanResponderMove: (evt, gestureState) => {
        let newy = gestureState.dy
        if (this.state.toTop && newy < 0 ) return
        if (this.state.toTop) {
          position.setValue({x: 0, y: newy});
        } else {
          position.setValue({x: 0, y: initialPosition.y + newy});
        }
      },
      onPanResponderRelease: (evt, gestureState) => {
        if (this.state.toTop) {
          if (gestureState.dy > 50) {
            this.snapToBottom(initialPosition)
          } else {
            this.snapToTop()
          }
        } else {
          if (gestureState.dy < -90) {
            this.snapToTop()
          } else {
            this.snapToBottom(initialPosition)
          }
        }
      },
    });

    this.offset = 0;
    this.parentResponder = parentResponder;
    this.state = { position, toTop: false };
  }

  snapToTop = () => {
    Animated.timing(this.state.position, {
      toValue: {x: 0, y: 0},
      duration: 300,
    }).start(() => {});
    this.setState({ toTop: true })
  }

  snapToBottom = (initialPosition) => {
    Animated.timing(this.state.position, {
      toValue: initialPosition,
      duration: 150,
    }).start(() => {});
    this.setState({ toTop: false })
  }

  hasReachedTop({layoutMeasurement, contentOffset, contentSize}){
    return contentOffset.y == 0;
  }

  render() {
    const {height} = Dimensions.get('window');

    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>Welcome to React Native!</Text>
        <Text style={styles.instructions}>To get started, edit App.js</Text>
        <Text style={styles.instructions}>{instructions}</Text>
        <Animated.View style={[styles.draggable, { height }, this.state.position.getLayout()]} {...this.parentResponder.panHandlers}>
          <Text style={styles.dragHandle}>=</Text>
          <ScrollView style={styles.scroll}>
            <Text style={{fontSize:44}}>Lorem Ipsum</Text>
            <Text style={{fontSize:44}}>dolor sit amet</Text>
            <Text style={{fontSize:44}}>consectetur adipiscing elit.</Text>
            <Text style={{fontSize:44}}>In ut ullamcorper leo.</Text>
            <Text style={{fontSize:44}}>Sed sed hendrerit nulla,</Text>
            <Text style={{fontSize:44}}>sed ullamcorper nisi.</Text>
            <Text style={{fontSize:44}}>Mauris nec eros luctus</Text>
            <Text style={{fontSize:44}}>leo vulputate ullamcorper</Text>
            <Text style={{fontSize:44}}>et commodo nulla.</Text>
            <Text style={{fontSize:44}}>Nullam id turpis vitae</Text>
            <Text style={{fontSize:44}}>risus aliquet dignissim</Text>
            <Text style={{fontSize:44}}>at eget quam.</Text>
            <Text style={{fontSize:44}}>Nulla facilisi.</Text>
            <Text style={{fontSize:44}}>Vivamus luctus lacus</Text>
            <Text style={{fontSize:44}}>eu efficitur mattis</Text>
          </ScrollView>
        </Animated.View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
  draggable: {
      position: 'absolute',
      right: 0,
      backgroundColor: 'skyblue',
      alignItems: 'center'
  },
  dragHandle: {
    fontSize: 22,
    color: '#707070',
    height: 60
  },
  scroll: {
    paddingLeft: 10,
    paddingRight: 10
  }
});
diogenesgg
  • 2,601
  • 2
  • 20
  • 29
  • Hi @diogenesgg, your solution is great. I just have one problem, I made some changes from your code where my sheet when pulled up, reaches half of the screen. Now when I would just start pulling my sheet downwards, the sheet view initially goes to the top of screen and then starts going downwards. Can you please help me with it. – Shubham Bisht Jul 02 '19 at 07:03
  • And also, how can I keep scrolling downwards in the sheet without making the sheet go down? – Shubham Bisht Jul 02 '19 at 10:15
  • Is this actually working for anyone? I even tried this out with expo on my android device but it doesn't work. – pravchuk Mar 29 '23 at 03:05
2

Maybe, this is same as your problem

https://github.com/rome2rio/react-native-touch-through-view

A better fork, I think

https://github.com/simonhoss/react-native-touch-through-view/issues/5

tuledev
  • 10,177
  • 4
  • 29
  • 49
  • This looks interesting but doesn't fit my needs exactly. I need the draggable area to snap smoothly and take in swipe velocity and such. ScrollView in Android has limitations so this won't work for me. Thanks for the info though :) – Gilbert Nwaiwu Mar 12 '19 at 07:25
0

I think you can create a bottomsheetlayout to fulfill your requirement . Or you can use actionsheets in iOS. Consider below libraries. It may helpfull for you

https://github.com/cesardeazevedo/react-native-bottom-sheet-behavior

or

https://github.com/maxs15/react-native-modalbox

Vinayak B
  • 4,430
  • 4
  • 28
  • 58