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?
@pontusab I think it possible to add
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.
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.
@osdnk any comments or suggestion for us to add a FlatList in the content?
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?
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 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
+1 for virtualization support for the the bottom sheet (maybe based on a generic react-native-gesture-handler based scrollview with virtualization support).
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?
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
@blohamen you can use FlatList from import { FlatList } from 'react-native-gesture-handler', it works for me 馃憤
@Gorhom Please, send an example, very interesting. I tried to do something similar, but I didn鈥檛 succeed. Your example will be very helpfull.
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
},
});
@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!
I really needed this feature as well.
I am basically recreating flatlist while retaining the bottomsheet behavior.
Maybe @Gorhom or @Priyatham51 can shed a light on how to use the Flatlist?
@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
@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.
@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
Yes, ill try my best too but I just got started in RN. But please let us know if you have made progress.
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;
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_?
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! 馃憤馃徏
Very interested in the feature too, on big phones it鈥檚 not easy to close the bottom sheet by reaching the header, when it鈥檚 fully opened. I think this is also what user intuitively expects
hey, did any one found a workaround for this?
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.
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.
@southerneer It'd be super useful!
@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,
},
});
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
I have to switch to another component because of this.
Most helpful comment
@blohamen you can use
FlatListfromimport { FlatList } from 'react-native-gesture-handler', it works for me 馃憤