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

Add Floating Action Button example to Reanimated Cookbook #6079

Merged
merged 5 commits into from
Jun 18, 2024
Merged
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
42 changes: 42 additions & 0 deletions packages/docs-reanimated/blog/floating-action-button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
slug: floatingactionbutton
title: Floating Action Button
---

Floating Action Button provides user with easy-accessible panel with primary or most common actions on the screen.

import FloatingActionButton from '@site/static/examples/FloatingActionButton';
import FloatingActionButtonSrc from '!!raw-loader!@site/static/examples/FloatingActionButton';
import ExampleVideo from '@site/src/components/ExampleVideo';
import CollapsibleCode from '@site/src/components/CollapsibleCode';

<InteractiveExample src={FloatingActionButtonSrc} component={FloatingActionButton} />

We use [shared values](/docs/fundamentals/glossary#shared-value) to monitor if the button is expanded. The `useSharedValue` hook helps prevent unnecessary re-renders during state changes.

<CollapsibleCode src={FloatingActionButtonSrc} showLines={[49,52]}/>

The state is toggled when the main _Actions_ button is pressed, which triggers animations for other secondary buttons.

<ExampleVideo
sources={{
android: "/react-native-reanimated/recordings/examples/fab_android.mov",
ios: "/react-native-reanimated/recordings/examples/fab_ios.mov"
}}
/>

It also relies on [animatable values](/docs/fundamentals/glossary#animatable-value). Leveraging animatable values of rotation and position enables smooth transition between the two states.

<samp id="FloatingActionButton">Floating Action Button</samp>

<CollapsibleCode src={FloatingActionButtonSrc} showLines={[55,67]}/>

The **FloatingActionButton** is a reusable component that manages button styles, content and animations. For this we use props: `buttonLetter`, `index` and `isExpanded`.

<CollapsibleCode src={FloatingActionButtonSrc} showLines={[21,46]}/>

We define the animated styles for the buttons within the FloatingActionButton component, passing the necessary values as props. The delay in their appearance on the screen is calculated based on the button's index. Buttons with a higher index will appear later and be positioned higher in the "column" of buttons.

<samp id="FloatingActionButton">Floating Action Button</samp>

<CollapsibleCode src={FloatingActionButtonSrc} showLines={[22,46]}/>
156 changes: 156 additions & 0 deletions packages/docs-reanimated/static/examples/FloatingActionButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React from 'react';
import { StyleSheet, SafeAreaView, View, Pressable, Text } from 'react-native';
import Animated, {
withDelay,
interpolate,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from 'react-native-reanimated';

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

const SPRING_CONFIG = {
duration: 1200,
overshootClamping: true,
dampingRatio: 0.8,
};

const OFFSET = 60;

const FloatingActionButton = ({ isExpanded, index, buttonLetter }) => {
const animatedStyles = useAnimatedStyle(() => {
// highlight-next-line
const moveValue = isExpanded.value ? OFFSET * index : 0;
const translateValue = withSpring(-moveValue, SPRING_CONFIG);
//highlight-next-line
const delay = index * 100;

const scaleValue = isExpanded.value ? 1 : 0;

return {
transform: [
{ translateY: translateValue },
{
scale: withDelay(delay, withTiming(scaleValue)),
},
],
};
});

return (
<AnimatedPressable style={[animatedStyles, styles.shadow, styles.button]}>
<Animated.Text style={styles.content}>{buttonLetter}</Animated.Text>
</AnimatedPressable>
);
};

export default function App() {
const isExpanded = useSharedValue(false);

const handlePress = () => {
isExpanded.value = !isExpanded.value;
};

const plusIconStyle = useAnimatedStyle(() => {
// highlight-next-line
const moveValue = interpolate(Number(isExpanded.value), [0, 1], [0, 2]);
const translateValue = withTiming(moveValue);
const rotateValue = isExpanded.value ? '45deg' : '0deg';

return {
transform: [
{ translateX: translateValue },
{ rotate: withTiming(rotateValue) },
],
};
});

return (
<SafeAreaView>
<View style={styles.mainContainer}>
<View style={styles.buttonContainer}>
<AnimatedPressable
onPress={handlePress}
style={[styles.shadow, mainButtonStyles.button]}>
<Animated.Text style={[plusIconStyle, mainButtonStyles.content]}>
+
</Animated.Text>
</AnimatedPressable>
<FloatingActionButton
isExpanded={isExpanded}
index={1}
buttonLetter={'M'}
/>
<FloatingActionButton
isExpanded={isExpanded}
index={2}
buttonLetter={'W'}
/>
<FloatingActionButton
isExpanded={isExpanded}
index={3}
buttonLetter={'S'}
/>
</View>
</View>
</SafeAreaView>
);
}

const mainButtonStyles = StyleSheet.create({
button: {
zIndex: 1,
height: 56,
width: 56,
borderRadius: 100,
backgroundColor: '#b58df1',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
content: {
fontSize: 24,
color: '#f8f9ff',
},
});

const styles = StyleSheet.create({
mainContainer: {
position: 'relative',
height: 260,
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
},
button: {
width: 40,
height: 40,
backgroundColor: '#82cab2',
position: 'absolute',
borderRadius: 100,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: -2,
flexDirection: 'row',
},
buttonContainer: {
position: 'absolute',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
shadow: {
shadowColor: '#171717',
shadowOffset: { width: -0.5, height: 3.5 },
shadowOpacity: 0.2,
shadowRadius: 3,
},
content: {
color: '#f8f9ff',
fontWeight: 500,
},
});
Binary file not shown.
Binary file not shown.