UNPKG

rn-fab

Version:

A simple React Native Floating Action Button using NativeDriver for animations.

251 lines (206 loc) 5.86 kB
import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import { View, Animated, Easing, TouchableWithoutFeedback } from 'react-native'; import { styles, radius } from './styles.js'; export default class Ripple extends PureComponent { static defaultProps = { ...TouchableWithoutFeedback.defaultProps, rippleColor: 'rgb(0, 0, 0)', rippleOpacity: 0.30, rippleDuration: 400, rippleSize: 0, rippleContainerBorderRadius: 0, rippleCentered: false, rippleSequential: false, rippleFades: true, disabled: false, onRippleAnimation: (animation, callback) => animation.start(callback), }; static propTypes = { ...Animated.View.propTypes, ...TouchableWithoutFeedback.propTypes, rippleColor: PropTypes.string, rippleOpacity: PropTypes.number, rippleDuration: PropTypes.number, rippleSize: PropTypes.number, rippleContainerBorderRadius: PropTypes.number, rippleCentered: PropTypes.bool, rippleSequential: PropTypes.bool, rippleFades: PropTypes.bool, disabled: PropTypes.bool, onRippleAnimation: PropTypes.func, }; constructor(props) { super(props); this.onLayout = this.onLayout.bind(this); this.onPress = this.onPress.bind(this); this.onPressIn = this.onPressIn.bind(this); this.onPressOut = this.onPressOut.bind(this); this.onLongPress = this.onLongPress.bind(this); this.onAnimationEnd = this.onAnimationEnd.bind(this); this.renderRipple = this.renderRipple.bind(this); this.unique = 0; this.mounted = false; this.state = { width: 0, height: 0, ripples: [], }; } componentDidMount() { this.mounted = true; } componentWillUnmount() { this.mounted = false; } onLayout(event) { let { width, height } = event.nativeEvent.layout; let { onLayout } = this.props; if ('function' === typeof onLayout) { onLayout(event); } this.setState({ width, height }); } onPress(event) { let { ripples } = this.state; let { onPress, rippleSequential } = this.props; if (!rippleSequential || !ripples.length) { if ('function' === typeof onPress) { requestAnimationFrame(() => onPress(event)); } this.startRipple(event); } } onLongPress(event) { let { onLongPress } = this.props; if ('function' === typeof onLongPress) { requestAnimationFrame(() => onLongPress(event)); } this.startRipple(event); } onPressIn(event) { let { onPressIn } = this.props; if ('function' === typeof onPressIn) { onPressIn(event); } } onPressOut(event) { let { onPressOut } = this.props; if ('function' === typeof onPressOut) { onPressOut(event); } } onAnimationEnd() { if (this.mounted) { this.setState(({ ripples }) => ({ ripples: ripples.slice(1) })); } } startRipple(event) { let { width, height } = this.state; let { rippleDuration, rippleCentered, rippleSize, onRippleAnimation, } = this.props; let w2 = 0.5 * width; let h2 = 0.5 * height; let { locationX, locationY } = rippleCentered? { locationX: w2, locationY: h2 }: event.nativeEvent; let offsetX = Math.abs(w2 - locationX); let offsetY = Math.abs(h2 - locationY); let R = rippleSize > 0? 0.5 * rippleSize: Math.sqrt(Math.pow(w2 + offsetX, 2) + Math.pow(h2 + offsetY, 2)); let ripple = { unique: this.unique++, progress: new Animated.Value(0), locationX, locationY, R, }; let animation = Animated .timing(ripple.progress, { toValue: 1, easing: Easing.out(Easing.ease), duration: rippleDuration, useNativeDriver: true, }); onRippleAnimation(animation, this.onAnimationEnd); this.setState(({ ripples }) => ({ ripples: ripples.concat(ripple) })); } renderRipple({ unique, progress, locationX, locationY, R }) { let { rippleColor, rippleOpacity, rippleFades } = this.props; let rippleStyle = { top: locationY - radius, left: locationX - radius, backgroundColor: rippleColor, transform: [{ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [0.5 / radius, R / radius], }), }], opacity: rippleFades? progress.interpolate({ inputRange: [0, 1], outputRange: [rippleOpacity, 0], }): rippleOpacity, }; return ( <Animated.View style={[styles.ripple, rippleStyle]} key={unique} /> ); } render() { let { ripples } = this.state; let { onPress, onPressIn, onPressOut, onLongPress, onLayout } = this; let { delayLongPress, delayPressIn, delayPressOut, disabled, hitSlop, pressRetentionOffset, children, rippleContainerBorderRadius, testID, nativeID, accessible, accessibilityLabel, onLayout: __ignored__, ...props } = this.props; let touchableProps = { delayLongPress, delayPressIn, delayPressOut, disabled, hitSlop, pressRetentionOffset, onPress, onPressIn, testID, nativeID, accessible, accessibilityLabel, onPressOut, onLongPress: props.onLongPress? onLongPress : undefined, onLayout, }; let containerStyle = { borderRadius: rippleContainerBorderRadius, }; return ( <TouchableWithoutFeedback {...touchableProps}> <Animated.View {...props} pointerEvents='box-only'> {children} <View style={[styles.container, containerStyle]}> {ripples.map(this.renderRipple)} </View> </Animated.View> </TouchableWithoutFeedback> ); } }