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
JavaScript
'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) {
}
}