Search code examples
javascriptandroidiosreact-nativeflatlist

React Native Touch Through Flatlist


For "react-native": "^0.70.5"

Requirement:

  • Flatlist as an overlay above Clickable elements
  • Flatlist header has a transparent area, with pointerEvents="none" to make the elements below clickable and yet allow the Flatlist to scroll. enter image description here

Issues with some possible approaches

  1. pointerEvents="none" doesn't work with Flatlist, as internally how Flatlist is built it will block the events at all values of pointerEvents. It's the same with Scrollview as well.
  2. react-native-touch-through-view (the exact library I need) doesn't work with RN 0.70.2, library is outdated. After fixing the build issues, touch events are not propagating to the clickable elements.
  3. Created a custom component ScrollableView, as pointerEvents with View work well. With this adding pointerEvents to none on parts of the children, lets the touch event to propagate to elements below.
  • This is working well on Android, but failing on iOS.
  • Also the scrolling of the view is not smooth.
  • Requires further handling for performance optimisation for long lists
import React, { useState, useRef } from 'react';
import { View, PanResponder, Animated } from 'react-native';

const ScrollableView = ({children, style, onScroll}) => {
    const scrollY = useRef(new Animated.Value(0)).current;
    const lastScrollY = useRef(0);
    const scrollYClamped = Animated.diffClamp(scrollY, 0, 1000);

    const panResponder = useRef(
        PanResponder.create({
            onStartShouldSetPanResponder: () => true,
            onPanResponderMove: (_, gestureState) => {
                scrollY.setValue(lastScrollY.current + gestureState.dy);
            },
            onPanResponderRelease: (_, { vy, dy }) => {
                lastScrollY.current += dy;
                Animated.spring(scrollY, {
                    toValue: lastScrollY.current,
                    velocity: vy,
                    tension: 2,
                    friction: 8,
                    useNativeDriver: false,
                }).start();
            },

        })
    ).current;

    const combinedStyle = [
        {
            transform: [{ translateY: scrollYClamped }],
        },
        style
    ];

    return (
        <Animated.View
            {...panResponder.panHandlers}
            pointerEvents="box-none"
            style={combinedStyle}
        >
            {children}
        </Animated.View>
    );
};

export default ScrollableView;

Any solution to any of the above three approaches is appreciated.


Solution

  • https://ui.gorhom.dev/components/bottom-sheet/components/bottomsheetflatlist

    import React, { useCallback, useRef, useMemo } from "react";
    import { StyleSheet, View, Text, Button } from "react-native";
    import BottomSheet, { BottomSheetFlatList } from "@gorhom/bottom-sheet";
    
    const App = () => {
      // hooks
      const sheetRef = useRef<BottomSheet>(null);
    
      // variables
      const data = useMemo(
        () =>
          Array(50)
            .fill(0)
            .map((_, index) => `index-${index}`),
        []
      );
      const snapPoints = useMemo(() => ["25%", "50%", "90%"], []);
    
      // callbacks
      const handleSheetChange = useCallback((index) => {
        console.log("handleSheetChange", index);
      }, []);
      const handleSnapPress = useCallback((index) => {
        sheetRef.current?.snapToIndex(index);
      }, []);
      const handleClosePress = useCallback(() => {
        sheetRef.current?.close();
      }, []);
    
      // render
      const renderItem = useCallback(
        ({ item }) => (
          <View style={styles.itemContainer}>
            <Text>{item}</Text>
          </View>
        ),
        []
      );
      return (
        <View style={styles.container}>
          <Button title="Snap To 90%" onPress={() => handleSnapPress(2)} />
          <Button title="Snap To 50%" onPress={() => handleSnapPress(1)} />
          <Button title="Snap To 25%" onPress={() => handleSnapPress(0)} />
          <Button title="Close" onPress={() => handleClosePress()} />
          <BottomSheet
            ref={sheetRef}
            snapPoints={snapPoints}
            onChange={handleSheetChange}
          >
            <BottomSheetFlatList
              data={data}
              keyExtractor={(i) => i}
              renderItem={renderItem}
              contentContainerStyle={styles.contentContainer}
            />
          </BottomSheet>
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        paddingTop: 200,
      },
      contentContainer: {
        backgroundColor: "white",
      },
      itemContainer: {
        padding: 6,
        margin: 6,
        backgroundColor: "#eee",
      },
    });
    
    export default App;