Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Close sheet when top of FlatList is reached #20

Open
pontusab opened this issue Mar 27, 2019 · 30 comments
Open

Close sheet when top of FlatList is reached #20

pontusab opened this issue Mar 27, 2019 · 30 comments

Comments

@pontusab
Copy link

Hey! Great lib!

Is it possible to add a FlatList to content and use that scroll position when the bottom sheet should expand or not?

Like the apple map, when the list is in the top and you move upwards, the sheat goes up. but when you scroll down the sheet stays until the user scroll to the top of the list within.

Is it possible to hook up using this or should I go custom with reanimates and gesture handler?

@Priyatham51
Copy link

Priyatham51 commented Mar 28, 2019

@pontusab I think it possible to add to the container and it does behave like Apple maps. Here is my implementation

  renderInner = () => (
        <View style={[styles.panel, {height:Math.max(this.props.data.length*75,300)}]}>
              <FlatList
                showsVerticalScrollIndicator={false}
                scrollEnabled={false}
                data={this.props.data}
                keyExtractor = {(item) => item.BusID}
                renderItem={({item}) => 
                    <PlainBusCard data={item}></PlainBusCard>
                }
                contentContainerStyle={{ flexGrow: 1 }}
                ItemSeparatorComponent={this.renderSeparator}
                ListEmptyComponent={this.props.emptyView}
            />
        </View>
      )
      renderHeader = () => (
        <View style={styles.header}>
          <View style={styles.panelHeader}>
            <Text>Search Busstop</Text>
          </View>
        </View>
      )

   render() {
        return (
            <View style={styles.container}>
                <BottomSheet
                    snapPoints = {[600, 300, 100]}
                    renderContent = {this.renderInner}
                    renderHeader = {this.renderHeader}
                />
                <MapView
                    onRegionChangeComplete={() => this.props.mapMoved()}
                    initialRegion={{latitude: this.appDefaultLocation.latitude, longitude:this.appDefaultLocation.longitude, latitudeDelta:0.003, longitudeDelta:0.003}}
                    style={{ flex: 1 }}
                />
            </View>
        )
    }

@osdnk Please let me know if I'm using this lib in a wrong way.

@pontusab
Copy link
Author

Thanks! of course, it's possible to add it as a container, but I want the sheet to expand if the scroll position within Flatlist is 0 and when the user scrolls down it should stay. but when the user scrolls to top again the sheet should go down.

@Priyatham51
Copy link

Priyatham51 commented Apr 5, 2019

@osdnk any comments or suggestion for us to add a FlatList in the content?

@osdnk
Copy link
Owner

osdnk commented Apr 8, 2019

Considering two examples I have added a few days ago I think I cover most cases of FlatList usage.

The only point is virtualization, but you see it's necessary for bottom-sheet?

@pontusab
Copy link
Author

pontusab commented Apr 9, 2019

Thanks, I would like to have a list of images from camera roll that the user can scroll, then I need a FlatList to be performant, The list should expand like this bottom sheet when the user starts to scroll up, but when reaching the top of the Flatlist again it should go down, I guess its not possible whit this lib because I need to handle FlatList scroll position?

@Priyatham51
Copy link

@osdnk I was able to get the flat list working very close to apple maps. But the biggest issue I'm facing is the innerContent height when my flat list has no data or few rows.
My use case is my inner content is determined basing on a service call. There might no rows vs 20 rows.
With the manual calculations I'm not getting the smooth animations that you were able to achieve with your examples. :(

I can share the example I have if you think it will be useful to see the issue

@siderakis
Copy link

+1 for virtualization support for the the bottom sheet (maybe based on a generic react-native-gesture-handler based scrollview with virtualization support).

@osdnk
Copy link
Owner

osdnk commented Apr 27, 2019

I don't have a clear idea of how to do it neither enough time, but I'll happily see some solution.

Maybe you could build some own idea of virtualization basing on the position of content?

@blohamen
Copy link

blohamen commented Jun 3, 2019

Thanks, I would like to have a list of images from camera roll that the user can scroll, then I need a FlatList to be performant, The list should expand like this bottom sheet when the user starts to scroll up, but when reaching the top of the Flatlist again it should go down, I guess its not possible whit this lib because I need to handle FlatList scroll position?

@osdnk it is possible to handle this? Really needs this feature for my app

@gorhom
Copy link
Contributor

gorhom commented Jun 6, 2019

@blohamen you can use FlatList from import { FlatList } from 'react-native-gesture-handler', it works for me 👍

@avegrv
Copy link

avegrv commented Jul 9, 2019

@gorhom Please, send an example, very interesting. I tried to do something similar, but I didn’t succeed. Your example will be very helpfull.

@avegrv
Copy link

avegrv commented Jul 9, 2019

My example with bottom sheet

import BottomSheet from 'reanimated-bottom-sheet'
import React from 'react';
import {Dimensions, Modal, Text, View} from 'react-native';
import StyleSheet from "react-native-extended-stylesheet";
import {colors} from "components/colors";
import Animated from "react-native-reanimated";
import {Header} from 'react-navigation';
import {getStatusBarHeight, isIphoneX} from "react-native-iphone-x-helper";

const {Value, onChange, call, cond, eq, abs, sub, min} = Animated;

const handleContainerHeight = 38;
const headerBottomPosition = Dimensions.get('window').height - Header.HEIGHT - (isIphoneX() ? getStatusBarHeight() : 0);

export class BottomSheetExample extends React.Component {

  state = {
    visible: false,
    bottomSheetOpened: false,
    contentHeight: null,
  };

  constructor(props) {
    super(props);
    this.position = new Value(1);
    this.opacity = min(abs(sub(this.position, 1)), 0.8);
  }

  openModal () {
    this.setState({visible: true});
  }

  handleInitBottomSheet = (bottomSheet) => {
    if (!this.state.bottomSheetOpened && !!bottomSheet) {
      this.setState({bottomSheetOpened: true}, () => {bottomSheet.snapTo(1)});
    }
  };

  handleCloseModal = () => {
    this.setState({visible: false, bottomSheetOpened: false});
  };

  renderHeader = () => {
    return (
      <View style={styles.handleContainer}>
        <View style={styles.handle} />
        <View style={styles.headerBorder} />
      </View>
    );
  };

  onLayoutContent = event => {
    if (this.state.dimensions) return; // layout was already called
    let {height} = event.nativeEvent.layout;
    this.setState({contentHeight: height})
  };

  renderInner = () => {
    const contentHeight = this.state.contentHeight;
    let height = '100%';
    if (!!contentHeight) {
      height = headerBottomPosition - contentHeight - handleContainerHeight;
    }
    return (
      <View>
        {this.renderContent()}
        <View style={{height, width: '100%', backgroundColor: colors.WHITE}} />
      </View>
    )
  };

  renderContent = () => {
    return (
      <View onLayout={this.onLayoutContent}>
        {[...Array(3)].map((e, i) => (
          <View
            key={i}
            style={{ height: 40, backgroundColor: `#${i % 10}88424` }}
          >
            <Text>computed</Text>
          </View>
        ))}
      </View>
    )
  };

  render() {
    return (
      <Modal
        animationType='fade'
        transparent
        visible={this.state.visible}
        onRequestClose={this.state.onPressClose}
      >
        <View style={styles.container}>
          <Animated.View
            style={[styles.shadow, {opacity: this.opacity}]}
          />
          <BottomSheet
            callbackNode={this.position}
            snapPoints={[0, headerBottomPosition]}
            renderContent={this.renderInner}
            renderHeader={this.renderHeader}
            ref={ref => this.handleInitBottomSheet(ref)}
          />
          <Animated.Code exec={onChange(this.position, [cond(eq(this.position, 1), call([], this.handleCloseModal))])}/>
        </View>
      </Modal>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  wrapper: {
    height: headerBottomPosition
  },
  handleContainer: {
    height: handleContainerHeight,
    alignItems: 'center',
    justifyContent: 'flex-end',
  },
  handle: {
    height: 4,
    width: 48,
    borderRadius: 2,
    marginBottom: 12,
    backgroundColor: colors.WHITE,
  },
  headerBorder: {
    height: 10,
    width: '100%',
    backgroundColor: colors.WHITE,
    borderTopRightRadius: 10,
    borderTopLeftRadius: 10,
  },
  shadow: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    flex: 1,
    backgroundColor: colors.BLACK
  },
});

@FRizzonelli
Copy link

@osdnk I was able to get the flat list working very close to apple maps. But the biggest issue I'm facing is the innerContent height when my flat list has no data or few rows.
My use case is my inner content is determined basing on a service call. There might no rows vs 20 rows.
With the manual calculations I'm not getting the smooth animations that you were able to achieve with your examples. :(

I can share the example I have if you think it will be useful to see the issue

Hello there! I'm close to reach a proper implementation of this animation. But I'm stuck on last part I think. Can you share your example with "apple maps alike" bottom sheet please? Thanks man!

@jeffreybello
Copy link

I really needed this feature as well.

I am basically recreating flatlist while retaining the bottomsheet behavior.

@jeffreybello
Copy link

Maybe @gorhom or @Priyatham51 can shed a light on how to use the Flatlist?

@gorhom
Copy link
Contributor

gorhom commented Oct 6, 2019

@jeffreybello , @avegrv sorry i have been busy lately , however i setup this example to how to handle flatlist scrolling, hope it helps

https://gist.github.com/Gorhom/32db5ee4d0423c8f227fd3c48e3dd991

@jeffreybello
Copy link

@gorhom thank you very much for your input. I appreciate it.
I did the same but now we have a problem where if the bottom sheet is open and you reached the beginning of the list (after scrolling from the bottom) will not pull down to close.

@gorhom
Copy link
Contributor

gorhom commented Oct 8, 2019

@jeffreybello , yeah that part is tricky 😅, you would need to simulate the flatlist scrolling with bottomsheet pan gesture, i will give it a try this weekend

@jeffreybello
Copy link

Yes, ill try my best too but I just got started in RN. But please let us know if you have made progress.

@brian-ws
Copy link

brian-ws commented Oct 17, 2019

This is not perfect, but as good as it gets with simple implementation. First see the video:
http://wayshorter.com/down/bottomsheet2.mp4
Basic idea is that when the FlatList's offset's y value is less than like -30 (-40 works better for me), then I snap the bottom sheet to a different index and also reset the offset of the FlatList.

Here I post code that are relevant:

const NavigateScreen = ({ navigation }) => {
   let snapPoints = [
      screenHeight - topPositionFromTop,
      (screenHeight - topPositionFromTop - bottomPosition) / 2 + bottomPosition,
      bottomPosition
   ];

   //  bottom  sheet  index
   const initialSnapIndex = 1;
   const [ bottomSheetIndex, setBottomSheetIndex ] = useState(initialSnapIndex);
   const drawerCallbackNode = useRef(new Animated.Value(0)).current;
   const [ over30Disabled, setOver30Disabled ] = useState(false);

   useCode(
      onChange(
         drawerCallbackNode,
         block([
            cond(
               lessOrEq(drawerCallbackNode, 0.1),
               call([], () => {
                  bottomSheetIndex === 0 ? null : setBottomSheetIndex(0);
               })
            ),
            cond(
               and(greaterThan(drawerCallbackNode, 0.1), lessOrEq(drawerCallbackNode, 0.6)),
               call([], () => {
                  bottomSheetIndex === 1 ? null : setBottomSheetIndex(1);
                  over30Disabled == true ? setOver30Disabled(false) : null;
               })
            ),
            cond(
               greaterThan(drawerCallbackNode, 0.6),
               call([], () => {
                  bottomSheetIndex === 2 ? null : setBottomSheetIndex(2);
               })
            )
         ])
      ),
      null
   );

   const bottomSheetRef = useRef();
   const flatListRef = useRef();

   const renderContent = () => (
      //  bottom  sheet  inner  area
      <View
         style={{
            backgroundColor: 'white'
         }}
      >
         {/*  list  in  categories  */}
         <View style={{ height: flatListHeightTo }}>
            <FlatList
               ref={flatListRef}
               onScroll={(event) => {
                  if (event.nativeEvent.contentOffset.y < -30) {
                     setOver30Disabled(true);
                     bottomSheetRef.current.snapTo(1);
                     flatListRef.current.scrollToOffset({ offset: 0, animated: true });
                  }
               }}
               scrollEnabled={bottomSheetIndex == 0 && over30Disabled == false ? true : false}
            />
         </View>
      </View>
   );

   return (
      <React.Fragment style={{ flex: 1 }}>
         <View
            style={{
               backgroundColor: 'rgb(15,  104,  186)',
               flex: 1
            }}
         >
            <SafeAreaView style={{ flex: 1 }}>...</SafeAreaView>
         </View>
         <BottomSheet
            ref={bottomSheetRef}
            snapPoints={snapPoints}
            renderHeader={renderHeader}
            renderContent={renderContent}
            initialSnap={initialSnapIndex}
            callbackNode={drawerCallbackNode}
         />
      </React.Fragment>
   );
};

export default NavigateScreen;

@walidvb
Copy link

walidvb commented Nov 11, 2019

hey all,

I'm trying to understand how to put a FlatList and make it fill up the screen. Do i need to explicitly set its height?
I've made a (buggy) example with a FlatList that is visible as a PR here or snacked here. Basically the height of the FlatList seems to always be the max height of the BottomSheet

Maybe this requires an issue of its own(or some documentation, if i'm missing smth).

@pontusab could you perhaps rename this issue to smth like Close sheet when top of FlatList is reached?

@L-U-C-K-Y
Copy link

Great approaches and workarounds here, highly appreciated! 😄

We are also integrating RN Reanimated-Bottomsheet with a Flatlist into our app. Am I correct to assume that we have not found a solution to virtualization yet?

Thanks! 👍🏼

@pontusab pontusab changed the title Add flatlist as content Close sheet when top of FlatList is reached Dec 5, 2019
@oferRounds
Copy link

Very interested in the feature too, on big phones it’s not easy to close the bottom sheet by reaching the header, when it’s fully opened. I think this is also what user intuitively expects

@oferRounds
Copy link

hey, did any one found a workaround for this?

@L-U-C-K-Y
Copy link

Unfortunately, we were not able to find a viable solution for flatlist and have disabled scroll to dismiss for the flatlist area as of now.
Would be huge for us if we can solve it.

@southerneer
Copy link

southerneer commented Jan 9, 2020

I was able to achieve this functionality with an adaptation of an older example in the react-native-gesture-handler repo. It works, but it lacks the elegance/sophistication of react-native-reanimated-bottomsheet. I will clean up and generalize my implementation and post back here with a gist.

@FRizzonelli
Copy link

@southerneer It'd be super useful!

@emroot
Copy link

emroot commented Feb 2, 2020

@southerneer any update on your implementation? I'm running into the same issue where the sheet doesn't close when I reach the top of my flat list (works fine when I have I use a View instead of a FlatList). Here's my code:

/* BottomSheet */

import React, { ReactNode, useRef, useEffect, useState } from "react";
import { Dimensions, View, StyleSheet } from "react-native";
import { useDimensions } from "react-native-hooks";
import BottomSheetRN from "reanimated-bottom-sheet";
import size, { unit } from "../themes/size";
import Elevation from "../themes/elevation";

interface BottomSheetProps {
  children?: ReactNode;
  onClose?: () => void;
  onOpen?: () => void;
  renderHeader?: () => ReactNode;
  isVisible?: boolean;
}

const BottomSheet = (props: BottomSheetProps) => {
  const { children, renderHeader, onClose, onOpen } = props;
  const [isVisible, setIsVisible] = useState(props.isVisible);
  const ref = useRef<BottomSheetRN>();
  const {
    screen: { height },
  } = useDimensions();
  const [headerHeight, setHeaderHeight] = useState(0);

  useEffect(() => setIsVisible(props.isVisible), [props.isVisible]);

  useEffect(() => {
    if (ref.current) {
      if (isVisible) {
        ref.current.snapTo(1);
      } else {
        ref.current.snapTo(0);
      }
    }
  }, [ref.current, isVisible]);

  return (
    <View style={[styles.container, isVisible && styles.containerVisible]}>
      <BottomSheetRN
        ref={ref}
        initialSnap={0}
        snapPoints={[0, "80%"]}
        onCloseEnd={() => {
          setIsVisible(false);
          onClose && onClose();
        }}
        onOpenEnd={() => {
          setIsVisible(true);
          onOpen && onOpen();
        }}
        enabledContentTapInteraction
        enabledGestureInteraction
        enabledContentGestureInteraction
        enabledInnerScrolling
        renderHeader={() => (
          <View
            style={styles.header}
            onLayout={event => setHeaderHeight(event.nativeEvent.layout.height)}
          >
            <View style={styles.panelHeader}>
              <View style={styles.panelHandle} />
            </View>
            {renderHeader()}
          </View>
        )}
        renderContent={() => (
          <View style={[styles.panel, { height: ((height - headerHeight) * 80) / 100 }]}>
            {children}
          </View>
        )}
      />
    </View>
  );
};

export default BottomSheet;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    position: "absolute",
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  },
  containerVisible: {
    zIndex: 100,
    ...Elevation.tertiary,
  },
  panel: {
    backgroundColor: "white",
  },
  header: {
    backgroundColor: "white",
    paddingTop: size.vertical.small,
    borderTopLeftRadius: unit * 2,
    borderTopRightRadius: unit * 2,
  },
  panelHeader: {
    alignItems: "center",
  },
  panelHandle: {
    width: 40,
    height: unit / 2,
    borderRadius: 4,
    backgroundColor: "#00000040",
    marginBottom: 20,
  },
  panelTitle: {
    fontSize: 27,
    height: 35,
  },
  panelSubtitle: {
    fontSize: 14,
    color: "gray",
    height: 30,
    marginBottom: 10,
  },
  panelButton: {
    padding: 20,
    borderRadius: 10,
    backgroundColor: "#318bfb",
    alignItems: "center",
    marginVertical: 10,
  },
  panelButtonTitle: {
    fontSize: 17,
    fontWeight: "bold",
    color: "white",
  },
  photo: {
    width: "100%",
    height: 225,
    marginTop: 30,
  },
  map: {
    height: "100%",
    width: "100%",
  },
});

/* AutocompleteSheet */


import React, { useState, useEffect } from "react";
import { View, StyleSheet, TextInput, ActivityIndicator } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import useDebounce from "../hooks/useDebounce";
import { ListItem } from "../types";
import AutocompleteRow from "./AutocompleteRow";
import BottomSheet from "./BottomSheet";
import size from "../themes/size";
import Elevation from "../themes/elevation";
import color from "../themes/color";

interface AutocompletePanelProps {
  isVisible?: boolean;
  onChange: (term: string) => Promise<void>;
  onVisibilityChange: (isVisible: boolean) => void;
  onSelect?: (item: any, index?: number) => void;
  keyExtractor?: (item: any, index?: number) => string;
  results: Array<ListItem>;
}

const AutocompletePanel = ({
  isVisible,
  onChange,
  onSelect,
  onVisibilityChange,
  keyExtractor,
  results,
}: AutocompletePanelProps) => {
  const [searchTerm, onChangeSearchTerm] = useState("");
  const debouncedSearchTerm = useDebounce(searchTerm, 400);
  const [isLoading, setIsLoading] = useState(false);
  useEffect(() => {
    (async () => {
      setIsLoading(true);
      await onChange(debouncedSearchTerm);
      setIsLoading(false);
    })();
  }, [debouncedSearchTerm]);
  return (
    <BottomSheet
      isVisible={isVisible}
      onClose={() => onVisibilityChange(false)}
      onOpen={() => onVisibilityChange(true)}
      renderHeader={() => (
        <View style={[styles.container, styles.row]}>
          <TextInput
            style={styles.input}
            placeholder={"Type text here"}
            onChangeText={text => onChangeSearchTerm(text)}
            value={searchTerm}
          />
          {isLoading && <ActivityIndicator size='small' color='#4631EB' style={styles.loading} />}
        </View>
      )}
    >
      <FlatList
        data={results}
        scrollEnabled
        keyExtractor={keyExtractor}
        keyboardShouldPersistTaps='always'
        style={styles.listView}
        renderItem={({ item, index }) => (
          <AutocompleteRow
            id={item.id}
            index={index}
            onSelect={onSelect}
            properties={item.properties}
          />
        )}
      />
    </BottomSheet>
  );
};

export default AutocompletePanel;

const styles = StyleSheet.create({
  container: {
    paddingHorizontal: size.horizontal.large,
  },
  listView: {
    backgroundColor: "red",
    paddingHorizontal: 24,
  },
  loading: {
    position: "absolute",
    top: 8,
    right: 32,
  },
  row: {
    flexDirection: "row",
  },
  input: {
    width: "100%",
    paddingHorizontal: size.horizontal.regular,
    paddingVertical: size.vertical.medium,
    fontSize: 18,
    borderRadius: 8,
    borderWidth: 0.5,
    borderColor: "#ddd",
    ...Elevation.tertiary,
    backgroundColor: color.white,
  },
});

@yasir-netlinks
Copy link

yasir-netlinks commented Feb 2, 2020

Hi guys, @gorhom solution is still way buggy and doesn't work properly , where first and last item gets cut off. Any solution regarding this is very very much appreciated.
Though, not using FlatList and just mapping items inside a view supposedly should work fine, unfortunately likewise, a problem persists where I can't scroll to the end of the list

@wobsoriano
Copy link

I have to switch to another component because of this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests