question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

ScrollView working wrong

See original GitHub issue

Description

I try to reproduce bottom-sheet like this: https://github.com/software-mansion/react-native-gesture-handler/blob/main/example/src/new_api/bottom_sheet/index.tsx , but using reanimted api and functional component. But I faced this problem, scrollview not scrolling in first snap, but, when snap changes, scrollview will constantly scroll Please help, I don’t understand how this interaction should happen

Platforms

  • iOS
  • Android
  • Web

Video

https://user-images.githubusercontent.com/23406402/161047876-c7d06d7c-7de1-44c6-b47f-d61364d5c57f.mp4

Steps To Reproduce

Just use below code

Snack or minimal code example

import React, {
  createRef,
  forwardRef,
  PropsWithChildren,
  useCallback,
  useImperativeHandle,
  useMemo,
  useState,
} from 'react';
import { Dimensions, View } from 'react-native';
import {
  GestureHandlerRootView,
  NativeViewGestureHandler,
  PanGestureHandler,
  PanGestureHandlerGestureEvent,
  TapGestureHandler,
} from 'react-native-gesture-handler';
import Animated, {
  runOnJS,
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const DAMPING_COEFFICIENT = 20;
const { height: HEIGHT } = Dimensions.get('window');

const useSheetPosition = () => {
  const { top } = useSafeAreaInsets();

  return useMemo(
    () => ({
      startSnap: HEIGHT / 2,
      hideSnap: HEIGHT / 1.5,
      maxSnap: top,
    }),
    [top],
  );
};

interface SheetProps {
  onOpened?: (state: boolean) => void;
}

export interface SheetRefProps {
  setCollapse: (state: boolean) => void;
}

const Sheet = forwardRef<PropsWithChildren<SheetRefProps>, SheetProps>(
  ({ children, onOpened }, ref) => {
    const { top } = useSafeAreaInsets();
    const { startSnap, maxSnap, hideSnap } = useSheetPosition();

    const [currentSnap, setCurrentSnap] = useState(startSnap);
    const scrollOffset = useSharedValue(0);
    const opacity = useSharedValue(1);
    const translateY = useSharedValue(startSnap);

    const tapMainRef = createRef();
    const panHeaderRef = createRef();
    const nativeScrollRef = createRef();
    const panContainerRef = createRef();

    const transformTo = useCallback(
      (destination: number) => {
        'worklet';

        translateY.value = withSpring(destination, { mass: 0.5 });
      },
      [translateY],
    );

    const setCollapse = useCallback(
      state => {
        if (state) {
          transformTo(hideSnap);
          opacity.value = 0.8;
        } else {
          transformTo(startSnap);
          opacity.value = 1;
        }
      },
      [hideSnap, opacity, startSnap, transformTo],
    );

    useImperativeHandle(ref, () => ({ setCollapse }), [setCollapse]);

    const onHandlerEndOnJS = ({ open, snap }) => {
      if (onOpened) onOpened(open);
      setCurrentSnap(snap);
    };

    const gestureHandler = useAnimatedGestureHandler<
      PanGestureHandlerGestureEvent,
      { startY: number }
    >({
      onStart: (_, ctx) => {
        ctx.startY = translateY.value;
      },
      onActive: (event, ctx) => {
        if (
          (translateY.value > top - DAMPING_COEFFICIENT && translateY.value <= startSnap) ||
          (translateY.value > startSnap && translateY.value < startSnap + DAMPING_COEFFICIENT)
        ) {
          translateY.value = event.translationY + ctx.startY;
        }
      },
      onEnd: () => {
        if (translateY.value < HEIGHT / 3) {
          transformTo(maxSnap);
          runOnJS(onHandlerEndOnJS)({
            open: true,
            snap: maxSnap,
          });
        } else {
          transformTo(startSnap);
          runOnJS(onHandlerEndOnJS)({
            open: false,
            snap: startSnap,
          });
        }
      },
    });

    const sheetAnimatedStyle = useAnimatedStyle(() => {
      return {
        transform: [{ translateY: translateY.value }],
        opacity: opacity.value,
      };
    });

    return (
      <View
        style={{ flex: 1, position: 'absolute', top: 0, zIndex: 11, left: 0, right: 0 }}
        pointerEvents="box-none"
      >
        <GestureHandlerRootView style={{ flex: 1 }}>
          <TapGestureHandler
            ref={tapMainRef}
            maxDurationMs={100000}
            maxDeltaY={currentSnap - maxSnap}
          >
            <Animated.View style={[styles.container, sheetAnimatedStyle]}>
              <PanGestureHandler
                ref={panHeaderRef}
                simultaneousHandlers={[nativeScrollRef, tapMainRef]}
                shouldCancelWhenOutside={false}
                enableTrackpadTwoFingerGesture
                onGestureEvent={gestureHandler}
              >
                <Animated.View style={styles.handle} />
              </PanGestureHandler>
              <PanGestureHandler
                ref={panContainerRef}
                simultaneousHandlers={[nativeScrollRef, tapMainRef]}
                shouldCancelWhenOutside={false}
                onGestureEvent={gestureHandler}
                enableTrackpadTwoFingerGesture
              >
                <Animated.View style={styles.innerContainer}>
                  <NativeViewGestureHandler
                    ref={nativeScrollRef}
                    waitFor={tapMainRef}
                    simultaneousHandlers={panContainerRef}
                  >
                    <Animated.ScrollView
                      style={styles.scrollContainer}
                      bounces={false}
                      scrollEventThrottle={1}
                      onScrollBeginDrag={e => {
                        scrollOffset.value = e.nativeEvent.contentOffset.y;
                      }}
                    >
                      {children}
                    </Animated.ScrollView>
                  </NativeViewGestureHandler>
                </Animated.View>
              </PanGestureHandler>
            </Animated.View>
          </TapGestureHandler>
        </GestureHandlerRootView>
      </View>
    );
  },
);

const styles = StyleSheet.create({
      container: {
        position: 'absolute',
        top: 0,
        left: 0,
        right: 0,
        width: '100%',
        height: HEIGHT,
        paddingBottom: 48,
        backgroundColor: '#fff',
        borderTopLeftRadius: 10,
        borderTopRightRadius: 10,
      },
      handle: {
         height: 30,
         background: '#f0f0f0',
      }
      innerContainer: {
        flex: 1,
      },
      scrollContainer: {
        flex: 1,
        maxHeight: '100%',
      },
});

export { Sheet };

Expected behavior

Work right, like bottom sheet

Actual behavior

Scrollview scrolling always after change state for TapGestureHandler

Package versions

  • React: 17.0.2
  • React Native: 0.67.4
  • React Native Gesture Handler: ^2.3.2
  • React Native Reanimated: ^2.4.1

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:6 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
j-piaseckicommented, Apr 3, 2022

I took a more in-depth look at your code, here are some tips to get it to work:

  • use useRef instead of createRef in functional components, createRef will return a new object every render while useRef will return the same one
  • don’t update the sheet position when the scroll is active (i.e. contentOffset.y is not zero)

Here’s the updated code to show what I mean:

import React, {
  useRef,
  forwardRef,
  PropsWithChildren,
  useCallback,
  useImperativeHandle,
  useMemo,
  useState,
} from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
import {
  GestureHandlerRootView,
  NativeViewGestureHandler,
  PanGestureHandler,
  PanGestureHandlerGestureEvent,
  TapGestureHandler,
} from 'react-native-gesture-handler';
import Animated, {
  runOnJS,
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const DAMPING_COEFFICIENT = 20;
const { height: HEIGHT } = Dimensions.get('window');

const useSheetPosition = () => {
  const { top } = useSafeAreaInsets();

  return useMemo(
    () => ({
      startSnap: HEIGHT / 2,
      hideSnap: HEIGHT / 1.5,
      maxSnap: top,
    }),
    [top],
  );
};

interface SheetProps {
  onOpened?: (state: boolean) => void;
}

export interface SheetRefProps {
  setCollapse: (state: boolean) => void;
}

const Sheet = forwardRef<PropsWithChildren<SheetRefProps>, SheetProps>(
  ({ children, onOpened }, ref) => {
    const { top } = useSafeAreaInsets();
    const { startSnap, maxSnap, hideSnap } = useSheetPosition();

    const [currentSnap, setCurrentSnap] = useState(startSnap);
    const scrollStartOffset = useSharedValue(0);
    const scrollOffset = useSharedValue(0);
    const opacity = useSharedValue(1);
    const translateY = useSharedValue(startSnap);

    const tapMainRef = useRef();
    const panHeaderRef = useRef();
    const nativeScrollRef = useRef();
    const panContainerRef = useRef();

    const transformTo = useCallback(
      (destination: number) => {
        'worklet';

        translateY.value = withSpring(destination, { mass: 0.5 });
      },
      [translateY],
    );

    const setCollapse = useCallback(
      state => {
        if (state) {
          transformTo(hideSnap);
          opacity.value = 0.8;
        } else {
          transformTo(startSnap);
          opacity.value = 1;
        }
      },
      [hideSnap, opacity, startSnap, transformTo],
    );

    useImperativeHandle(ref, () => ({ setCollapse }), [setCollapse]);

    const onHandlerEndOnJS = ({ open, snap }) => {
      if (onOpened) onOpened(open);
      setCurrentSnap(snap);
    };

    const gestureHandler = useAnimatedGestureHandler<
      PanGestureHandlerGestureEvent,
      { startY: number }
    >({
      onStart: (_, ctx) => {
        ctx.startY = translateY.value;
      },
      onActive: (event, ctx) => {
        if (
          ((translateY.value > top - DAMPING_COEFFICIENT && translateY.value <= startSnap) ||
          (translateY.value > startSnap && translateY.value < startSnap + DAMPING_COEFFICIENT)) &&
          scrollOffset.value === 0
        ) {
          translateY.value = event.translationY + ctx.startY - scrollStartOffset.value;
        }
      },
      onEnd: () => {
        if (translateY.value < HEIGHT / 3) {
          transformTo(maxSnap);
          runOnJS(onHandlerEndOnJS)({
            open: true,
            snap: maxSnap,
          });
        } else {
          transformTo(startSnap);
          runOnJS(onHandlerEndOnJS)({
            open: false,
            snap: startSnap,
          });
        }
      },
    });

    const sheetAnimatedStyle = useAnimatedStyle(() => {
      return {
        transform: [{ translateY: translateY.value }],
        opacity: opacity.value,
      };
    });

    return (
      <View
        style={{ flex: 1, position: 'absolute', top: 0, zIndex: 11, left: 0, right: 0 }}
        pointerEvents="box-none"
      >
        <GestureHandlerRootView style={{ flex: 1 }}>
          <TapGestureHandler
            ref={tapMainRef}
            maxDurationMs={100000}
            maxDeltaY={currentSnap - maxSnap}
          >
            <Animated.View style={[styles.container, sheetAnimatedStyle]}>
              <PanGestureHandler
                ref={panHeaderRef}
                simultaneousHandlers={[nativeScrollRef, tapMainRef]}
                shouldCancelWhenOutside={false}
                enableTrackpadTwoFingerGesture
                onGestureEvent={gestureHandler}
              >
                <Animated.View style={styles.handle} />
              </PanGestureHandler>
              <PanGestureHandler
                ref={panContainerRef}
                simultaneousHandlers={[nativeScrollRef, tapMainRef]}
                shouldCancelWhenOutside={false}
                onGestureEvent={gestureHandler}
                enableTrackpadTwoFingerGesture
              >
                <Animated.View style={styles.innerContainer}>
                  <NativeViewGestureHandler
                    ref={nativeScrollRef}
                    waitFor={tapMainRef}
                    simultaneousHandlers={panContainerRef}
                  >
                    <Animated.ScrollView
                      style={styles.scrollContainer}
                      bounces={false}
                      scrollEventThrottle={1}
                      onScrollBeginDrag={e => {
                        scrollStartOffset.value = e.nativeEvent.contentOffset.y;
                      }}
                      onScroll={e => {
                        scrollOffset.value = e.nativeEvent.contentOffset.y;
                      }}
                    >
                      {children}
                    </Animated.ScrollView>
                  </NativeViewGestureHandler>
                </Animated.View>
              </PanGestureHandler>
            </Animated.View>
          </TapGestureHandler>
        </GestureHandlerRootView>
      </View>
    );
  },
);

const styles = StyleSheet.create({
      container: {
        position: 'absolute',
        top: 0,
        left: 0,
        right: 0,
        width: '100%',
        height: HEIGHT,
        paddingBottom: 48,
        backgroundColor: '#fff',
        borderTopLeftRadius: 10,
        borderTopRightRadius: 10,
      },
      handle: {
         height: 30,
         background: '#f0f0f0',
      },
      innerContainer: {
        flex: 1,
      },
      scrollContainer: {
        flex: 1,
        maxHeight: '100%',
      },
});

export { Sheet };
0reactions
j-piaseckicommented, Apr 6, 2022

Happy to see you have solved the problem.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Android ScrollView not working properly - Stack Overflow
I developed an android application in which the scroll-view is not scrolling.. I am posting the code here pls check and if found...
Read more >
Common bugs in React Native ScrollView and how to fix them
React Native's ScrollView component is ubiquitous, but its implementation can sometimes lead to mistakes. Learn what to look out for here.
Read more >
ScrollView won't scroll | Apple Developer Forums
I have successfully created a scrollview with several buttons that stack on top of one another. But the View won't scroll. What am...
Read more >
ScrollView - React Native
Component that wraps platform ScrollView while providing ... Only works for vertical ScrollViews ( horizontal prop must be false ).
Read more >
ScrollView - Android Developers
For vertical scrolling, consider NestedScrollView instead of scroll view ... android:saveEnabled, If false, no state will be saved for this view when it...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found