Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions example/app/src/screens/advanced/ScrollableDynamicHeightExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React, { useCallback, useRef, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import BottomSheet, {
BottomSheetScrollView,
useMaxHeightScrollableBottomSheet,
} from '@gorhom/bottom-sheet';
import { Button } from '../../components/button';
type CSSHeight = `${number}%` | number;

const DynamicSnapPointExample = () => {
// state
const [count, setCount] = useState(3);
const [maxHeight, setMaxHeight] = useState<CSSHeight>();
// hooks
const bottomSheetRef = useRef<BottomSheet>(null);
const {
animatedHandleHeight,
animatedSnapPoints,
animatedContentHeight,
handleContentLayout,
innerScrollViewAnimatedStyles,
} = useMaxHeightScrollableBottomSheet(maxHeight);
// callbacks

const handleExpandPress = useCallback(() => {
bottomSheetRef.current?.expand();
}, []);
const handleClosePress = useCallback(() => {
bottomSheetRef.current?.close();
}, []);

const changeItemsCount = useCallback((direction: 1 | -1) => {
setCount(state => state + direction);
}, []);

// styles

// renders
return (
<View style={styles.container}>
<Button label="Expand" onPress={handleExpandPress} />
<Button label="Close" onPress={handleClosePress} />

<BottomSheet
ref={bottomSheetRef}
snapPoints={animatedSnapPoints}
handleHeight={animatedHandleHeight}
contentHeight={animatedContentHeight}
enablePanDownToClose={true}
animateOnMount={true}
>
<BottomSheetScrollView
style={innerScrollViewAnimatedStyles}
onLayout={handleContentLayout}
scrollEnabled={true}
>
<View style={styles.actionsContainer}>
<View style={styles.buttonGroup}>
<Button
label="default (50%)"
onPress={() => {
setMaxHeight(undefined);
handleExpandPress();
}}
/>
<Button
label="75%"
onPress={() => {
setMaxHeight('75%');
handleExpandPress();
}}
/>
<Button
label="100%"
onPress={() => {
setMaxHeight('100%');
handleExpandPress();
}}
/>
<Button
label="150px"
onPress={() => {
setMaxHeight(150);
handleExpandPress();
}}
/>
</View>
<View style={styles.buttonGroup}>
<Button
style={styles.button}
label="Add Item"
onPress={() => changeItemsCount(1)}
/>
<View style={styles.gap} />
<Button
style={styles.button}
label="Remove Item"
onPress={() => changeItemsCount(-1)}
/>
</View>
<View style={styles.buttonGroup}>
<Button
style={styles.button}
label="Add 100 items"
onPress={() => setCount(100)}
/>

<View style={styles.gap} />
<Button
style={styles.button}
label="Remove all"
onPress={() => setCount(0)}
/>
</View>
</View>
{Array.from({ length: count }).map((_, index) => (
<View key={index} style={styles.item}>
<Text style={styles.itemText}>👋 I am a item 👋</Text>
</View>
))}
</BottomSheetScrollView>
</BottomSheet>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
},
contentContainerStyle: {
paddingTop: 12,
paddingBottom: 6,
paddingHorizontal: 24,
},
item: {
alignContent: 'center',
height: 50,
width: '100%',
},
itemText: {
fontSize: 16,
width: '100%',
textAlign: 'center',
},
button: {
flex: 1,
flexShrink: 1,
},
buttonGroup: {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between',
},
gap: {
width: 12,
height: '100%',
},
actionsContainer: {
padding: 12,
},
});

export default DynamicSnapPointExample;
6 changes: 6 additions & 0 deletions example/app/src/screens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ export const screens = [
require('./advanced/customGestureHandling/CustomGestureHandling')
.default,
},
{
name: 'Max height scrollable sheet',
slug: 'Advanced/ScrollableDynamicHeightExample',
getScreen: () =>
require('./advanced/ScrollableDynamicHeightExample').default,
},
] as ShowcaseExampleScreenType[],
},
];
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export { useNormalizedSnapPoints } from './useNormalizedSnapPoints';
export { useReactiveSharedValue } from './useReactiveSharedValue';
export { useBottomSheetDynamicSnapPoints } from './useBottomSheetDynamicSnapPoints';
export { useBottomSheetGestureHandlers } from './useBottomSheetGestureHandlers';
export { useMaxHeightScrollableBottomSheet } from './useMaxHeightScrollableBottomSheet';
53 changes: 53 additions & 0 deletions src/hooks/useMaxHeightScrollableBottomSheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useWindowDimensions } from 'react-native';
import { useDerivedValue, useAnimatedStyle } from 'react-native-reanimated';
import { useBottomSheetDynamicSnapPoints } from './useBottomSheetDynamicSnapPoints';
type CSSHeight = `${number}%` | number;

export const useMaxHeightScrollableBottomSheet = (
maxHeight?: CSSHeight | false
) => {
const deviceHeight = useWindowDimensions().height;
const {
animatedHandleHeight,
animatedSnapPoints,
animatedContentHeight,
handleContentLayout,
} = useBottomSheetDynamicSnapPoints(['CONTENT_HEIGHT']);

const derivedSnapPoints = useDerivedValue(() => {
// we get the dynamic size from the animatedSnapPoints
const dynamicHeight = animatedSnapPoints.value[0];
// if no fillHeight is provided, we use the default height (50%)
const fixedHeight = maxHeight === false ? undefined : maxHeight ?? '50%';
if (!fixedHeight) return [dynamicHeight];
// we calculate the fillHeight based on the device height
const paredFixedHeight =
typeof fixedHeight === 'number'
? fixedHeight
: (parseInt(fixedHeight.replace('%', ''), 10) / 100) * deviceHeight;

return typeof dynamicHeight === 'number' &&
dynamicHeight <= paredFixedHeight
? [dynamicHeight]
: [paredFixedHeight];
}, [deviceHeight, maxHeight]);

// changing the height of the inner scroll view
// based on the snap point calculated above
const innerScrollViewAnimatedStyles = useAnimatedStyle(() => {
// we do not set maxHeight until the hook is ready
// due to performance issues
if (animatedHandleHeight.value === -999) return {};
return {
maxHeight: derivedSnapPoints.value[0],
};
}, []);

return {
animatedHandleHeight,
animatedContentHeight,
handleContentLayout,
animatedSnapPoints: derivedSnapPoints,
innerScrollViewAnimatedStyles,
};
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { useBottomSheetTimingConfigs } from './hooks/useBottomSheetTimingConfigs
export { useBottomSheetInternal } from './hooks/useBottomSheetInternal';
export { useBottomSheetModalInternal } from './hooks/useBottomSheetModalInternal';
export { useBottomSheetDynamicSnapPoints } from './hooks/useBottomSheetDynamicSnapPoints';
export { useMaxHeightScrollableBottomSheet } from './hooks/useMaxHeightScrollableBottomSheet';
export { useScrollEventsHandlersDefault } from './hooks/useScrollEventsHandlersDefault';
export { useGestureEventsHandlersDefault } from './hooks/useGestureEventsHandlersDefault';
export { useBottomSheetGestureHandlers } from './hooks/useBottomSheetGestureHandlers';
Expand Down