diff --git a/docs/component-docs.config.js b/docs/component-docs.config.js index 7acc917e96..68c60d4128 100644 --- a/docs/component-docs.config.js +++ b/docs/component-docs.config.js @@ -41,6 +41,7 @@ function getPages() { .split('\n') .map((line) => line.split(' ').pop().replace(/('|;)/g, '')) .filter((line) => line.startsWith('./components/')) + .filter((line) => line !== './components/FAB/AnimatedFAB') .map((line) => { const file = require.resolve(path.join(__dirname, '../src', line)); if (/\/index\.(js|tsx?)$/.test(file)) { diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index dc38d73c4d..8a7381e88b 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -37,11 +37,13 @@ import ToggleButtonExample from './Examples/ToggleButtonExample'; import TouchableRippleExample from './Examples/TouchableRippleExample'; import ThemeExample from './Examples/ThemeExample'; import RadioButtonItemExample from './Examples/RadioButtonItemExample'; +import AnimatedFABExample from './Examples/AnimatedFABExample'; export const examples: Record< string, React.ComponentType & { title: string } > = { + ...(__DEV__ && { animatedFab: AnimatedFABExample }), activityIndicator: ActivityIndicatorExample, appbar: AppbarExample, avatar: AvatarExample, @@ -111,7 +113,6 @@ export default function ExampleList({ navigation }: Props) { paddingLeft: safeArea.left, paddingRight: safeArea.right, }} - style={{ backgroundColor: colors.background }} ItemSeparatorComponent={Divider} renderItem={renderItem} keyExtractor={keyExtractor} diff --git a/example/src/Examples/AnimatedFABExample.tsx b/example/src/Examples/AnimatedFABExample.tsx new file mode 100644 index 0000000000..bd96aa2813 --- /dev/null +++ b/example/src/Examples/AnimatedFABExample.tsx @@ -0,0 +1,223 @@ +import * as React from 'react'; +import { View, StyleSheet, FlatList, Animated, Platform } from 'react-native'; +import type { NativeSyntheticEvent, NativeScrollEvent } from 'react-native'; +import { + Colors, + AnimatedFAB, + useTheme, + Avatar, + Paragraph, +} from 'react-native-paper'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { animatedFABExampleData } from '../../utils'; + +type Item = { + id: string; + sender: string; + header: string; + message: string; + initials: string; + date: string; + read: boolean; + favorite: boolean; + bgColor: string; +}; + +type CustomFABProps = { + animatedValue: Animated.Value; + visible: boolean; + extended: boolean; +}; + +const isIOS = Platform.OS === 'ios'; + +const CustomFAB = ({ animatedValue, visible, extended }: CustomFABProps) => { + const [isExtended, setIsExtended] = React.useState(true); + + React.useEffect(() => { + if (!isIOS) { + animatedValue.addListener(({ value }: { value: number }) => + setIsExtended(value > 0 ? true : false) + ); + } else setIsExtended(extended); + }, [animatedValue, extended]); + + return ( + console.log('Pressed')} + visible={visible} + animateFrom="right" + iconMode="dynamic" + style={styles.fabStyle} + /> + ); +}; + +const AnimatedFABExample = () => { + const { + colors: { background }, + } = useTheme(); + + const [scrollPosition, setScrollPosition] = React.useState(0); + const [extended, setExtended] = React.useState(false); + const [visible, setVisible] = React.useState(true); + + const { current: velocity } = React.useRef( + new Animated.Value(0) + ); + + const renderItem = ({ item }: { item: Item }) => { + return ( + + + + + + {item.sender} + + + {item.date} + + + + + + + {item.header} + + + {item.message} + + + + setVisible(!visible)} + style={styles.icon} + /> + + + + ); + }; + + const onScroll = ({ + nativeEvent, + }: NativeSyntheticEvent) => { + if (!isIOS) { + return velocity.setValue(nativeEvent?.velocity?.y ?? 0); + } + + const currentScrollPosition = Math.floor(nativeEvent.contentOffset.y); + + const scrollHeight = + nativeEvent.contentSize.height - nativeEvent.layoutMeasurement.height; + + setScrollPosition(currentScrollPosition); + + if (currentScrollPosition > scrollPosition && scrollPosition > 0) { + setExtended(false); + } else if ( + currentScrollPosition < scrollPosition && + scrollPosition < scrollHeight + ) { + setExtended(true); + } + }; + + return ( + <> + item.id} + onEndReachedThreshold={0} + scrollEventThrottle={16} + style={[styles.flex, { backgroundColor: background }]} + contentContainerStyle={[ + styles.container, + { backgroundColor: background }, + ]} + onScroll={onScroll} + /> + + + ); +}; + +AnimatedFABExample.title = 'Animated Floating Action Button'; + +const styles = StyleSheet.create({ + container: { + padding: 16, + }, + avatar: { + marginRight: 16, + marginTop: 8, + }, + flex: { + flex: 1, + }, + itemContainer: { + marginBottom: 16, + flexDirection: 'row', + }, + itemTextContentContainer: { + flexDirection: 'column', + flex: 1, + }, + itemHeaderContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + itemMessageContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + flexGrow: 1, + }, + read: { + fontWeight: 'bold', + }, + icon: { + marginLeft: 16, + alignSelf: 'flex-end', + }, + date: { + fontSize: 12, + }, + header: { + fontSize: 14, + marginRight: 8, + flex: 1, + }, + fabStyle: { + right: 16, + bottom: 32, + position: 'absolute', + }, +}); + +export default AnimatedFABExample; diff --git a/example/utils/index.ts b/example/utils/index.ts index 3405e8d078..e17215aa58 100644 --- a/example/utils/index.ts +++ b/example/utils/index.ts @@ -47,3 +47,262 @@ export function inputReducer( return { ...state }; } } + +export const animatedFABExampleData = [ + { + id: '1', + sender: 'Hermann, Pfannel & Schumm', + header: + 'Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo. In blandit ultrices enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Proin interdum mauris non ligula pellentesque ultrices. Phasellus id sapien in sapien iaculis congue. Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl. Aenean lectus. Pellentesque eget nunc. Donec quis orci eget orci vehicula condimentum.', + message: + 'Duis consequat dui nec nisi volutpat eleifend. Donec ut dolor. Morbi vel lectus in quam fringilla rhoncus.\n\nMauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis. Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci. Mauris lacinia sapien quis libero.\n\nNullam sit amet turpis elementum ligula vehicula consequat. Morbi a ipsum. Integer a nibh.', + initials: 'H', + date: '29.1.2021', + read: false, + favorite: false, + bgColor: '#ff0173', + }, + { + id: '2', + sender: 'Ziemann, Lockman and Kuvalis', + header: + 'Vivamus vestibulum sagittis sapien. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam vel augue.', + message: + 'Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo. Pellentesque viverra pede ac diam. Cras pellentesque volutpat dui.\n\nMaecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam. Suspendisse potenti.\n\nNullam porttitor lacus at turpis. Donec posuere metus vitae ipsum. Aliquam non mauris.', + initials: 'J', + date: '5.9.2020', + read: false, + favorite: false, + bgColor: '#b287a9', + }, + { + id: '3', + sender: 'Daniel, Kuhn and Wolf', + header: + 'Aenean fermentum. Donec ut mauris eget massa tempor convallis. Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh. Quisque id justo sit amet sapien dignissim vestibulum.', + message: + 'Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla. Sed vel enim sit amet nunc viverra dapibus. Nulla suscipit ligula in lacus.\n\nCurabitur at ipsum ac tellus semper interdum. Mauris ullamcorper purus sit amet nulla. Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.', + initials: 'Y', + date: '13.6.2020', + read: true, + favorite: false, + bgColor: '#c1bde9', + }, + { + id: '4', + sender: 'Crona, Lind and Stoltenberg', + header: + 'Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla. Nunc purus. Phasellus in felis. Donec semper sapien a libero. Nam dui. Proin leo odio, porttitor id, consequat in, consequat ut, nulla.', + message: + 'Aenean lectus. Pellentesque eget nunc. Donec quis orci eget orci vehicula condimentum.', + initials: 'W', + date: '20.5.2020', + read: false, + favorite: false, + bgColor: '#932a24', + }, + { + id: '5', + sender: 'Bashirian-Hudson', + header: + 'Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue. Aliquam erat volutpat. In congue. Etiam justo. Etiam pretium iaculis justo. In hac habitasse platea dictumst.', + message: + 'Sed sagittis. Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci. Nullam molestie nibh in lectus.\n\nPellentesque at nulla. Suspendisse potenti. Cras in purus eu magna vulputate luctus.\n\nCum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus vestibulum sagittis sapien. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.', + initials: 'V', + date: '21.9.2020', + read: true, + favorite: false, + bgColor: '#eda5b7', + }, + { + id: '6', + sender: 'Schmitt-Jacobs', + header: + 'Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci. Mauris lacinia sapien quis libero. Nullam sit amet turpis elementum ligula vehicula consequat. Morbi a ipsum. Integer a nibh. In quis justo.', + message: + 'Phasellus sit amet erat. Nulla tempus. Vivamus in felis eu sapien cursus vestibulum.', + initials: 'M', + date: '2.6.2020', + read: true, + favorite: true, + bgColor: '#18aaba', + }, + { + id: '7', + sender: 'Graham-Champlin', + header: + 'Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est. Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros. Vestibulum ac est lacinia nisi venenatis tristique. Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue. Aliquam erat volutpat. In congue.', + message: + 'In hac habitasse platea dictumst. Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante. Nulla justo.\n\nAliquam quis turpis eget elit sodales scelerisque. Mauris sit amet eros. Suspendisse accumsan tortor quis turpis.', + initials: 'T', + date: '17.10.2020', + read: false, + favorite: true, + bgColor: '#cc5e54', + }, + { + id: '8', + sender: 'Schoen, Carroll and Herzog', + header: + 'Ut tellus. Nulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.', + message: + 'Cras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque.\n\nQuisque porta volutpat erat. Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla. Nunc purus.', + initials: 'F', + date: '31.10.2020', + read: false, + favorite: false, + bgColor: '#28db04', + }, + { + id: '9', + sender: 'Pouros-Fay', + header: + 'Donec semper sapien a libero. Nam dui. Proin leo odio, porttitor id, consequat in, consequat ut, nulla. Sed accumsan felis. Ut at dolor quis odio consequat varius. Integer ac leo. Pellentesque ultrices mattis odio. Donec vitae nisi.', + message: + 'Aliquam quis turpis eget elit sodales scelerisque. Mauris sit amet eros. Suspendisse accumsan tortor quis turpis.\n\nSed ante. Vivamus tortor. Duis mattis egestas metus.\n\nAenean fermentum. Donec ut mauris eget massa tempor convallis. Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.', + initials: 'Z', + date: '6.1.2021', + read: true, + favorite: true, + bgColor: '#b6f3fb', + }, + { + id: '10', + sender: 'McKenzie, Ruecker and Bernhard', + header: 'Nunc purus. Phasellus in felis.', + message: + 'In quis justo. Maecenas rhoncus aliquam lacus. Morbi quis tortor id nulla ultrices aliquet.', + initials: 'F', + date: '23.2.2021', + read: false, + favorite: false, + bgColor: '#96f066', + }, + { + id: '11', + sender: 'Olson Inc', + header: + 'Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo. Pellentesque viverra pede ac diam. Cras pellentesque volutpat dui. Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam. Suspendisse potenti.', + message: + 'Phasellus sit amet erat. Nulla tempus. Vivamus in felis eu sapien cursus vestibulum.\n\nProin eu mi. Nulla ac enim. In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.\n\nDuis aliquam convallis nunc. Proin at turpis a pede posuere nonummy. Integer non velit.', + initials: 'V', + date: '17.10.2020', + read: false, + favorite: false, + bgColor: '#f2d49d', + }, + { + id: '12', + sender: 'Walsh LLC', + header: + 'Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl. Nunc rhoncus dui vel sem. Sed sagittis.', + message: 'Phasellus in felis. Donec semper sapien a libero. Nam dui.', + initials: 'O', + date: '6.1.2021', + read: false, + favorite: true, + bgColor: '#f477dc', + }, + { + id: '13', + sender: 'Lemke, Cremin and Kutch', + header: + 'Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat. Praesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede. Morbi porttitor lorem id ligula. Suspendisse ornare consequat lectus.', + message: + 'In sagittis dui vel nisl. Duis ac nibh. Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.\n\nSuspendisse potenti. In eleifend quam a odio. In hac habitasse platea dictumst.\n\nMaecenas ut massa quis augue luctus tincidunt. Nulla mollis molestie lorem. Quisque ut erat.', + initials: 'D', + date: '10.12.2020', + read: false, + favorite: false, + bgColor: '#b2880e', + }, + { + id: '14', + sender: 'Ruecker Group', + header: + 'Nulla tempus. Vivamus in felis eu sapien cursus vestibulum. Proin eu mi. Nulla ac enim. In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.', + message: + 'In congue. Etiam justo. Etiam pretium iaculis justo.\n\nIn hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.\n\nNulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.', + initials: 'F', + date: '21.8.2020', + read: true, + favorite: false, + bgColor: '#64983a', + }, + { + id: '15', + sender: 'Kovacek, Lockman and Kautzer', + header: + 'Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl. Nunc rhoncus dui vel sem. Sed sagittis. Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci. Nullam molestie nibh in lectus. Pellentesque at nulla. Suspendisse potenti. Cras in purus eu magna vulputate luctus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.', + message: + 'Maecenas ut massa quis augue luctus tincidunt. Nulla mollis molestie lorem. Quisque ut erat.', + initials: 'L', + date: '5.3.2020', + read: true, + favorite: false, + bgColor: '#c7486b', + }, + { + id: '16', + sender: 'Jaskolski-Cummerata', + header: + 'In hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus. Nulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi. Cras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque.', + message: + 'Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam. Suspendisse potenti.\n\nNullam porttitor lacus at turpis. Donec posuere metus vitae ipsum. Aliquam non mauris.\n\nMorbi non lectus. Aliquam sit amet diam in magna bibendum imperdiet. Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.', + initials: 'C', + date: '21.4.2020', + read: false, + favorite: false, + bgColor: '#2f89a1', + }, + { + id: '17', + sender: 'Wunsch-Walker', + header: 'Donec vitae nisi.', + message: + 'Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa. Donec dapibus. Duis at velit eu est congue elementum.\n\nIn hac habitasse platea dictumst. Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante. Nulla justo.\n\nAliquam quis turpis eget elit sodales scelerisque. Mauris sit amet eros. Suspendisse accumsan tortor quis turpis.', + initials: 'A', + date: '15.4.2020', + read: false, + favorite: true, + bgColor: '#033cbf', + }, + { + id: '18', + sender: 'Ledner Inc', + header: + 'Curabitur convallis. Duis consequat dui nec nisi volutpat eleifend. Donec ut dolor. Morbi vel lectus in quam fringilla rhoncus. Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis. Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci. Mauris lacinia sapien quis libero. Nullam sit amet turpis elementum ligula vehicula consequat. Morbi a ipsum.', + message: + 'In congue. Etiam justo. Etiam pretium iaculis justo.\n\nIn hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.', + initials: 'Y', + date: '16.5.2020', + read: false, + favorite: true, + bgColor: '#166b59', + }, + { + id: '19', + sender: 'Heaney-Rolfson', + header: 'Nunc rhoncus dui vel sem.', + message: + 'In hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.', + initials: 'T', + date: '19.1.2021', + read: true, + favorite: false, + bgColor: '#da0148', + }, + { + id: '20', + sender: 'Dietrich, Beier and Leannon', + header: + 'Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo. Maecenas pulvinar lobortis est. Phasellus sit amet erat. Nulla tempus. Vivamus in felis eu sapien cursus vestibulum. Proin eu mi. Nulla ac enim. In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem. Duis aliquam convallis nunc.', + message: + 'Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa. Donec dapibus. Duis at velit eu est congue elementum.', + initials: 'Z', + date: '21.5.2020', + read: false, + favorite: false, + bgColor: '#22b33f', + }, +]; diff --git a/src/components/FAB/AnimatedFAB.tsx b/src/components/FAB/AnimatedFAB.tsx new file mode 100644 index 0000000000..35c60735f6 --- /dev/null +++ b/src/components/FAB/AnimatedFAB.tsx @@ -0,0 +1,456 @@ +import color from 'color'; +import * as React from 'react'; +import { + Animated, + View, + ViewStyle, + StyleSheet, + StyleProp, + Easing, + ScrollView, + Text, + Platform, +} from 'react-native'; +import Surface from '../Surface'; +import Icon from '../Icon'; +import TouchableRipple from '../TouchableRipple/TouchableRipple'; +import { withTheme } from '../../core/theming'; +import type { $RemoveChildren } from '../../types'; +import type { IconSource } from './../Icon'; +import type { + AccessibilityState, + NativeSyntheticEvent, + TextLayoutEventData, +} from 'react-native'; +import { white, black } from '../../styles/colors'; +import AnimatedText from '../Typography/AnimatedText'; +import getContrastingColor from '../../utils/getContrastingColor'; + +type Props = $RemoveChildren & { + /** + * Icon to display for the `FAB`. + */ + icon: IconSource; + /** + * Label for extended `FAB`. + */ + label: string; + /** + * Make the label text uppercased. + */ + uppercase?: boolean; + /** + * Accessibility label for the FAB. This is read by the screen reader when the user taps the FAB. + * Uses `label` by default if specified. + */ + accessibilityLabel?: string; + /** + * Accessibility state for the FAB. This is read by the screen reader when the user taps the FAB. + */ + accessibilityState?: AccessibilityState; + /** + * Custom color for the icon and label of the `FAB`. + */ + color?: string; + /** + * Whether `FAB` is disabled. A disabled button is greyed out and `onPress` is not called on touch. + */ + disabled?: boolean; + /** + * Whether `FAB` is currently visible. + */ + visible?: boolean; + /** + * Function to execute on press. + */ + onPress?: () => void; + /** + * Function to execute on long press. + */ + onLongPress?: () => void; + /** + * Whether icon should be translated to the end of extended `FAB` or be static and stay in the same place. The default value is `dynamic`. + */ + iconMode?: 'static' | 'dynamic'; + /** + * Indicates from which direction animation should be performed. The default value is `right`. + */ + animateFrom?: 'right' | 'left'; + /** + * Whether `FAB` should start animation to extend. + */ + extended: boolean; + style?: StyleProp; + /** + * @optional + */ + theme: ReactNativePaper.Theme; + testID?: string; +}; + +const SIZE = 56; +const BORDER_RADIUS = SIZE / 2; +const SCALE = 0.9; + +const AnimatedFAB = ({ + icon, + label, + accessibilityLabel = label, + accessibilityState, + color: customColor, + disabled, + onPress, + onLongPress, + theme, + style, + visible = true, + uppercase = true, + testID, + animateFrom = 'right', + extended = false, + iconMode = 'dynamic', + ...rest +}: Props) => { + const isIOS = Platform.OS === 'ios'; + const isAnimatedFromRight = animateFrom === 'right'; + const isIconStatic = iconMode === 'static'; + const { current: visibility } = React.useRef( + new Animated.Value(visible ? 1 : 0) + ); + const { current: animFAB } = React.useRef( + new Animated.Value(0) + ); + const { scale } = theme.animation; + + const [textWidth, setTextWidth] = React.useState(0); + const [textHeight, setTextHeight] = React.useState(0); + + React.useEffect(() => { + if (visible) { + Animated.timing(visibility, { + toValue: 1, + duration: 200 * scale, + useNativeDriver: true, + }).start(); + } else { + Animated.timing(visibility, { + toValue: 0, + duration: 150 * scale, + useNativeDriver: true, + }).start(); + } + }, [visible, scale, visibility]); + + const disabledColor = color(theme.dark ? white : black) + .alpha(0.12) + .rgb() + .string(); + + const { + backgroundColor = disabled ? disabledColor : theme.colors.accent, + } = (StyleSheet.flatten(style) || {}) as ViewStyle; + + let foregroundColor; + + if (typeof customColor !== 'undefined') { + foregroundColor = customColor; + } else if (disabled) { + foregroundColor = color(theme.dark ? white : black) + .alpha(0.32) + .rgb() + .string(); + } else { + foregroundColor = getContrastingColor( + backgroundColor, + white, + 'rgba(0, 0, 0, .54)' + ); + } + + const rippleColor = color(foregroundColor).alpha(0.32).rgb().string(); + + const extendedWidth = textWidth + 1.5 * SIZE; + + const distance = isAnimatedFromRight + ? -textWidth - BORDER_RADIUS + : textWidth + BORDER_RADIUS; + + React.useEffect(() => { + Animated.timing(animFAB, { + toValue: !extended ? 0 : distance, + duration: 150 * scale, + useNativeDriver: true, + easing: Easing.linear, + }).start(); + }, [animFAB, scale, distance, extended]); + + const onTextLayout = ({ + nativeEvent, + }: NativeSyntheticEvent) => { + const currentWidth = Math.ceil(nativeEvent.lines[0].width); + const currentHeight = Math.ceil(nativeEvent.lines[0].height); + + if (currentWidth !== textWidth || currentHeight !== textHeight) { + setTextWidth(currentWidth); + setTextHeight(currentHeight); + } + }; + + const getSidesPosition = () => { + if (isAnimatedFromRight) { + return { + left: -distance, + right: undefined, + }; + } else { + return { + left: undefined, + right: distance, + }; + } + }; + + return ( + + } + > + + + + + + + + + + + + + + + + + + + + + {label} + + + + {!isIOS && ( + // Method `onTextLayout` on Android returns sizes of text visible on the screen, + // however during render the text in `FAB` isn't fully visible. In order to get + // proper text measurements there is a need to additionaly render that text, but + // wrapped in absolutely positioned `ScrollView` which height is 0. + + {label} + + )} + + ); +}; + +const styles = StyleSheet.create({ + standard: { + height: SIZE, + borderRadius: BORDER_RADIUS, + }, + disabled: { + elevation: 0, + }, + container: { + position: 'absolute', + backgroundColor: 'transparent', + borderRadius: BORDER_RADIUS, + }, + innerWrapper: { + flexDirection: 'row', + overflow: 'hidden', + borderRadius: BORDER_RADIUS, + }, + shadowWrapper: { + elevation: 0, + }, + shadow: { + elevation: 6, + borderRadius: BORDER_RADIUS, + }, + touchable: { + borderRadius: BORDER_RADIUS, + }, + iconWrapper: { + alignItems: 'center', + justifyContent: 'center', + position: 'absolute', + height: SIZE, + width: SIZE, + }, + label: { + position: 'absolute', + }, + uppercaseLabel: { + textTransform: 'uppercase', + }, + textPlaceholderContainer: { + height: 0, + position: 'absolute', + }, +}); + +export default withTheme(AnimatedFAB); diff --git a/src/index.tsx b/src/index.tsx index 871bd69d00..439d34d8fb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,6 +29,7 @@ export { default as DataTable } from './components/DataTable/DataTable'; export { default as Dialog } from './components/Dialog/Dialog'; export { default as Divider } from './components/Divider'; export { default as FAB } from './components/FAB'; +export { default as AnimatedFAB } from './components/FAB/AnimatedFAB'; export { default as HelperText } from './components/HelperText'; export { default as IconButton } from './components/IconButton'; export { default as Menu } from './components/Menu/Menu';