react-native-reanimated#Easing TypeScript Examples
The following examples show how to use
react-native-reanimated#Easing.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: AnimatedHelper.ts From curved-bottom-navigation-bar with MIT License | 6 votes |
withSharedTransition = (
value: Animated.SharedValue<boolean>,
config: WithTimingConfig = {
duration: 500,
easing: Easing.bezier(0, 0.55, 0.45, 1),
}
): Animated.SharedValue<number> => {
'worklet';
return useDerivedValue(() =>
value.value ? withTiming(1, config) : withTiming(0, config)
);
}
Example #2
Source File: useInit.ts From react-native-gallery-toolkit with MIT License | 6 votes |
usedWorklets = {
withTiming,
withSpring,
bezier: Easing.bezier,
interpolate,
withDecay,
useAnimatedGestureHandler,
...usedVectors,
} as { [key: string]: any }
Example #3
Source File: useControls.tsx From react-native-gallery-toolkit with MIT License | 6 votes |
export function useControls() {
const controlsHidden = useSharedValue(false);
const translateYConfig = {
duration: 400,
easing: Easing.bezier(0.33, 0.01, 0, 1),
};
const controlsStyles = useAnimatedStyle(() => {
return {
opacity: controlsHidden.value ? withTiming(0) : withTiming(1),
transform: [
{
translateY: controlsHidden.value
? withTiming(-100, translateYConfig)
: withTiming(0, translateYConfig),
},
],
position: 'absolute',
top: 0,
width: '100%',
zIndex: 1,
};
});
const setControlsHidden = useWorkletCallback((hidden: boolean) => {
if (controlsHidden.value === hidden) {
return;
}
controlsHidden.value = hidden;
}, []);
return {
controlsHidden,
controlsStyles,
setControlsHidden,
};
}
Example #4
Source File: animated-stroke.tsx From react-native-checkbox-reanimated with MIT License | 6 votes |
AnimatedStroke = ({ progress, ...pathProps }: AnimatedStrokeProps) => {
const [length, setLength] = useState(0)
const ref = useRef<typeof AnimatedPath>(null)
const animatedProps = useAnimatedProps(() => ({
strokeDashoffset: Math.max(
0,
length - length * Easing.bezierFn(0.37, 0, 0.63, 1)(progress.value) - 0.1
)
}))
return (
<AnimatedPath
animatedProps={animatedProps}
// @ts-ignore
onLayout={() => setLength(ref.current!.getTotalLength())}
// @ts-ignore
ref={ref}
strokeDasharray={length}
{...pathProps}
/>
)
}
Example #5
Source File: RandomArc.tsx From reanimated-arc with MIT License | 6 votes |
RandomArc = () => {
const arcAngle = useRef(new Reanimated.Value(Math.random() * 360));
const animate = () =>
Reanimated.timing(arcAngle.current, {
toValue: Math.random() * 360,
easing: Easing.inOut(Easing.quad),
duration: 1000,
}).start();
useEffect(() => {
setTimeout(() => {
animate();
}, 2000);
setTimeout(() => {
animate();
}, 3000);
}, []);
return (
<>
<ReanimatedArcBase
color="coral"
diameter={200}
width={20}
arcSweepAngle={arcAngle.current}
lineCap="round"
rotation={Reanimated.divide(arcAngle.current, 2)}
/>
<View style={{paddingTop: 20}}>
<Button title="Animate Arc!" onPress={animate} />
</View>
</>
);
}
Example #6
Source File: AnimatedHelper.ts From curved-bottom-navigation-bar with MIT License | 6 votes |
useSharedTransition = (
state: boolean,
config: WithTimingConfig = {
duration: 500,
easing: Easing.bezier(0.33, 0.01, 0, 1),
}
): Animated.SharedValue<number> => {
'worklet';
return useDerivedValue(() =>
state ? withTiming(1, config) : withTiming(0, config)
);
}
Example #7
Source File: AnimatedHelper.ts From curved-bottom-navigation-bar with MIT License | 6 votes |
sharedTiming = (
toValue: number,
config?: WithTimingConfig,
callBack?: AnimationCallback
) => {
'worklet';
return withTiming(
toValue,
Object.assign(
{
duration: 500,
easing: Easing.bezier(0.22, 1, 0.36, 1),
},
config
),
callBack
);
}
Example #8
Source File: CountdownTimer.tsx From tic-tac-toe-app with MIT License | 6 votes |
CountdownTimer: React.FC<PropTypes> = ({ size, duration }) => {
const clock = new Clock();
const progress = timing({
clock,
duration,
from: 0,
to: 1,
easing: Easing.linear,
});
return <CircularProgress size={size} progress={progress} />;
}
Example #9
Source File: Progress.tsx From reanimated-arc with MIT License | 5 votes |
Progress = () => {
const arcAngle = useRef(new Reanimated.Value(Math.random() * 240));
const [text, setText] = useState('0%');
const randomizeProgress = useCallback(() => {
Reanimated.timing(arcAngle.current, {
toValue: Math.random() * 240,
easing: Easing.inOut(Easing.quad),
duration: 1000,
}).start();
}, []);
return (
<View>
<View style={styles.container}>
<Reanimated.Code
exec={Reanimated.call([arcAngle.current], ([value]) => {
setText(`${Math.round((value / 240) * 100)}%`);
})}
/>
<ReanimatedArcBase
color="lightgrey"
diameter={200}
width={20}
arcSweepAngle={240}
lineCap="round"
rotation={240}
style={styles.absolute}
/>
<ReanimatedArcBase
color="purple"
diameter={200}
width={20}
arcSweepAngle={arcAngle.current}
lineCap="round"
rotation={240}
style={styles.absolute}
/>
<Text style={styles.text}>{text}</Text>
</View>
<Button title="Randomize progress" onPress={randomizeProgress} />
</View>
);
}
Example #10
Source File: ScalableImage.tsx From react-native-gallery-toolkit with MIT License | 5 votes |
defaultTimingConfig = { duration: 250, easing: Easing.bezier(0.33, 0.01, 0, 1), }
Example #11
Source File: ImageTransformer.tsx From react-native-gallery-toolkit with MIT License | 5 votes |
defaultTimingConfig = { duration: 250, easing: Easing.bezier(0.33, 0.01, 0, 1), }
Example #12
Source File: WeekIntro.tsx From nyxo-app with GNU General Public License v3.0 | 5 votes |
DEFAULT_EASING: Animated.EasingFunction = Easing.bezier( 0.5, 0, 0.25, 1 )
Example #13
Source File: Notification.tsx From react-native-crypto-wallet-app with MIT License | 5 votes |
Notification: React.FC<INotification> = ({ type, message }) => {
const opacity = useValue(0);
const { dispatch } = useContext(AppContext);
const { timing } = Animated;
const animConfig = {
duration: 300,
easing: Easing.inOut(Easing.ease),
};
useEffect(() => {
timing(opacity, { ...animConfig, toValue: 1 }).start();
}, [animConfig, opacity, timing]);
useEffect(() => {
const fade = setTimeout(() => {
timing(opacity, { ...animConfig, toValue: 0 }).start(({ finished }) => {
if (finished) {
dispatch({
type: CLEAR_NOTIFICATION,
});
}
});
}, 2000);
return () => {
clearTimeout(fade);
};
}, [animConfig, dispatch, opacity, timing]);
return (
<SafeAreaView
style={{
...StyleSheet.absoluteFillObject,
}}
>
<AnimatedBox
flexDirection="row"
justifyContent="space-between"
alignItems="center"
height={56}
position="absolute"
top={16}
left={16}
right={16}
backgroundColor="toast"
borderRadius="full"
{...{ opacity }}
style={NotificationStyle.container}
>
<Box flexDirection="row" alignItems="center">
<Icon name={type!} />
<StyledText variant="label" color="white" style={NotificationStyle.message}>
{message}
</StyledText>
</Box>
<Icon name="x" color="white" />
</AnimatedBox>
</SafeAreaView>
);
}
Example #14
Source File: index.tsx From react-native-checkbox-reanimated with MIT License | 5 votes |
AnimatedCheckbox = (props: Props) => {
const { checked, checkmarkColor, highlightColor, boxOutlineColor } = props
const progress = useSharedValue(0)
useEffect(() => {
progress.value = withTiming(checked ? 1 : 0, {
duration: checked ? 300 : 100,
easing: Easing.linear
})
}, [checked])
const animatedBoxProps = useAnimatedProps(
() => ({
stroke: interpolateColor(
Easing.bezierFn(0.16, 1, 0.3, 1)(progress.value),
[0, 1],
[boxOutlineColor, highlightColor],
'RGB'
),
fill: interpolateColor(
Easing.bezierFn(0.16, 1, 0.3, 1)(progress.value),
[0, 1],
['#00000000', highlightColor],
'RGB'
)
}),
[highlightColor, boxOutlineColor]
)
return (
<Svg
viewBox={[-MARGIN, -MARGIN, vWidth + MARGIN, vHeight + MARGIN].join(' ')}
>
<Defs>
<ClipPath id="clipPath">
<Path
fill="white"
stroke="gray"
strokeLinejoin="round"
strokeLinecap="round"
d={outlineBoxPath}
/>
</ClipPath>
</Defs>
<AnimatedStroke
progress={progress}
d={checkMarkPath}
stroke={highlightColor}
strokeWidth={10}
strokeLinejoin="round"
strokeLinecap="round"
strokeOpacity={checked || false ? 1 : 0}
/>
<AnimatedPath
d={outlineBoxPath}
strokeWidth={7}
strokeLinejoin="round"
strokeLinecap="round"
animatedProps={animatedBoxProps}
/>
<G clipPath="url(#clipPath)">
<AnimatedStroke
progress={progress}
d={checkMarkPath}
stroke={checkmarkColor}
strokeWidth={10}
strokeLinejoin="round"
strokeLinecap="round"
strokeOpacity={checked || false ? 1 : 0}
/>
</G>
</Svg>
)
}
Example #15
Source File: ReanimatedArc.tsx From reanimated-arc with MIT License | 5 votes |
static defaultProps = {
...defaultProps,
initialAnimation: true,
animationDuration: 800,
easing: Easing.linear,
};
Example #16
Source File: Stopwatch.tsx From reanimated-arc with MIT License | 5 votes |
easing = Easing.inOut(Easing.quad)
Example #17
Source File: Donut2.tsx From reanimated-arc with MIT License | 5 votes |
easing = Easing.inOut(Easing.quad)
Example #18
Source File: Logo.tsx From reanimated-arc with MIT License | 5 votes |
App = () => {
const arcAngle = useRef(new Reanimated.Value(-90));
const animate = () =>
Reanimated.timing(arcAngle.current, {
toValue: 270,
easing: Easing.inOut(Easing.quad),
duration: 2000,
}).start();
useEffect(() => {
animate();
}, []);
return (
<View style={styles.wrapper}>
<ReanimatedArcBase
color="#121330"
diameter={200}
width={20}
lineCap="round"
arcSweepAngle={160}
rotation={Reanimated.add(arcAngle.current, 10)}
style={styles.arc1}
/>
<ReanimatedArcBase
color="#3eefd8"
diameter={140}
width={20}
lineCap="round"
arcSweepAngle={170}
rotation={Reanimated.multiply(
Reanimated.sub(arcAngle.current, 185),
-1,
)}
style={styles.arc2}
/>
<ReanimatedArcBase
color="#121330"
diameter={80}
width={20}
lineCap="round"
arcSweepAngle={180.1}
rotation={arcAngle.current}
style={styles.arc3}
/>
</View>
);
}
Example #19
Source File: AuthForm.tsx From react-native-crypto-wallet-app with MIT License | 4 votes |
AuthForm: React.FC<IAuthFormProps> = ({
isSignUp,
email,
isEmailValid,
password,
isPasswordValid,
loading,
isButtonDisabled,
showPassword,
submitButtonLabel,
bottomSectionLightTextLabel,
bottomSectionAccentTextLabel,
onEmailChange,
onPasswordChange,
onShowPasswordPress,
onSignUpPress,
onForgotPasswordPress,
onNavigationToLoginOrSignUp,
}) => {
const keyboardDidShow = useKeyboardDidShow();
const { height } = Dimensions.get('window');
const UNFOCUSED_HEIGHT = (height * 57) / 100;
const FOCUSED_HEIGHT = (height * 87) / 100;
const containerInitialHeight = useValue(UNFOCUSED_HEIGHT);
const containerAnimConfig = {
duration: 200,
easing: Easing.inOut(Easing.ease),
};
useEffect(() => {
if (keyboardDidShow) {
timing(containerInitialHeight, { ...containerAnimConfig, toValue: FOCUSED_HEIGHT }).start();
} else {
timing(containerInitialHeight, { ...containerAnimConfig, toValue: UNFOCUSED_HEIGHT }).start();
}
}, [
FOCUSED_HEIGHT,
UNFOCUSED_HEIGHT,
containerAnimConfig,
containerInitialHeight,
keyboardDidShow,
]);
return (
<ContentContainer height={containerInitialHeight}>
<StyledInput
label="Email Address"
value={email}
onChangeText={onEmailChange}
keyboardType="email-address"
disabled={loading}
errorText={isEmailValid === undefined || isEmailValid ? '' : 'Email address is not valid'}
ariaLabel="email"
/>
<StyledInput
{...{ showPassword }}
label="Password"
value={password}
onChangeText={onPasswordChange}
onShowPasswordPress={onShowPasswordPress}
disabled={loading}
errorText={isPasswordValid === undefined || isPasswordValid ? '' : 'Password is not valid'}
isPassword
ariaLabel="password"
/>
{!isSignUp && (
<Box alignSelf="flex-end">
<PressableText
variant="link"
label="Forgot your password?"
onPress={onForgotPasswordPress!}
/>
</Box>
)}
<View style={AuthFormStyle.bottomSection}>
<BottomSection
mainButtonVariant="primary"
mainButtonLabel={submitButtonLabel}
mainButtonLoading={loading}
mainButtonDisabled={isButtonDisabled}
lightTextLabel={bottomSectionLightTextLabel}
accentTextLabel={bottomSectionAccentTextLabel}
onMainButtonPress={onSignUpPress}
onAccentTextPress={onNavigationToLoginOrSignUp}
/>
</View>
</ContentContainer>
);
}
Example #20
Source File: Dot.tsx From react-native-wagmi-charts with MIT License | 4 votes |
export function LineChartDot({
at,
color: defaultColor = 'black',
dotProps,
hasOuterDot: defaultHasOuterDot = false,
hasPulse = false,
inactiveColor,
outerDotProps,
pulseBehaviour = 'while-inactive',
pulseDurationMs = 800,
showInactiveColor = true,
size = 4,
outerSize = size * 4,
}: LineChartDotProps) {
const { data, isActive } = useLineChart();
const { path, pathWidth: width } = React.useContext(
LineChartDimensionsContext
);
////////////////////////////////////////////////////////////
const { isInactive: _isInactive } = React.useContext(LineChartPathContext);
const isInactive = showInactiveColor && _isInactive;
const color = isInactive ? inactiveColor || defaultColor : defaultColor;
const opacity = isInactive && !inactiveColor ? 0.5 : 1;
const hasOuterDot = defaultHasOuterDot || hasPulse;
////////////////////////////////////////////////////////////
const parsedPath = React.useMemo(() => parse(path), [path]);
////////////////////////////////////////////////////////////
const pointWidth = React.useMemo(
() => width / (data.length - 1),
[data.length, width]
);
////////////////////////////////////////////////////////////
const x = useDerivedValue(() => withTiming(pointWidth * at));
const y = useDerivedValue(() =>
withTiming(getYForX(parsedPath!, x.value) || 0)
);
////////////////////////////////////////////////////////////
const animatedDotProps = useAnimatedProps(() => ({
cx: x.value,
cy: y.value,
}));
const animatedOuterDotProps = useAnimatedProps(() => {
let defaultProps = {
cx: x.value,
cy: y.value,
opacity: 0.1,
r: outerSize,
};
if (!hasPulse) {
return defaultProps;
}
if (isActive.value && pulseBehaviour === 'while-inactive') {
return {
...defaultProps,
r: 0,
};
}
const easing = Easing.out(Easing.sin);
const animatedOpacity = withRepeat(
withSequence(
withTiming(0.8),
withTiming(0, {
duration: pulseDurationMs,
easing,
})
),
-1,
false
);
const scale = withRepeat(
withSequence(
withTiming(0),
withTiming(outerSize, {
duration: pulseDurationMs,
easing,
})
),
-1,
false
);
if (pulseBehaviour === 'while-inactive') {
return {
...defaultProps,
opacity: isActive.value ? withTiming(0) : animatedOpacity,
r: isActive.value ? withTiming(0) : scale,
};
}
return {
...defaultProps,
opacity: animatedOpacity,
r: scale,
};
}, [outerSize]);
////////////////////////////////////////////////////////////
return (
<>
<AnimatedCircle
animatedProps={animatedDotProps}
r={size}
fill={color}
opacity={opacity}
{...dotProps}
/>
{hasOuterDot && (
<AnimatedCircle
animatedProps={animatedOuterDotProps}
fill={color}
{...outerDotProps}
/>
)}
</>
);
}
Example #21
Source File: ProgressBar.tsx From jellyfin-audio-player with MIT License | 4 votes |
function ProgressBar() {
const { position, buffered, duration } = useProgress();
const width = useSharedValue(0);
const pos = useSharedValue(0);
const buf = useSharedValue(0);
const dur = useSharedValue(0);
const isDragging = useSharedValue(false);
const offset = useSharedValue(0);
const bufferAnimation = useDerivedValue(() => {
return calculateProgressTranslation(buf.value, dur.value, width.value);
}, [[dur, buf, width.value]]);
const progressAnimation = useDerivedValue(() => {
if (isDragging.value) {
return calculateProgressTranslation(offset.value, width.value, width.value);
} else {
return calculateProgressTranslation(pos.value, dur.value, width.value);
}
});
const timePassed = useDerivedValue(() => {
if (isDragging.value) {
const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
return getMinutes(currentPosition) + ':' + getSeconds(currentPosition);
} else {
return getMinutes(pos.value) + ':' + getSeconds(pos.value);
}
}, [pos]);
const timeRemaining = useDerivedValue(() => {
if (isDragging.value) {
const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
const remaining = (currentPosition - dur.value) * -1;
return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
} else {
const remaining = (pos.value - dur.value) * -1;
return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
}
}, [pos, dur]);
const pan = Gesture.Pan()
.minDistance(1)
.activeOffsetX(1)
.activeOffsetY(1)
.onBegin((e) => {
isDragging.value = true;
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
}).onUpdate((e) => {
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
}).onFinalize(() => {
pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
isDragging.value = false;
runOnJS(TrackPlayer.seekTo)(pos.value);
});
const tap = Gesture.Tap()
.onBegin((e) => {
isDragging.value = true;
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
}).onFinalize(() => {
pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
isDragging.value = false;
runOnJS(TrackPlayer.seekTo)(pos.value);
});
const gesture = Gesture.Exclusive(pan, tap);
useEffect(() => {
pos.value = position;
buf.value = buffered;
dur.value = duration;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [position, buffered, duration]);
const dragHandleStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateX: offset.value },
{
scale: withTiming(isDragging.value ? 1 : 0, {
duration: 100,
easing: Easing.out(Easing.ease),
})
}
],
};
});
const bufferStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: bufferAnimation.value }
]
}));
const progressStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateX: progressAnimation.value }
]
};
});
const timePassedStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateY: withTiming(isDragging.value && offset.value < 48 ? 12 : 0, {
duration: 145,
easing: Easing.ease
}) },
],
};
});
const timeRemainingStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateY: withTiming(isDragging.value && offset.value > width.value - 48 ? 12 : 0, {
duration: 150,
easing: Easing.ease
}) },
],
};
});
return (
<GestureDetector gesture={gesture}>
<Container onLayout={(e) => { width.value = e.nativeEvent.layout.width; }}>
<ProgressTrackContainer>
<ProgressTrack
opacity={0.15}
/>
<ProgressTrack
style={bufferStyles}
opacity={0.15}
/>
<ProgressTrack
style={progressStyles}
/>
</ProgressTrackContainer>
<DragHandle style={dragHandleStyles} />
<NumberBar style={{ flex: 1 }}>
<Number text={timePassed} style={timePassedStyles} />
<Number text={timeRemaining} style={timeRemainingStyles} />
</NumberBar>
</Container>
</GestureDetector>
);
}
Example #22
Source File: SectionListExample.tsx From react-native-scroll-bottom-sheet with MIT License | 4 votes |
SectionListExample: React.FC<Props> = () => {
const snapPointsFromTop = [96, '45%', windowHeight - 264];
const animatedPosition = React.useRef(new Value(0.5));
const handleLeftRotate = concat(
interpolate(animatedPosition.current, {
inputRange: [0, 0.4, 1],
outputRange: [25, 0, 0],
extrapolate: Extrapolate.CLAMP,
}),
'deg'
);
const handleRightRotate = concat(
interpolate(animatedPosition.current, {
inputRange: [0, 0.4, 1],
outputRange: [-25, 0, 0],
extrapolate: Extrapolate.CLAMP,
}),
'deg'
);
const cardScale = interpolate(animatedPosition.current, {
inputRange: [0, 0.6, 1],
outputRange: [1, 1, 0.9],
extrapolate: Extrapolate.CLAMP,
});
const renderSectionHeader = React.useCallback(
({ section }) => (
<View style={styles.section}>
<Text>{section.title}</Text>
</View>
),
[]
);
const renderItem = React.useCallback(
({ item }) => <Transaction {...item} />,
[]
);
return (
<View style={styles.container}>
<View style={styles.balanceContainer}>
<Text style={styles.poundSign}>£</Text>
<Text style={styles.balance}>4,345</Text>
</View>
<ProgressBar
style={styles.progressBar}
progress={0.8}
color={Colors.green600}
/>
<Animated.Image
source={require('../assets/card-front.png')}
style={[styles.card, { transform: [{ scale: cardScale }] }]}
/>
<View style={styles.row}>
<View>
<View style={styles.action}>
<FontAwesome5 name="credit-card" size={24} color="black" />
</View>
<Text style={{ textAlign: 'center' }}>Account</Text>
</View>
<View>
<View style={styles.action}>
<FontAwesome5 name="eye" size={24} color="black" />
</View>
<Text style={{ textAlign: 'center' }}>Pin</Text>
</View>
<View>
<View style={styles.action}>
<Ionicons name="md-snow" size={24} color="black" />
</View>
<Text style={{ textAlign: 'center' }}>Freeze</Text>
</View>
<View>
<View style={styles.action}>
<FontAwesome5 name="plus" size={24} color="black" />
</View>
<Text style={{ textAlign: 'center' }}>Top up</Text>
</View>
</View>
<ScrollBottomSheet<ListItemData>
enableOverScroll
removeClippedSubviews={Platform.OS === 'android' && sections.length > 0}
componentType="SectionList"
topInset={statusBarHeight + navBarHeight}
animatedPosition={animatedPosition.current}
snapPoints={snapPointsFromTop}
initialSnapIndex={1}
animationConfig={{
easing: Easing.inOut(Easing.linear),
}}
renderHandle={() => (
<Handle style={{ paddingVertical: 20, backgroundColor: '#F3F4F9' }}>
<Animated.View
style={[
styles.handle,
{
left: windowWidth / 2 - 20,
transform: [{ rotate: handleLeftRotate }],
},
]}
/>
<Animated.View
style={[
styles.handle,
{
right: windowWidth / 2 - 20,
transform: [{ rotate: handleRightRotate }],
},
]}
/>
</Handle>
)}
contentContainerStyle={styles.contentContainerStyle}
stickySectionHeadersEnabled
sections={sections}
keyExtractor={i => i.id}
renderSectionHeader={renderSectionHeader}
renderItem={renderItem}
/>
</View>
);
}