UNPKG

react-native-flip-card

Version:

The card component which has a motion of flip for React Native(iOS/Android)

312 lines (286 loc) 8.66 kB
'use strict' import React, { Component } from "react"; import PropTypes from "prop-types"; import { View, TouchableOpacity, Animated, Platform, } from "react-native"; import { ViewPropTypes } from 'deprecated-react-native-prop-types' import S from './Style.js' export default class FlipCard extends Component { static propTypes = { style: ViewPropTypes.style } constructor (props) { super(props) // set reversed boolean for detect other side size const isFlipped = (this.props.alignHeight || this.props.alignWidth) ? !props.flip : props.flip; this.state = { isFlipped: isFlipped, isFlipping: false, rotate: new Animated.Value(Number(props.flip)), mesured: false, // the flag to check whether it is measured or not. height: 0, width: 0, face: { width: 0, height: 0 }, back: { width: 0, height: 0 } } } static getDerivedStateFromProps (nextProps, prevState) { if (prevState.isFlipped !== nextProps.flip) { return { flip: nextProps.flip } } else return null } componentDidUpdate (prevProps) { if (prevProps.flip !== this.props.flip) { this._toggleCard() } } _toggleCard () { this.setState({isFlipping: true}) this.props.onFlipStart(this.state.isFlipped) this._animation(!this.state.isFlipped) } _animation (isFlipped) { if (!this.timer) { this.timer = setTimeout(() => { this.setState({isFlipped: !this.state.isFlipped}) this.timer = null }, 120) } Animated.spring(this.state.rotate, { toValue: Number(isFlipped), friction: this.props.friction, useNativeDriver: this.props.useNativeDriver } ).start((param) => { this.setState({isFlipping: false}) this.props.onFlipEnd(this.state.isFlipped) }) } componentDidMount () { if (this.props.alignHeight || this.props.alignWidth) { // need to check the other side width or height or both this.measureOtherSideTimeout = setTimeout(this.measureOtherSide.bind(this), 32); } } componentWillUnmount() { clearTimeout(this.measureOtherSideTimeout); } measureOtherSide () { this.setState({ isFlipped: !this.state.isFlipped, mesured: true }) } render () { var c = this.props.children; var transform = this.props.perspective ? [{ perspective: this.props.perspective }] : [] var render_side = false if (this.props.flipHorizontal) { transform.push( {rotateY: this.state.rotate.interpolate({ inputRange: [0, 1], outputRange: [ '0deg', '180deg' ] })} ) } if (this.props.flipVertical) { transform.push( {rotateX: this.state.rotate.interpolate({ inputRange: [0, 1], outputRange: [ '0deg', '180deg' ] })} ) } if (this.state.isFlipped) { render_side = ( <Back style={[ this.state.height > 0 && {height: this.state.height}, this.state.width > 0 && {width: this.state.width}]} flipHorizontal={this.props.flipHorizontal} flipVertical={this.props.flipVertical} perspective={this.props.perspective} onLayout={(event) => { var {x, y, width, height} = event.nativeEvent.layout var _update = Object.assign(this.state.back, {width: width, height: height}) this.setState({back: _update}) if (this.state.mesured) { if (this.props.alignHeight) { this.setState({height: Math.max(this.state.face.height, this.state.back.height)}) } if (this.props.alignWidth) { this.setState({width: Math.max(this.state.face.width, this.state.back.width)}) } } }} > {c[1]} </Back> ) } else { render_side = ( <Face style={[ this.state.height > 0 && { height: this.state.height }, this.state.width > 0 && { width: this.state.width }]} onLayout={(event) => { var {x, y, width, height} = event.nativeEvent.layout; var _update = Object.assign(this.state.face, {width: width, height: height}) this.setState({face: _update}) if (this.state.mesured) { if (this.props.alignHeight) { this.setState({height: Math.max(this.state.face.height, this.state.back.height)}) } if (this.props.alignWidth) { this.setState({width: Math.max(this.state.face.width, this.state.back.width)}) } } }} > {c[0]} </Face> ) } // FIX: ScrollView inside Flip Card not scrollable // TODO: Replace below fix with by using "disabled" function (RN 0.21 exclusive) // REF: https://github.com/facebook/react-native/pull/5931 // https://github.com/facebook/react-native/issues/2103 if (this.props.clickable) { let opacity = 0; if ( ((this.props.alignHeight || this.props.alignWidth) && this.state.mesured) || !(this.props.alignHeight || this.props.alignWidth)) { // if you set alignXXX property, we show this side after mesured // Otherwise, it's showed immediately opacity = 1; } return ( <TouchableOpacity style={{flex:1}} testID={this.props.testID} activeOpacity={1} onPress={() => { this._toggleCard(); }} > <Animated.View {...this.props} style={[ S.flipCard, { transform, opacity, }, this.props.style ]} > {render_side} </Animated.View> </TouchableOpacity> ) } else { return ( <Animated.View {...this.props} style={[S.flipCard, {transform: transform}, this.props.style ]} > {render_side} </Animated.View> ) } } } FlipCard.propTypes = { flip: PropTypes.bool, friction: PropTypes.number, perspective: PropTypes.number, flipHorizontal: PropTypes.bool, flipVertical: PropTypes.bool, clickable: PropTypes.bool, onFlipEnd: PropTypes.func, onFlipStart: PropTypes.func, alignHeight: PropTypes.bool, alignWidth: PropTypes.bool, useNativeDriver: PropTypes.bool, children (props, propName, componentName) { const prop = props[propName] if (React.Children.count(prop) !== 2) { return new Error( '`' + componentName + '` ' + 'should contain exactly two children. ' + 'The first child represents the front of the card. ' + 'The second child represents the back of the card.' ) } } } FlipCard.defaultProps = { flip: false, friction: 6, perspective: 1000, flipHorizontal: false, flipVertical: true, clickable: true, onFlipEnd: () => {}, onFlipStart: () => {}, alignHeight: false, alignWidth: false, useNativeDriver: true, } export class Face extends Component { render () { return ( <View style={[S.face, this.props.style]} onLayout={this.props.onLayout} > {this.props.children} </View> ) } } Face.propTypes = { children (props, propName, componentName) { } } export class Back extends Component { render () { var transform = [] if (this.props.flipHorizontal) { transform.push({scaleX: -1}) if (Platform.OS === "android") { transform.push({perspective: this.props.perspective}) } } if (this.props.flipVertical) { transform.push({scaleY: -1}) if (Platform.OS === "android") { transform.push({perspective: this.props.perspective}) } } return ( <View style={[ S.back, this.props.style, {transform: transform} ]} onLayout={this.props.onLayout}> {this.props.children} </View> ) } } Back.defaultProps = { flipHorizontal: false, flipVertical: true, perspective: 1000, } Back.propTypes = { flipHorizontal: PropTypes.bool, flipVertical: PropTypes.bool, perspective: PropTypes.number, children (props, propName, componentName) { } }