Skip to content

Commit

Permalink
feat: add FABGroup component for FAB with speed dial (#218)
Browse files Browse the repository at this point in the history
Fixes #210
  • Loading branch information
lukewalczak authored and satya164 committed May 25, 2018
1 parent 0feb270 commit e1c795e
Show file tree
Hide file tree
Showing 8 changed files with 681 additions and 93 deletions.
Binary file added docs/assets/screenshots/fab-group.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 29 additions & 11 deletions example/src/FABExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,57 @@

import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { Colors, FAB, withTheme } from 'react-native-paper';
import { Colors, FAB, FABGroup, withTheme } from 'react-native-paper';
import type { Theme } from 'react-native-paper/types';

type Props = {
theme: Theme,
};

class ButtonExample extends React.Component<Props> {
type State = {
open: boolean,
};

class ButtonExample extends React.Component<Props, State> {
static title = 'Floating Action Button';

_handlePress = () => {};
state = {
open: false,
};

render() {
const {
theme: {
colors: { background },
},
} = this.props;

return (
<View style={[styles.container, { backgroundColor: background }]}>
<View style={styles.row}>
<FAB
small
icon="add"
style={styles.fab}
onPress={this._handlePress}
/>
<FAB icon="favorite" style={styles.fab} onPress={this._handlePress} />
<FAB small icon="add" style={styles.fab} onPress={() => {}} />
<FAB icon="favorite" style={styles.fab} onPress={() => {}} />
<FAB
icon="done"
label="Extended FAB"
style={styles.fab}
onPress={this._handlePress}
onPress={() => {}}
/>
<FABGroup
open={this.state.open}
icon={this.state.open ? 'today' : 'add'}
actions={[
{ icon: 'add', onPress: () => {} },
{ icon: 'star', label: 'Star', onPress: () => {} },
{ icon: 'email', label: 'Email', onPress: () => {} },
{ icon: 'notifications', label: 'Remind', onPress: () => {} },
]}
onStateChange={({ open }) => this.setState({ open })}
onPress={() => {
if (this.state.open) {
// do something if the speed dial is open
}
}}
/>
</View>
</View>
Expand Down
143 changes: 143 additions & 0 deletions src/components/CrossFadeIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* @flow */

import * as React from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { polyfill } from 'react-lifecycles-compat';
import Icon, { isValidIcon, isEqualIcon } from './Icon';
import type { IconSource } from './Icon';

type Props = {
/**
* Icon to display for the `CrossFadeIcon`.
*/
source: IconSource,
/**
* Color of the icon.
*/
color: string,
/**
* Size of the icon.
*/
size: number,
};

type State = {
currentIcon: IconSource,
previousIcon: ?IconSource,
fade: Animated.Value,
};

class CrossFadeIcon extends React.Component<Props, State> {
static getDerivedStateFromProps(nextProps: Props, nextState: State) {
if (nextState.currentIcon === nextProps.source) {
return null;
}

return {
currentIcon: nextProps.source,
previousIcon: nextState.currentIcon,
};
}

state = {
currentIcon: this.props.source,
previousIcon: null,
fade: new Animated.Value(1),
};

componentDidUpdate(prevProps: Props, prevState: State) {
const { previousIcon } = this.state;

if (
!isValidIcon(previousIcon) ||
isEqualIcon(previousIcon, prevState.previousIcon)
) {
return;
}

this.state.fade.setValue(1);

Animated.timing(this.state.fade, {
duration: 200,
toValue: 0,
}).start();
}

render() {
const { color, size } = this.props;
const opacityPrev = this.state.fade;
const opacityNext = this.state.previousIcon
? this.state.fade.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
})
: 1;

const rotatePrev = this.state.fade.interpolate({
inputRange: [0, 1],
outputRange: ['-90deg', '0deg'],
});

const rotateNext = this.state.previousIcon
? this.state.fade.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '-180deg'],
})
: '0deg';

return (
<View
style={[
styles.content,
{
height: size,
width: size,
},
]}
>
{this.state.previousIcon ? (
<Animated.View
style={[
styles.icon,
{
opacity: opacityPrev,
transform: [{ rotate: rotatePrev }],
},
]}
>
<Icon name={this.state.previousIcon} size={size} color={color} />
</Animated.View>
) : null}
<Animated.View
style={[
styles.icon,
{
opacity: opacityNext,
transform: [{ rotate: rotateNext }],
},
]}
>
<Icon name={this.state.currentIcon} size={size} color={color} />
</Animated.View>
</View>
);
}
}

polyfill(CrossFadeIcon);

export default CrossFadeIcon;

const styles = StyleSheet.create({
content: {
alignItems: 'center',
justifyContent: 'center',
},
icon: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
});
16 changes: 10 additions & 6 deletions src/components/FAB.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

import color from 'color';
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import { StyleSheet, View, Animated } from 'react-native';
import Paper from './Paper';
import Icon from './Icon';
import CrossFadeIcon from './CrossFadeIcon';
import Text from './Typography/Text';
import TouchableRipple from './TouchableRipple';
import { white } from '../styles/colors';
import withTheme from '../core/withTheme';
import type { Theme } from '../types';
import type { IconSource } from './Icon';

const AnimatedPaper = Animated.createAnimatedComponent(Paper);

type Props = {
/**
* Icon to display for the `FAB`.
Expand Down Expand Up @@ -80,7 +82,8 @@ class FAB extends React.Component<Props> {
...rest
} = this.props;

const backgroundColor = theme.colors.accent;
const { backgroundColor = theme.colors.accent } =
StyleSheet.flatten(style) || {};
const isDark =
typeof dark === 'boolean' ? dark : !color(backgroundColor).light();
const textColor = iconColor || (isDark ? white : 'rgba(0, 0, 0, .54)');
Expand All @@ -90,7 +93,7 @@ class FAB extends React.Component<Props> {
.string();

return (
<Paper
<AnimatedPaper
{...rest}
style={[{ backgroundColor, elevation: 12 }, styles.container, style]}
>
Expand All @@ -105,8 +108,9 @@ class FAB extends React.Component<Props> {
styles.content,
label ? styles.extended : small ? styles.small : styles.standard,
]}
pointerEvents="none"
>
<Icon name={icon} size={24} color={textColor} />
<CrossFadeIcon source={icon} size={24} color={textColor} />
{label ? (
<Text
style={[
Expand All @@ -119,7 +123,7 @@ class FAB extends React.Component<Props> {
) : null}
</View>
</TouchableRipple>
</Paper>
</AnimatedPaper>
);
}
}
Expand Down
Loading

0 comments on commit e1c795e

Please sign in to comment.