UNPKG

react-native-pins

Version:

Pins for displaying progress (active / non-active) with animation and feedback ⚫⚫⚪⚪⚪

348 lines (312 loc) 8.98 kB
/** * @author Luke Brandon Farrell * @description Pins component with shake animation */ import React, { Component } from "react"; import { Animated, StyleSheet, Vibration } from "react-native"; import PropTypes from "prop-types"; class Pins extends Component { /** * [ Built-in React method. ] * * Allows us to render JSX to the screen */ constructor(props) { super(props); this.state = { shake: new Animated.Value(0), animatedPinValue: new Animated.Value(0), positionOfActivePin: 1, prevPinSizes: [], currentPinSizes: [], }; this.setPositionOfPins = this.setPositionOfPins.bind(this); this.getPinSize = this.getPinSize.bind(this); this.updatePinSizes = this.updatePinSizes.bind(this); } /** * [ Built-in React method. ] * * Executed when the component is mounted to the screen. */ componentDidMount() { if (this.props.onRef != undefined) { this.props.onRef(this); } // Get and set the initial pin sizes const { numberOfPins } = this.props; let initialPinSizes = []; for (let p = 0; p < numberOfPins; p++) { let size = this.getPinSize(p); initialPinSizes[p] = size; } this.setState({ prevPinSizes: initialPinSizes, currentPinSizes: initialPinSizes, }); } /** * [ Built-in React method. ] * * Executed when the component is unmounted from the screen */ componentWillUnmount() { if (this.props.onRef != undefined) { this.props.onRef(undefined); } } /** * [ Built-in React method. ] * * Executed when there is any changes to the props or state */ componentDidUpdate(prevProps) { const prevNumberOfPinsActive = prevProps.numberOfPinsActive; const { numberOfPinsActive } = this.props; if (prevNumberOfPinsActive != numberOfPinsActive) { this.setPositionOfPins(prevNumberOfPinsActive).then(() => { this.updatePinSizes(); // Reset animation to so we can reanimate the pins this.state.animatedPinValue.setValue(0); Animated.timing(this.state.animatedPinValue, { duration: 300, toValue: 1, useNativeDriver: false, }).start(); }); } } shouldComponentUpdate(nextProps, nextState) { return ( this.state.currentPinSizes != nextState.currentPinSizes || this.props.numberOfPinsActive != nextProps.numberOfPinsActive ); } /** * [ Built-in React method. ] * * Allows us to render JSX to the screen */ render() { /** Styles */ const { containerDefaultStyle, pinDefaultStyle, pinActiveDefaultStyle, } = styles; /** Props */ const { numberOfPinsActive, numberOfPins, numberOfPinsMaximum, // Style Props containerStyle, pinStyle, pinActiveStyle, activeOnly, pinSize, spacing, } = this.props; /** State */ const { shake, animatedPinValue, prevPinSizes, currentPinSizes, } = this.state; // Create the pins from set props const pins = []; const hasMaxNumberOfPins = numberOfPinsMaximum < numberOfPins && numberOfPinsMaximum > 0 && numberOfPins; for (let p = 0; p < numberOfPins; p++) { const prevSize = prevPinSizes[p]; const currentSize = currentPinSizes[p]; const currentSizeIsMoreThanZero = hasMaxNumberOfPins ? currentSize > 0 : true; const size = hasMaxNumberOfPins ? animatedPinValue.interpolate({ inputRange: [0, 1], outputRange: [prevSize, currentSize], }) : pinSize; pins.push( <Animated.View key={p} style={[ pinDefaultStyle, pinStyle, activeOnly ? p == numberOfPinsActive - 1 ? { ...pinActiveDefaultStyle, ...pinActiveStyle } : {} : p < numberOfPinsActive ? { ...pinActiveDefaultStyle, ...pinActiveStyle } : {}, pinSize && { width: size, height: size, borderRadius: size, }, spacing && currentSizeIsMoreThanZero && { marginRight: spacing, marginLeft: spacing, }, ]} /> ); } // Create the shake animation via interpolation const shakeAnimation = shake.interpolate({ inputRange: [0, 0.2, 0.4, 0.6, 0.8, 1], outputRange: [0, -20, 20, -20, 20, 0], }); return ( <Animated.View style={[ containerDefaultStyle, containerStyle, { transform: [{ translateX: shakeAnimation }], }, ]} > {pins} </Animated.View> ); } /** * Shakes the pins */ shake() { // If vibration is enabled then we vibrate on error if (this.props.vibration) { Vibration.vibrate(500); } // Reset animation to so we can reanimate this.state.shake.setValue(0); // Animate the pins to shake Animated.spring(this.state.shake, { toValue: 1, useNativeDriver: true, }).start(() => { if (this.props.animationShakeCallback) { this.props.animationShakeCallback(); } }); } /** * Sets the position of the active pin among the large pins. * @param {Number} prevNumberOfPinsActive The index of the previous active pin */ async setPositionOfPins(prevNumberOfPinsActive) { const { numberOfPinsMaximum, numberOfPinsActive } = this.props; const { positionOfActivePin } = this.state; // If index of pin increases if (numberOfPinsActive > prevNumberOfPinsActive) { // If the position of the pin is not at the right-end, we increase the position of the active pin among the large pins if (positionOfActivePin != numberOfPinsMaximum) { await this.setState({ positionOfActivePin: positionOfActivePin + 1 }); } // If index of pin decreases } else { // If the position of the pin is not at the left-end, we decrease the position of the active pin among the large pins if (positionOfActivePin != 1) { await this.setState({ positionOfActivePin: positionOfActivePin - 1 }); } } } /** * Updates the sizes of all the pins depending on the number of pins active. */ updatePinSizes() { const { currentPinSizes } = this.state; let updatedPinSizes = []; this.setState({ prevPinSizes: currentPinSizes }, () => { currentPinSizes.map((prevSize, idx) => { let size = this.getPinSize(idx); updatedPinSizes[idx] = size; }); }); this.setState({ currentPinSizes: updatedPinSizes }); } /** * Returns size of an individual pin. * @param {number} idx Index of the pin */ getPinSize(idx) { const { numberOfPinsMaximum, numberOfPinsActive, pinSize } = this.props; const { positionOfActivePin } = this.state; if ( idx > numberOfPinsActive - positionOfActivePin - 1 && idx < numberOfPinsActive - positionOfActivePin + numberOfPinsMaximum ) { return pinSize; } if ( idx == numberOfPinsActive - positionOfActivePin - 1 || idx == numberOfPinsActive - positionOfActivePin + numberOfPinsMaximum ) { return pinSize / 2; } if ( idx == numberOfPinsActive - positionOfActivePin - 2 || idx == numberOfPinsActive - positionOfActivePin + numberOfPinsMaximum + 1 ) { return pinSize / 4; } return 0; } } Pins.propTypes = { onRef: PropTypes.any, numberOfPins: PropTypes.number, numberOfPinsActive: PropTypes.number, vibration: PropTypes.bool, animationShakeCallback: PropTypes.func, numberOfPinsMaximum: PropTypes.number, activeOnly: PropTypes.bool, // Style props containerStyle: PropTypes.object, pinStyle: PropTypes.object, pinActiveStyle: PropTypes.object, pinSize: PropTypes.number, spacing: PropTypes.number, }; Pins.defaultProps = { // Number of pins to create numberOfPins: 5, // Active number of pins numberOfPinsActive: 0, // Is vibration enabled or disabled vibration: true, // Default pin size pinSize: 18, // Default spacing between pins spacing: 15, }; export default Pins; /** -------------------------------------------- */ /** Component Styling */ /** -------------------------------------------- */ const styles = StyleSheet.create({ // Style for pin container. You can use the flex // property to expand the pins to take up more space // on the screen. The default is 0.8. containerDefaultStyle: { flex: 1, flexDirection: "row", alignItems: "center", paddingTop: 25, paddingBottom: 25, }, pinDefaultStyle: { borderRadius: 9, opacity: 0.45, backgroundColor: "#FFF", }, pinActiveDefaultStyle: { opacity: 1.0, }, });