react-native-gesture-handler: Flashlist is not scrolling

Description

THE ISSUE IS ONLY ON ANDROID

Flashlist is not scrolling when I wrap the List component to BottomSheet component, However when I comment out the BottomSheet component it works fine. So the issue is related to the BottomSheet component.

The List component itself is been rendered inside a flashlist, This is how I’m using it

<GestureHandlerRootView style={{  height: '100%', width: '100%', position: 'absolute' }}>
                            <View style={{ position: 'absolute', height: '100%', width: '100%', }}>
                                <BottomSheet ref={ref}>
                                    <List />
                                </BottomSheet>
                            </View>
 </GestureHandlerRootView>

BottomSheet component:

import { Dimensions, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useImperativeHandle } from 'react';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from 'react-native-reanimated';

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

const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + 50;

type BottomSheetProps = {
  children?: React.ReactNode;
};

export type BottomSheetRefProps = {
  scrollTo: (destination: number) => void;
  isActive: () => boolean;
};

const BottomSheet = React.forwardRef<BottomSheetRefProps, BottomSheetProps>(
  ({ children }, ref) => {
    const translateY = useSharedValue(0);
    const active = useSharedValue(false);

    const scrollTo = useCallback((destination: number) => {
      'worklet';
      active.value = destination !== 0;

      translateY.value = withSpring(destination, { damping: 50 });
    }, []);

    const isActive = useCallback(() => {
      return active.value;
    }, []);

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

    const context = useSharedValue({ y: 0 });
    const gesture = Gesture.Pan()
      .onStart(() => {
        context.value = { y: translateY.value };
      })
      .onUpdate((event) => {
        translateY.value = event.translationY + context.value.y;
        translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);
      })
      .onEnd(() => {
        if (translateY.value > -SCREEN_HEIGHT / 3) {
          scrollTo(0);
        } else if (translateY.value < -SCREEN_HEIGHT / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
      });

    const rBottomSheetStyle = useAnimatedStyle(() => {
      const borderRadius = interpolate(
        translateY.value,
        [MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y],
        [25, 5],
        Extrapolate.CLAMP
      );

      return {
        borderRadius,
        transform: [{ translateY: translateY.value }],
      };
    });

    return (
      <View>
        <GestureDetector gesture={gesture}>
          <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
            <View style={styles.line} />
            {children}
          </Animated.View>
        </GestureDetector>
      </View>
    );
  }
);

const styles = StyleSheet.create({
  bottomSheetContainer: {
    height: SCREEN_HEIGHT,
    width: '100%',
    backgroundColor: 'black',
    position: 'absolute',
    top: SCREEN_HEIGHT,
    borderRadius: 25,
  },
  line: {
    width: 75,
    height: 4,
    backgroundColor: 'grey',
    alignSelf: 'center',
    marginVertical: 15,
    borderRadius: 2,
  },
});

export default BottomSheet;

List Component:

import { StyleSheet, Text, View } from 'react-native'
import React from 'react'
import { FlashList } from '@shopify/flash-list'

const List = () => {
    const data = [
        { id: '1', title: 'Item 1' },
        { id: '2', title: 'Item 2' },
        { id: '3', title: 'Item 1' },
        { id: '243', title: 'Item 2' },
        { id: '14dff3', title: 'Item 1' },
        { id: '2564ewe36', title: 'Item 2' },
        { id: '24ere3', title: 'Item 2' },
        { id: '14drereff3', title: 'Item 1' },
        { id: '25erer6436', title: 'Item 2' },
        { id: '24rereer3', title: 'Item 2' },
        { id: '14dferrf3', title: 'Item 1' },
        { id: '2564er36', title: 'Item 2' }
    ];

    return (
        <View style={styles.container}>
            <View style={{ flex: 1, maxHeight: '80%', }}>
                <FlashList
                    nestedScrollEnabled={true}
                    data={data}
                    estimatedItemSize={100}
                    contentContainerStyle={{ padding: 37 }}
                    renderItem={({ item }) => (
                        <>
                            <Text style={styles.text}>
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                            </Text>
                        </>
                    )}
                    keyExtractor={(item) => item.id}
                />
            </View>
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        backgroundColor: 'black',
        borderTopLeftRadius: 40,
        borderTopRightRadius: 40,
        height: '100%',
        width: '100%',
        position: 'absolute'
    },
    text: {
        color: 'gray',
        fontWeight: '500',
        fontSize: 14,
        left: 15,
        textAlign: 'left',
        width: '90%',
        marginBottom: 20
    },
})
export default List

Steps to reproduce

Take your mouse in the list and just Drag down the list it won’t work on Andriod but works fine on iOS.

Snack or a link to a repository

null

Gesture Handler version

2.12.1

React Native version

0.72.4

Platforms

Android

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

Fabric (New Architecture)

Build type

Debug mode

Device

Android emulator

Device model

Google Pixel 4

Acknowledgements

Yes

About this issue

  • Original URL
  • State: closed
  • Created 10 months ago
  • Comments: 25 (8 by maintainers)

Most upvoted comments

Yep, I did close this, But Thanks for the help Michał 😃

Okay, I’ve looked at it and noticed few things:

  1. Use <GestureHandlerRootView> at top level of your application. Also you can treat it as standard <View> and for example apply styles to it. So instead of
 <View style={{ height, width, backgroundColor: 'pink' }}>
      <FlashList
        data={data}
        renderItem={renderItem}
        nestedScrollEnabled={true}
        pagingEnabled
        viewabilityConfig={{
          itemVisiblePercentThreshold: 0
        }}
        estimatedItemSize={20}
      />
    </View>

in your Home component, do:

    <GestureHandlerRootView style={{ height, width, backgroundColor: 'pink' }}>
      <FlashList
        data={data}
        renderItem={renderItem}
        nestedScrollEnabled={true}
        pagingEnabled
        viewabilityConfig={{
          itemVisiblePercentThreshold: 0,
        }}
        estimatedItemSize={20}
      />
    </GestureHandlerRootView>

so that gestures will work properly and you won’t have to use another <GestureHandlerRootView>

  1. In your repro you don’t actually use GestureHandler. I assume you’ve overlooked that and code inside BottomModal should have <GestureDetector> instead of <GestureHandlerRootView>
      <GestureDetector gesture={gesture}>
        <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
          <View style={styles.line} />
          {children}
        </Animated.View>
      </GestureDetector>
  1. onPress is not working because you use many <View> components with position: 'absolute'; width: 100%; height:100%;. If you inspect that, they actually lie on top of your button hence it doesn’t work.

  2. I’ve managed to fix issues mentioned above and it seems to work fine with activeOffsetY. The thing is, if you set activeOffsetY, pan won’t activate unless you exceed its activation threshold. In the meantime FlashList has its opportunity to activate, therefore cancelling pan - this is why scrolling FlashList works and BottomSheet is not moving.

To summarize, I’ve fixed some problems with your repro and by adding activeOffsetY GestureHandler seems to work as expected. I haven’t seen it affecting the animation either. Note that you can manipulate with activeOffsetY and increase its value if the one that you’re using right now doesn’t satisfy you.

Here is the corrected code:

import React, {
  useCallback,
  useEffect,
  useRef,
  useImperativeHandle,
} from 'react';
import {
  View,
  Text,
  Image,
  Dimensions,
  StyleSheet,
  Button,
} from 'react-native';
import {
  GestureHandlerRootView,
  Gesture,
  GestureDetector,
} from 'react-native-gesture-handler';
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';
import { FlashList } from '@shopify/flash-list';

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

const MAX_TRANSLATE_Y = -height + 50;

type BottomModalProps = {
  children?: React.ReactNode;
};

export type BottomModalRefProps = {
  scrollTo: (destination: number) => void;
  isActive: () => boolean;
};

const BottomModal = React.forwardRef<BottomModalRefProps, BottomModalProps>(
  ({ children }, ref) => {
    const translateY = useSharedValue(0);
    const active = useSharedValue(false);

    const scrollTo = useCallback((destination: number) => {
      'worklet';
      active.value = destination !== 0;
      translateY.value = withSpring(destination, { damping: 50 });
    }, []);

    const isActive = useCallback(() => {
      return active.value;
    }, []);

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

    const context = useSharedValue({ y: 0 });
    const gesture = Gesture.Pan()
      .activeOffsetY([-10, 10])
      .onStart(() => {
        context.value = { y: translateY.value };
      })
      .onUpdate((event) => {
        translateY.value = event.translationY + context.value.y;
        translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);
      })
      .onEnd(() => {
        if (translateY.value > -height / 3) {
          scrollTo(0);
        } else if (translateY.value < -height / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
      });

    const rBottomSheetStyle = useAnimatedStyle(() => {
      const borderRadius = interpolate(
        translateY.value,
        [MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y],
        [25, 5],
        Extrapolate.CLAMP
      );

      return {
        borderRadius,
        transform: [{ translateY: translateY.value }],
      };
    });

    return (
      <GestureDetector gesture={gesture}>
        <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
          <View style={styles.line} />
          {children}
        </Animated.View>
      </GestureDetector>
    );
  }
);

const List = () => {
  const generateObjects = () => {
    const objects = [];
    for (let i = 1; i <= 50; i++) {
      const object = {
        id: i.toString(),
        title: `Item ${i}`,
      };
      objects.push(object);
    }
    return objects;
  };

  return (
    <View style={styles.container}>
      <View style={{ flex: 1, maxHeight: '90%' }}>
        <FlashList
          nestedScrollEnabled={true}
          data={generateObjects()}
          estimatedItemSize={100}
          contentContainerStyle={{ padding: 37 }}
          renderItem={({ item }) => (
            <>
              <Text>{item.title}</Text>
            </>
          )}
          keyExtractor={(item) => item.id}
        />
      </View>
    </View>
  );
};

const Display = () => {
  const ref = useRef<BottomModalRefProps>(null);

  const HandleonPress = () => {
    const isActive = ref?.current?.isActive();
    if (isActive) {
      ref?.current?.scrollTo(0);
    } else {
      ref?.current?.scrollTo(-550);
    }
  };

  useEffect(() => {
    setTimeout(() => {
      HandleonPress();
    }, 2000);
  }, []);

  return (
    <View style={{ backgroundColor: 'gray' }}>
      <Image
        source={{
          uri: 'https://i.natgeofe.com/n/4f5aaece-3300-41a4-b2a8-ed2708a0a27c/domestic-dog_thumb_square.jpg',
        }}
        style={styles.backgroundProduct}
      />
      <View
        style={{ position: 'absolute', top: '20%', alignContent: 'center' }}>
        <Button title="Press me" onPress={() => console.log('Hello, World')} />
      </View>
      <View style={{ position: 'absolute', height: '100%', width: '100%' }}>
        <BottomModal ref={ref}>
          <List />
        </BottomModal>
      </View>
    </View>
  );
};

const Home = () => {
  let data = [1, 2];
  const renderItem = useCallback(({ item, index }) => {
    return <Display />;
  }, []);

  return (
    <GestureHandlerRootView style={{ height, width, backgroundColor: 'pink' }}>
      <FlashList
        data={data}
        renderItem={renderItem}
        nestedScrollEnabled={true}
        pagingEnabled
        viewabilityConfig={{
          itemVisiblePercentThreshold: 0,
        }}
        estimatedItemSize={20}
      />
    </GestureHandlerRootView>
  );
};

const styles = StyleSheet.create({
  backgroundProduct: {
    height,
    width,
  },
  bottomSheetContainer: {
    height: height,
    width: '100%',
    backgroundColor: 'white',
    position: 'absolute',
    top: height,
    borderRadius: 25,
  },
  line: {
    width: 75,
    height: 4,
    backgroundColor: 'grey',
    alignSelf: 'center',
    marginVertical: 15,
    borderRadius: 2,
  },
  container: {
    backgroundColor: 'white',
    borderTopLeftRadius: 40,
    borderTopRightRadius: 40,
    height: '70%',
  },
});

export default Home;