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

feat: allow dynamic snap points #81

Merged
merged 12 commits into from
Nov 27, 2020
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ To start the sheet closed and snap to initial index when it's mounted.

> `required:` NO | `type:` boolean | `default:` false

#### `handleHeight`

Handle height to help adjust snap points.

> `required:` NO | `type:` number | `default:` undefined

#### `animationDuration`

Snapping animation duration.
Expand Down
9 changes: 9 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ const App = () => {
require('./screens/advanced/MapExample').default
}
/>
<Stack.Screen
name="Advanced/DynamicSnapPointExample"
options={{
title: 'Dynamic Snap Point',
}}
getComponent={() =>
require('./screens/advanced/DynamicSnapPointExample').default
}
/>
</Stack.Navigator>
</NavigationContainer>
</AppearanceProvider>
Expand Down
51 changes: 30 additions & 21 deletions example/src/components/handle/Handle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,33 @@ interface HandleProps extends BottomSheetHandleProps {

const Handle: React.FC<HandleProps> = ({ style, animatedPositionIndex }) => {
//#region animations
const borderTopRadius = interpolate(animatedPositionIndex, {
inputRange: [1, 2],
outputRange: [20, 0],
extrapolate: Extrapolate.CLAMP,
});
const indicatorTransformOriginY = interpolate(animatedPositionIndex, {
inputRange: [0, 1, 2],
outputRange: [-1, 0, 1],
extrapolate: Extrapolate.CLAMP,
});
const leftIndicatorRotate = interpolate(animatedPositionIndex, {
inputRange: [0, 1, 2],
outputRange: [toRad(-30), 0, toRad(30)],
extrapolate: Extrapolate.CLAMP,
});
const borderTopRadius = useMemo(
() =>
interpolate(animatedPositionIndex, {
inputRange: [1, 2],
outputRange: [20, 0],
extrapolate: Extrapolate.CLAMP,
}),
[animatedPositionIndex]
);
const indicatorTransformOriginY = useMemo(
() =>
interpolate(animatedPositionIndex, {
inputRange: [0, 1, 2],
outputRange: [-1, 0, 1],
extrapolate: Extrapolate.CLAMP,
}),
[animatedPositionIndex]
);
const leftIndicatorRotate = useMemo(
() =>
interpolate(animatedPositionIndex, {
inputRange: [0, 1, 2],
outputRange: [toRad(-30), 0, toRad(30)],
extrapolate: Extrapolate.CLAMP,
}),
[animatedPositionIndex]
);
const rightIndicatorRotate = interpolate(animatedPositionIndex, {
inputRange: [0, 1, 2],
outputRange: [toRad(30), 0, toRad(-30)],
Expand All @@ -42,8 +54,7 @@ const Handle: React.FC<HandleProps> = ({ style, animatedPositionIndex }) => {
borderTopRightRadius: borderTopRadius,
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[style]
[borderTopRadius, style]
);
const leftIndicatorStyle = useMemo(
() => ({
Expand All @@ -57,8 +68,7 @@ const Handle: React.FC<HandleProps> = ({ style, animatedPositionIndex }) => {
}
),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
[indicatorTransformOriginY, leftIndicatorRotate]
);
const rightIndicatorStyle = useMemo(
() => ({
Expand All @@ -72,8 +82,7 @@ const Handle: React.FC<HandleProps> = ({ style, animatedPositionIndex }) => {
}
),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
[indicatorTransformOriginY, rightIndicatorRotate]
);
//#endregion

Expand Down
11 changes: 9 additions & 2 deletions example/src/components/weather/Weather.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native';
import Animated, { Extrapolate, interpolate } from 'react-native-reanimated';
import { useAppearance } from '../../hooks';
import Text from '../text';
import { SEARCH_HANDLE_HEIGHT } from '../../components/searchHandle';

interface WeatherProps {
animatedPosition: Animated.Node<number>;
Expand All @@ -21,8 +22,14 @@ const Weather = ({ animatedPosition, snapPoints }: WeatherProps) => {
transform: [
{
translateY: interpolate(animatedPosition, {
inputRange: [snapPoints[0], snapPoints[1]],
outputRange: [-snapPoints[0], -snapPoints[1]],
inputRange: [
snapPoints[0] + SEARCH_HANDLE_HEIGHT,
snapPoints[1] + SEARCH_HANDLE_HEIGHT,
],
outputRange: [
-(snapPoints[0] + SEARCH_HANDLE_HEIGHT),
-(snapPoints[1] + SEARCH_HANDLE_HEIGHT),
],
extrapolate: Extrapolate.CLAMP,
}),
},
Expand Down
4 changes: 4 additions & 0 deletions example/src/screens/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ const data = [
name: 'Map',
slug: 'Advanced/MapExample',
},
{
name: 'Dynamic Snap Point',
slug: 'Advanced/DynamicSnapPointExample',
},
],
},
].reverse();
Expand Down
3 changes: 0 additions & 3 deletions example/src/screens/advanced/CustomHandleExample.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import { useHeaderHeight } from '@react-navigation/stack';
import BottomSheet from '@gorhom/bottom-sheet';
import Handle from '../../components/handle';
import Button from '../../components/button';
Expand All @@ -12,7 +11,6 @@ const CustomHandleExample = () => {

// hooks
const bottomSheetRef = useRef<BottomSheet>(null);
const headerHeight = useHeaderHeight();

// variables
const snapPoints = useMemo(() => [150, 300, 450], []);
Expand Down Expand Up @@ -88,7 +86,6 @@ const CustomHandleExample = () => {
enabled={enabled}
snapPoints={snapPoints}
initialSnapIndex={1}
topInset={headerHeight}
handleComponent={Handle}
>
<ContactList type="View" count={3} header={renderHeader} />
Expand Down
147 changes: 147 additions & 0 deletions example/src/screens/advanced/DynamicSnapPointExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';
import { useSafeArea } from 'react-native-safe-area-context';
import { Easing } from 'react-native-reanimated';
import Button from '../../components/button';

const DynamicSnapPointExample = () => {
// state
const [count, setCount] = useState(0);
const [contentHeight, setContentHeight] = useState(0);

// hooks
const bottomSheetRef = useRef<BottomSheet>(null);
const { bottom: safeBottomArea } = useSafeArea();

// variables
const snapPoints = useMemo(() => [0, contentHeight], [contentHeight]);

// callbacks
const handleIncreaseContentPress = useCallback(() => {
setCount(state => state + 1);
}, []);
const handleDecreaseContentPress = useCallback(() => {
setCount(state => Math.max(state - 1, 0));
}, []);
const handleExpandPress = useCallback(() => {
bottomSheetRef.current?.expand();
}, []);
const handleClosePress = useCallback(() => {
bottomSheetRef.current?.close();
}, []);
const handleOnLayout = useCallback(
({
nativeEvent: {
layout: { height },
},
}) => {
// console.log('SCREEN \t\t', 'handleOnLayout', height);
setContentHeight(height);
},
[]
);

// styles
const contentContainerStyle = useMemo(
() => ({
...styles.contentContainerStyle,
paddingBottom: safeBottomArea,
}),
[safeBottomArea]
);
const emojiContainerStyle = useMemo(
() => ({
...styles.emojiContainer,
height: 50 * count,
}),
[count]
);

// renders
const renderBackground = useCallback(
() => <View style={styles.background} />,
[]
);

// console.log('SCREEN \t\t', 'render \t');
return (
<View style={styles.container}>
<Button
label="Expand"
style={styles.buttonContainer}
onPress={handleExpandPress}
/>
<Button
label="Close"
style={styles.buttonContainer}
onPress={handleClosePress}
/>
<BottomSheet
ref={bottomSheetRef}
snapPoints={snapPoints}
initialSnapIndex={1}
animateOnMount={true}
animationEasing={Easing.out(Easing.quad)}
animationDuration={250}
backgroundComponent={renderBackground}
>
<BottomSheetView
style={contentContainerStyle}
onLayout={handleOnLayout}
>
<Text style={styles.message}>
Could this sheet resize to its content height ?
</Text>
<View style={emojiContainerStyle}>
<Text style={styles.emoji}>😍</Text>
</View>
<Button
label="Yes"
style={styles.buttonContainer}
onPress={handleIncreaseContentPress}
/>
<Button
label="Mayby"
style={styles.buttonContainer}
onPress={handleDecreaseContentPress}
/>
</BottomSheetView>
</BottomSheet>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
},
background: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'white',
},
buttonContainer: {
marginBottom: 6,
},
contentContainerStyle: {
paddingTop: 12,
paddingHorizontal: 24,
backgroundColor: 'white',
},
message: {
fontSize: 24,
fontWeight: '600',
marginBottom: 12,
},
emoji: {
fontSize: 156,
textAlign: 'center',
alignSelf: 'center',
},
emojiContainer: {
justifyContent: 'center',
},
});

export default DynamicSnapPointExample;
19 changes: 10 additions & 9 deletions example/src/screens/advanced/MapExample.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { View, StyleSheet, Dimensions, StatusBar } from 'react-native';
import { View, StyleSheet, Dimensions } from 'react-native';
import MapView from 'react-native-maps';
import { interpolate, Extrapolate, Easing, max } from 'react-native-reanimated';
import { useValue } from 'react-native-redash';
Expand All @@ -12,9 +12,7 @@ import BottomSheet, {
} from '@gorhom/bottom-sheet';
import withModalProvider from '../withModalProvider';
import { createLocationListMockData, Location } from '../../utils';
import SearchHandle, {
SEARCH_HANDLE_HEIGHT,
} from '../../components/searchHandle';
import SearchHandle from '../../components/searchHandle';
import LocationItem from '../../components/locationItem';
import LocationDetails, {
LOCATION_DETAILS_HEIGHT,
Expand All @@ -23,7 +21,7 @@ import LocationDetailsHandle from '../../components/locationDetailsHandle';
import Weather from '../../components/weather';
import BlurredBackground from '../../components/blurredBackground';

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

const MapExample = () => {
// refs
Expand All @@ -39,11 +37,11 @@ const MapExample = () => {
const data = useMemo(() => createLocationListMockData(15), []);
const snapPoints = useMemo(
() => [
SEARCH_HANDLE_HEIGHT + bottomSafeArea,
bottomSafeArea,
LOCATION_DETAILS_HEIGHT + bottomSafeArea,
SCREEN_HEIGHT - topSafeArea - (StatusBar.currentHeight ?? 0),
SCREEN_HEIGHT,
],
[topSafeArea, bottomSafeArea]
[bottomSafeArea]
);
const animatedPosition = useValue<number>(0);
const animatedModalPosition = useValue<number>(0);
Expand Down Expand Up @@ -91,6 +89,7 @@ const MapExample = () => {
{
initialSnapIndex: 1,
snapPoints,
topInset: topSafeArea,
animatedPosition: animatedModalPosition,
animationDuration: 500,
animationEasing: Easing.out(Easing.exp),
Expand All @@ -103,6 +102,7 @@ const MapExample = () => {
[
snapPoints,
animatedModalPosition,
topSafeArea,
present,
handleCloseLocationDetails,
handleLocationDetailSheetChanges,
Expand Down Expand Up @@ -161,6 +161,7 @@ const MapExample = () => {
pointerEvents="none"
animatedOpacity={animatedOverlayOpacity}
/>

<Weather
animatedPosition={weatherAnimatedPosition}
snapPoints={snapPoints}
Expand All @@ -172,9 +173,9 @@ const MapExample = () => {
topInset={topSafeArea}
animatedPosition={animatedPosition}
animatedPositionIndex={animatedPositionIndex}
handleComponent={SearchHandle}
animationDuration={500}
animationEasing={Easing.out(Easing.exp)}
handleComponent={SearchHandle}
backgroundComponent={BlurredBackground}
onChange={handleSheetChanges}
>
Expand Down
Loading