rnfa-rb42
Version:
Original project: https://github.com/santomegonzalo/react-native-floating-action
693 lines (617 loc) • 16.5 kB
JavaScript
import React, { Component } from "react"; // eslint-disable-line
import PropTypes from "prop-types";
import {
StyleSheet,
Image,
Animated,
Dimensions,
TouchableOpacity,
LayoutAnimation,
Platform,
Keyboard
} from "react-native";
import FloatingActionItem from "./FloatingActionItem";
import AddIcon from "./AddIcon";
import { isIphoneX } from "./utils/platform";
import { getTouchableComponent, getRippleProps } from "./utils/touchable";
const DEVICE_WIDTH = Dimensions.get("window").width;
const DEFAULT_SHADOW_PROPS = {
shadowOpacity: 0.35,
shadowOffset: {
width: 0,
height: 5
},
shadowColor: "#000000",
shadowRadius: 3
};
class FloatingAction extends Component {
constructor(props) {
super(props);
this.state = {
active: false
};
this.mainBottomAnimation = new Animated.Value(
this.distanceToVerticalEdge + props.mainVerticalDistance
);
this.actionsBottomAnimation = new Animated.Value(
props.buttonSize +
this.distanceToVerticalEdge +
props.actionsPaddingTopBottom +
props.mainVerticalDistance
);
this.animation = new Animated.Value(0);
this.actionsAnimation = new Animated.Value(0);
this.visibleAnimation = new Animated.Value(props.visible ? 0 : 1);
/*
* this animation will fix an error on ReactNative (Android) where
* interpolations with 0 and 1 don't work as expected.
*/
this.fadeAnimation = new Animated.Value(props.visible ? 1 : 0);
}
componentDidMount() {
const { openOnMount, listenKeyboard } = this.props;
if (openOnMount) {
this.animateButton();
}
if (listenKeyboard) {
const showEvent =
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
const hideEvent =
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
this.keyboardWillShowListener = Keyboard.addListener(
showEvent,
this.onKeyboardShow
);
this.keyboardWillHideListener = Keyboard.addListener(
hideEvent,
this.onKeyboardHideHide
);
}
}
componentDidUpdate(prevProps) {
const { visible } = this.props;
if (prevProps.visible !== visible) {
if (visible) {
Animated.parallel([
Animated.spring(this.visibleAnimation, { toValue: 0, useNativeDriver: false }),
Animated.spring(this.fadeAnimation, { toValue: 1, useNativeDriver: false })
]).start();
}
if (!visible) {
Animated.parallel([
Animated.spring(this.visibleAnimation, { toValue: 1, useNativeDriver: false }),
Animated.spring(this.fadeAnimation, { toValue: 0, useNativeDriver: false })
]).start();
}
}
}
componentWillUnmount() {
const { listenKeyboard } = this.props;
if (listenKeyboard) {
this.keyboardWillShowListener.remove();
this.keyboardWillHideListener.remove();
}
}
get distanceToHorizontalEdge() {
const { distanceToEdge } = this.props;
return typeof distanceToEdge === 'number'
? distanceToEdge
: distanceToEdge.horizontal;
}
get distanceToVerticalEdge() {
const { distanceToEdge } = this.props;
return typeof distanceToEdge === 'number'
? distanceToEdge
: distanceToEdge.vertical;
}
onKeyboardShow = e => {
const { buttonSize, actionsPaddingTopBottom } = this.props;
const { height } = e.endCoordinates;
Animated.parallel([
Animated.spring(this.actionsBottomAnimation, {
bounciness: 0,
useNativeDriver: false,
toValue:
buttonSize +
this.distanceToVerticalEdge +
actionsPaddingTopBottom +
height -
(isIphoneX() ? 40 : 0),
duration: 250
}),
Animated.spring(this.mainBottomAnimation, {
bounciness: 0,
useNativeDriver: false,
toValue: this.distanceToVerticalEdge + height - (isIphoneX() ? 40 : 0),
duration: 250
})
]).start();
};
onKeyboardHideHide = () => {
const { buttonSize, actionsPaddingTopBottom } = this.props;
Animated.parallel([
Animated.spring(this.actionsBottomAnimation, {
bounciness: 0,
useNativeDriver: false,
toValue: buttonSize + this.distanceToVerticalEdge + actionsPaddingTopBottom,
duration: 250
}),
Animated.spring(this.mainBottomAnimation, {
bounciness: 0,
useNativeDriver: false,
toValue: this.distanceToVerticalEdge,
duration: 250
})
]).start();
};
getShadow = () => {
const { shadow } = this.props;
return {
...DEFAULT_SHADOW_PROPS,
...shadow
};
};
getIcon = () => {
const {
actions,
floatingIcon,
overrideWithAction,
iconWidth,
iconHeight,
iconColor
} = this.props;
if (overrideWithAction) {
const { icon } = actions[0];
if (React.isValidElement(icon)) {
return icon;
}
return (
<Image style={{ width: iconWidth, height: iconHeight }} source={icon} />
);
}
if (floatingIcon) {
if (React.isValidElement(floatingIcon)) {
return floatingIcon;
}
return (
<Image
style={{ width: iconWidth, height: iconHeight }}
source={floatingIcon}
/>
);
}
return <AddIcon width={iconWidth} height={iconHeight} backgroundColor={iconColor} />;
};
reset = () => {
const { animated, onClose } = this.props;
if (animated) {
Animated.spring(this.animation, { toValue: 0, useNativeDriver: false }).start();
Animated.spring(this.actionsAnimation, { toValue: 0, useNativeDriver: false }).start();
}
this.updateState(
{
active: false
},
() => {
if (onClose) {
onClose();
}
}
);
};
animateButton = () => {
const {
overrideWithAction,
actions,
floatingIcon,
animated,
dismissKeyboardOnPress,
onPressMain,
onOpen
} = this.props;
const { active } = this.state;
if (dismissKeyboardOnPress) {
Keyboard.dismiss();
}
if (overrideWithAction) {
this.handlePressItem(actions[0].name);
return;
}
if (onPressMain) {
onPressMain(!active);
}
if (!active) {
if (!floatingIcon) {
if (animated) {
Animated.spring(this.animation, { toValue: 1, useNativeDriver: false }).start();
}
}
if (animated) {
Animated.spring(this.actionsAnimation, { toValue: 1, useNativeDriver: false }).start();
// only execute it for the background to prevent extra calls
LayoutAnimation.configureNext({
duration: 180,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity
}
});
}
this.updateState(
{
active: true
},
() => {
if (onOpen) {
onOpen();
}
}
);
} else {
this.reset();
}
};
updateState = (nextState, callback) => {
const { onStateChange } = this.props;
const { active } = this.state;
this.setState(nextState, () => {
if (callback) {
callback();
}
if (onStateChange) {
onStateChange({
isActive: active
});
}
});
};
handlePressBackdrop = () => {
const { onPressBackdrop } = this.props;
if (onPressBackdrop) {
onPressBackdrop();
}
this.reset();
};
handlePressItem = itemName => {
const { onPressItem } = this.props;
if (onPressItem) {
onPressItem(itemName);
}
this.reset();
};
renderMainButton() {
const {
// @deprecated in favor of "color"
buttonColor, // eslint-disable-line
buttonSize,
color,
position,
overrideWithAction,
animated
} = this.props;
const { active } = this.state;
if (buttonColor) {
console.warn(
'FloatingAction: "buttonColor" property was deprecated. Please use "color"'
);
}
const mainButtonColor = buttonColor || color;
let animatedVisibleView;
let animatedViewStyle;
if (animated) {
animatedVisibleView = {
opacity: this.fadeAnimation,
transform: [
{
rotate: this.visibleAnimation.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "90deg"],
useNativeDriver: true
})
},
{
scale: this.visibleAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
useNativeDriver: true
})
}
]
};
animatedViewStyle = {
transform: [
{
rotate: this.animation.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "45deg"],
useNativeDriver: true
})
}
]
};
if (overrideWithAction) {
animatedViewStyle = {};
}
} else if (active) {
animatedVisibleView = {};
animatedViewStyle = {
transform: [
{
rotate: "45deg"
}
]
};
} else {
animatedVisibleView = {};
animatedViewStyle = {
transform: [
{
rotate: "0deg"
}
]
};
}
const Touchable = getTouchableComponent();
const propStyles = {
backgroundColor: mainButtonColor,
bottom: this.mainBottomAnimation // I need to imporove this to run on native thread and not on JS thread
};
if (["left", "right"].indexOf(position) > -1) {
propStyles[position] = this.distanceToHorizontalEdge;
}
const sizeStyle = {
width: buttonSize,
height: buttonSize,
borderRadius: buttonSize / 2
};
return (
<Animated.View
style={[
styles.buttonContainer,
sizeStyle,
styles[`${position}Button`],
propStyles,
animatedVisibleView,
this.getShadow()
]}
accessible
accessibilityLabel="Floating Action Button"
>
<Touchable
{...getRippleProps(mainButtonColor)}
style={[styles.button, sizeStyle]}
activeOpacity={0.85}
onPress={this.animateButton}
>
<Animated.View
style={[styles.buttonTextContainer, sizeStyle, animatedViewStyle]}
>
{this.getIcon()}
</Animated.View>
</Touchable>
</Animated.View>
);
}
renderActions() {
const {
actions,
position,
overrideWithAction,
distanceToEdge,
actionsPaddingTopBottom,
animated
} = this.props;
const { active } = this.state;
if (!actions || actions.length === 0) {
return undefined;
}
if (overrideWithAction) {
return null;
}
let animatedActionsStyle;
if (animated) {
animatedActionsStyle = {
opacity: this.actionsAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0, 1]
})
};
} else {
animatedActionsStyle = { opacity: active ? 1 : 0 };
}
const actionsStyles = [
styles.actions,
styles[`${position}Actions`],
animatedActionsStyle,
{
bottom: this.actionsBottomAnimation
}
];
if (active) {
actionsStyles.push(styles[`${position}ActionsVisible`]);
}
const sortedActions = actions.sort((a, b) => a.position - b.position);
return (
<Animated.View style={actionsStyles} pointerEvents="box-none">
{sortedActions.map(action => {
const textColor = action.textColor || action.actionsTextColor;
const textBackground =
action.textBackground || action.actionsTextBackground;
return (
<FloatingActionItem
paddingTopBottom={actionsPaddingTopBottom}
distanceToEdge={distanceToEdge}
key={action.name}
textColor={textColor}
textBackground={textBackground}
shadow={this.getShadow()}
{...action}
position={position}
active={active}
onPress={this.handlePressItem}
animated={animated}
/>
);
})}
</Animated.View>
);
}
renderTappableBackground() {
const { overlayColor } = this.props;
// TouchableOpacity don't require a child
return (
<TouchableOpacity
activeOpacity={1}
style={[styles.overlay, { backgroundColor: overlayColor }]}
onPress={this.handlePressBackdrop}
/>
);
}
render() {
const { active } = this.state;
const { showBackground } = this.props;
return (
<Animated.View
pointerEvents="box-none"
style={[styles.overlay, { backgroundColor: "transparent" }]}
>
{active && showBackground && this.renderTappableBackground()}
{this.renderActions()}
{this.renderMainButton()}
</Animated.View>
);
}
}
FloatingAction.propTypes = {
actions: PropTypes.arrayOf(
PropTypes.shape({
color: PropTypes.string,
icon: PropTypes.any,
name: PropTypes.string.isRequired,
buttonSize: PropTypes.number,
text: PropTypes.string,
textBackground: PropTypes.string,
textColor: PropTypes.string,
component: PropTypes.func,
animated: PropTypes.bool
})
),
animated: PropTypes.bool,
color: PropTypes.string,
distanceToEdge: PropTypes.oneOfType([
PropTypes.number,
PropTypes.shape({
vertical: PropTypes.number,
horizontal: PropTypes.number
})
]),
mainVerticalDistance: PropTypes.number,
visible: PropTypes.bool,
overlayColor: PropTypes.string,
position: PropTypes.oneOf(["right", "left", "center"]),
overrideWithAction: PropTypes.bool, // replace mainAction with first action from actions
floatingIcon: PropTypes.any,
showBackground: PropTypes.bool,
openOnMount: PropTypes.bool,
actionsPaddingTopBottom: PropTypes.number,
buttonSize: PropTypes.number,
iconHeight: PropTypes.number,
iconWidth: PropTypes.number,
listenKeyboard: PropTypes.bool,
dismissKeyboardOnPress: PropTypes.bool,
shadow: PropTypes.shape({
shadowOpacity: PropTypes.number,
shadowOffset: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number
}),
shadowColor: PropTypes.string,
shadowRadius: PropTypes.number
}),
onPressItem: PropTypes.func,
onPressMain: PropTypes.func,
onClose: PropTypes.func,
onOpen: PropTypes.func,
onPressBackdrop: PropTypes.func,
onStateChange: PropTypes.func
};
FloatingAction.defaultProps = {
dismissKeyboardOnPress: false,
listenKeyboard: false,
actionsPaddingTopBottom: 8,
overrideWithAction: false,
visible: true,
color: "#1253bc",
overlayColor: "rgba(68, 68, 68, 0.6)",
position: "right",
distanceToEdge: 30,
openOnMount: false,
showBackground: true,
buttonSize: 56,
iconHeight: 15,
iconWidth: 15,
iconColor: '#fff',
mainVerticalDistance: 0,
animated: true,
shadow: {}
};
const styles = StyleSheet.create({
actions: {
position: "absolute",
bottom: 85,
zIndex: 10
},
rightActions: {
alignItems: "flex-end",
right: -1000 // this magic number will make always disspear the text from screen
},
leftActions: {
alignItems: "flex-start",
left: -1000 // this magic number will make always disspear the text from screen
},
centerActions: {
left: -1000
},
rightActionsVisible: {
right: 0
},
leftActionsVisible: {
left: 0
},
centerActionsVisible: {
left: DEVICE_WIDTH / 2 - 30
},
overlay: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
top: 0,
elevation: 0,
zIndex: 0
},
buttonContainer: {
overflow: Platform.OS === "ios" ? "visible" : "hidden",
zIndex: 2,
alignItems: "center",
justifyContent: "center",
elevation: 5,
position: "absolute"
},
button: {
zIndex: 3,
alignItems: "center",
justifyContent: "center"
},
rightButton: {},
leftButton: {},
centerButton: {
left: DEVICE_WIDTH / 2 - 28
},
buttonTextContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
export default FloatingAction;