BottomSheet with react-native-reanimated, react-native-gesture-handler and react-native-redash
This library provide multiple components but most importantly the BottomSheet. The BottomSheet is highly customizable. Visit the homepage for example: https://marcuzgabriel.github.io/reanimated-bottom-sheet/
Props
BottomSheet
Prop | Type | Description |
---|---|---|
isBottomSheetInactive | boolean | Set the bottom to an inactive state. Can be used for async handling og UX requirements |
initializeBottomSheetAsClosed | boolean | In some cases it might be relevant to show the background content before showing the bottomSheet |
contentResizeHeightTriggerOnFocusedInputField | number | At which content height should a resize in content height occour when the input field is focused? |
contentResizeHeightOnFocusedInputField | number | If contentResizeHeightTriggerOnFocusedInputField is met what should be the new content height size when input field is focused? |
snapEffectDirection | Animated.SharedValue | Used together with SnapEffect component. It tells the BottomSheet how to react to the effect. Please look in examples for more information |
snapPointBottom* | number | This prop is required for the BottomSheet to work |
extraOffset | number | If you need some extra offset when it comes to the panning event hitting the footer |
borderTopRightRadius and borderTopLeftRadius | number | Sets the border top radius’ |
backgroundColor | string | Sets the background color |
contentComponent | node | Content component |
footerComponent | node | Footer component |
headerComponent | node | Header component |
hideFooterOnCardCollapse | object | { isEnabled: boolean, offset: number } |
hideContentOnCardCollapse | object | { isEnabled: boolean, offset: number } |
scrollArrowTopComponent (currently disabled) | node | Scroll arrow top component |
scrollArrowBottomComponent (currently disabled) | node | Scroll arrow bottom component |
scrollArrows = { isEnabled: boolean, fill: string, dimensions: number, topArrowOffset: number, bottomArrowOffset: number } | object | When there is no scrollArrowBottom- or top component then this object can be used for styling the scroll arrows. |
extraSnapPointBottomOffset | number | Minor differences occours depending on the Platform. This prop helps to get the perfect snap point on all platforms |
keyboardAvoidBottomMargin (currently disabled) | number | An extra margin wrapper is implemented instead. The prop was used to create extra spacings when an input field is focused |
maxHeight | number | max height of the bottom sheet |
header = { height: number } | object | If there is no header component then this object can be used to style the header |
morphingArrow = { isEnabled: boolean, offset: number, fill: string } | object | As there currently is a bug on web when interpolating SVG’s with reanimated, then the morphing arrow can be disabled for specific platforms using this prop |
fadingScrollEdges = { isEnabled: boolean, androidFadingEdgeLength: number, iOSAndWebFadingEdgeHeight: number, nativeBackgroundColor: string, webBackgroundColorTop: { from: string to: string}, webBackgroundColorBottom: { from: string, to: string } | object | This prop ensures that there is a scrolling edge when the content is scrollable |
outerScrollEvent = { isEnabled?: boolean, scrollY?: Animated.SharedValue, autoScrollTriggerLength?: number } | object | Connect an outer scrolling event that the bottom sheet should react to |
testID | string | add testID to the bottomSheet |
openBottomSheetRequest & closeBottomSheetRequest | { isEnabled: boolean; callback: ((cb) => void) => void } | Custom trigger functions to make the bottom sheet go to bottom or top |
getCurrentConfigRequest(config) | function with callback | This function will provide the current configuration |
onLayoutRequest(cardHeight) | function with callback | In some use cases the card height of the BottomSheet might become useful |
Integration
React integration
import React from 'react';
import { Platform, useWindowDimensions } from 'react-native';
import styled from 'styled-components/native';
import Animated, {
useSharedValue,
useAnimatedScrollHandler,
useAnimatedRef,
} from 'react-native-reanimated';
import { BottomSheet, SnapEffect } from '@marcuzgabriel/reanimated-animation-library';
const HEADER_HEIGHT = 50;
const EXTRA_SNAP_POINT_OFFSET = 30;
const isAndroid = Platform.OS === 'android';
const fakeScrollItem = [
{
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur.
`,
},
];
const Wrapper = styled.View<{ windowHeight: number }>`
position: relative;
height: ${({ windowHeight }): number => windowHeight}px;
width: 100%;
`;
const Content = styled.View`
width: 100%;
height: 400;
background-color: purple;
`;
const Header = styled.View`
width: 100%;
height: 100px;
background: black;
justify
`;
const Text = styled.Text``;
const FakeContentWrapper = styled.View<{ windowHeight: number }>`
background: white;
height: ${({ windowHeight }): number => windowHeight}px;
width: 100%;
padding: 32px 16px;
`;
const ScrollViewWithSnapEffect: React.FC = () => {
const scrollViewRef = useAnimatedRef<Animated.ScrollView>();
const scrollY = useSharedValue(0);
const cardHeight = useSharedValue(0);
const snapEffectDirection = useSharedValue('');
const windowHeight = useWindowDimensions().height;
const onScrollHandler = useAnimatedScrollHandler({
onScroll: e => {
scrollY.value = e.contentOffset.y;
},
});
return (
<Wrapper windowHeight={windowHeight}>
<Animated.ScrollView
ref={scrollViewRef}
bounces={false}
alwaysBounceVertical={false}
onScroll={onScrollHandler}
scrollEventThrottle={16}
>
<SnapEffect cardHeight={cardHeight} snapEffectDirection={snapEffectDirection}>
{fakeScrollItem.map(({ text }, i) => (
<FakeContentWrapper windowHeight={windowHeight} key={`${i}_${text}`}>
<Text>{text}</Text>
</FakeContentWrapper>
))}
</SnapEffect>
</Animated.ScrollView>
<BottomSheet
scrollY={scrollY}
fadingScrollEdges={{ isEnabled: false }}
morphingArrow={{ isEnabled: Platform.OS !=='web', offset: 20 }}
keyboardAvoidBottomMargin={isAndroid ? 16 : 0}
snapEffectDirection={snapEffectDirection}
snapPointBottom={HEADER_HEIGHT + EXTRA_SNAP_POINT_OFFSET}
onLayoutRequest={(height: number): void => {
cardHeight.value = height;
}}
contentComponent={<Content />}
/>
</Wrapper>
);
};
export default ScrollViewWithSnapEffect;
Expo integration
npm install @marcuzgabriel/[email protected]
https://github.com/marcuzgabriel/reanimated-animation-library/packages/813007
Update app.json accordingly and remember to pod install and build the projects properly.
{
"name": "MyTSProject",
"displayName": "MyTSProject",
"expo": {
"name": "MyTSProject",
"slug": "MyTSProject",
"version": "1.0.0",
"assetBundlePatterns": [
"**/*"
],
"web": {
"build": {
"babel": {
"include": [
"@marcuzgabriel/reanimated-animation-library"
]
}
}
}
}
}
Performance
Performance observations
The only time a performance decrease occours is when the native keyboad appears. This type of performance decrease will always happend with or without reanimated. If you experience any other performance decrease, please let me know ?
Observations / notes
Observations
Latest react-native-gesture-handler version vs old and latest react-native-reanimated vs old
Package | Platform | Observations / bugs |
---|---|---|
#react-native-reanimated | web | The package has a bug on web when it comes to interpolating SVG’s. https://github.com/software-mansion/react-native-reanimated/issues/1951 |
#react-native-gesture-handler | all | There are quite some limitation from previously. Before react-native-gesture-handler handled the touches automatically with no further control to it. Now all pan gestures needs to be controlled with waitFor and simoustanously. |
#react-native-gesture-handler | web & Android | react-native-gesture-handler and the props waitFor and simultaneously don’t work properly for either web or Android. The behaviourial indefferences can be observed when you play around with simultaneously handlers. On iOS simultaneously handlers follow along (works as expected) where on Android and web they don’t. Please ask if you need an example. https://github.com/software-mansion/react-native-gesture-handler/issues/420 https://github.com/software-mansion/react-native-gesture-handler/issues/927 |
#useAnimatedGestureHandler | all | this approach is nice for simple use case but has no gesture state control. The same goes for useAnimatedScrollHander. Mixing, constraining and manipulating gestures directly is no longer achievably. |
#useAnimatedReaction | all | The oldschool approach with react-native-animated have a global scope for animations also known as the <Animation.Code> scope where values from different events can be mixed together and manipulated in direct time. It is rather difficult to achieve the same flexibility with the new hooks approach. Positively the new approach is probably more effective with the hooks and provides a smoother animation experience. useAnimatedReaction scope is the hook that comes the closest to <Animation.Code> |
#react-native-reanimated | all | A much better control of animations is now achieveable with HOA’s (higher-order animations) as the animations functions as a first-class citizen. A few examples can be found in the library under ./src/hoas |
#useWindowDimensions | Android | A micro difference occours when setting the child height within a Animated.ScrollView component to the window height with the use of useWindowDimensions. When exctracting the child height with (onContentSizeChange) then the height says 683.4285888671875 vs the windowheight 683.4285714285714. An offset constant is therefore needed to determine scrollability. |
simulator update behaviour | all | As reanimated is using worklets and other functionality that runs on a different thread, then a change in props might first work when the simulator is refreshed |
#useAnimatedStyle | iOS | Avoid attach dependencies to this type of hook. Freezing behaviour is likely to occour. Have multiple examples where iOS crashes without any further information. |
iOS simulator / Xcode bug | iOS / Xcode | Initially the keyboard is NOT toggled on the iOS simulator. This causes an ‘in-between’ animation to occour when an input field is focused. If the keyboard is toggled while the BottomSheet is in an ‘in-between’ state and it is the first run on the simulator then the simulator will crash while trying to collapse the BottomSheet. The crash message is cryptic. This bug has something to do with Xcode / simulator and is not reproducable on a live device where the keyboard is always shown / toggled by default. This bug dissapears when restarting the simulator after the crash. Ask for an example. |
#react-native-reanimated | all | As a programmer there is little to no information on why a worklet crashes in the console. The troubleshooting with reanimated is therefore (from a personal point of view) quite messy and time consuming. |
#react-native-reanimated | iOS | Rarely the simulator can crash when selecting an input field that also have an animation. When the crash occours it is reproducable until the moment the metro bundler and simulator is refreshed. The crash is not reproducable on a real device. |
debugging | all | Debugging tool has to be flipper: Turbomodules on the native side is not supported with Chrome software-mansion/react-native-reanimated#1663 |
useAnimatedStyle | web | It is not possible to have both a translate and interpolate opacity animation at the same time. Flickering will occour. |