diff --git a/SwipeCards.js b/SwipeCards.js index fd8029f..d54d71a 100644 --- a/SwipeCards.js +++ b/SwipeCards.js @@ -1,86 +1,86 @@ /* Gratefully copied from https://github.com/brentvatne/react-native-animated-demo-tinder */ 'use strict'; - -import React, { Component } from 'react'; +'use strict' import PropTypes from 'prop-types'; +import React, { Component } from 'react' + import { - StyleSheet, - Text, - View, Animated, - PanResponder, Dimensions, - Image -} from 'react-native'; + PanResponder, + Platform, + StyleSheet, + Text, + TouchableWithoutFeedback, + View +} from 'react-native' + +import clamp from 'clamp' -import clamp from 'clamp'; -import Defaults from './Defaults.js'; +const SWIPE_THRESHOLD = 70 -const viewport = Dimensions.get('window') -const SWIPE_THRESHOLD = 120; +const {width, height} = Dimensions.get('window') const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: 'transparent', + backgroundColor: 'transparent' }, yup: { - borderColor: 'green', - borderWidth: 2, - position: 'absolute', - padding: 20, - bottom: 20, + borderColor: '#4CAF50', + borderWidth: 10, + padding: 5, borderRadius: 5, - right: 0, + zIndex: 100, + left: 0, + width: 220 }, yupText: { - fontSize: 16, - color: 'green', - }, - maybe: { - borderColor: 'blue', - borderWidth: 2, - position: 'absolute', - padding: 20, - bottom: 20, - borderRadius: 5, - right: 20, - }, - maybeText: { - fontSize: 16, - color: 'blue', + color: '#4CAF50', + zIndex: 30, + textAlign: 'left', + fontWeight: 'bold', + backgroundColor: 'transparent' }, nope: { - borderColor: 'red', - borderWidth: 2, - position: 'absolute', - bottom: 20, - padding: 20, + borderColor: '#f44336', + borderWidth: 10, + padding: 5, borderRadius: 5, - left: 0, + zIndex: 100, + left: 20, + width: 250 }, nopeText: { - fontSize: 16, - color: 'red', + color: '#f44336', + zIndex: 30, + textAlign: 'right', + fontWeight: 'bold', + backgroundColor: 'transparent' + }, + card: { + shadowColor: 'black', + shadowOffset: {width: 0, height: 15}, + shadowOpacity: 0.5, + shadowRadius: 15, + elevation: 3 } -}); +}) -//Components could be unloaded and loaded and we will loose the users currentIndex, we can persist it here. -let currentIndex = {}; -let guid = 0; +// Components could be unloaded and loaded and we will loose the users currentIndex, we can persist it here. +let currentIndex = {} +let guid = 0 export default class SwipeCards extends Component { static propTypes = { cards: PropTypes.array, cardKey: PropTypes.string, - hasMaybeAction: PropTypes.bool, loop: PropTypes.bool, - onLoop: PropTypes.func, allowGestureTermination: PropTypes.bool, stack: PropTypes.bool, stackGuid: PropTypes.string, @@ -89,47 +89,40 @@ export default class SwipeCards extends Component { stackOffsetY: PropTypes.number, renderNoMoreCards: PropTypes.func, showYup: PropTypes.bool, - showMaybe: PropTypes.bool, showNope: PropTypes.bool, handleYup: PropTypes.func, - handleMaybe: PropTypes.func, handleNope: PropTypes.func, yupText: PropTypes.string, - yupView: PropTypes.element, - maybeText: PropTypes.string, - maybeView: PropTypes.element, - nopeText: PropTypes.string, - noView: PropTypes.element, + noText: PropTypes.string, onClickHandler: PropTypes.func, renderCard: PropTypes.func, cardRemoved: PropTypes.func, dragY: PropTypes.bool, - smoothTransition: PropTypes.bool + smoothTransition: PropTypes.bool, + stackX: PropTypes.number, + stackY: PropTypes.number }; static defaultProps = { cards: [], - cardKey: 'key', - hasMaybeAction: false, + cardKey: 'uid', loop: false, - onLoop: () => null, allowGestureTermination: true, - stack: false, + stack: true, stackDepth: 5, stackOffsetX: 25, stackOffsetY: 0, + stackOffsetFromScreenX: 10, + stackOffsetFromScreenY: 80, showYup: true, - showMaybe: true, showNope: true, handleYup: (card) => null, - handleMaybe: (card) => null, handleNope: (card) => null, - nopeText: "Nope!", - maybeText: "Maybe!", - yupText: "Yup!", - onClickHandler: () => { alert('tap') }, - onDragStart: () => {}, - onDragRelease: () => {}, + nopeText: 'NOPE!', + yupText: 'YUP!', + onClickHandler: () => { + alert('tap') + }, cardRemoved: (ix) => null, renderCard: (card) => null, style: styles.container, @@ -137,412 +130,409 @@ export default class SwipeCards extends Component { smoothTransition: false }; - constructor(props) { - super(props); + constructor (props) { + super(props) - //Use a persistent variable to track currentIndex instead of a local one. - this.guid = this.props.guid || guid++; - if (!currentIndex[this.guid]) currentIndex[this.guid] = 0; + // Use a persistent variable to track currentIndex instead of a local one. + this.guid = this.props.guid || guid++ + if (!currentIndex[this.guid]) currentIndex[this.guid] = 0 this.state = { - pan: new Animated.ValueXY(0), + pan: new Animated.ValueXY(), enter: new Animated.Value(0.5), cards: [].concat(this.props.cards), - card: this.props.cards[currentIndex[this.guid]], - }; + card: this.props.cards[currentIndex[this.guid]] + } + + this.lastX = 0 + this.lastY = 0 - this.lastX = 0; - this.lastY = 0; + this.cardAnimation = null - this.cardAnimation = null; + const self = this this._panResponder = PanResponder.create({ + onStartShouldSetPanResponderCapture: (e, gestureState) => { + this.lastX = gestureState.moveX + this.lastY = gestureState.moveY + return false + }, onMoveShouldSetPanResponderCapture: (e, gestureState) => { - if (Math.abs(gestureState.dx) > 3 || Math.abs(gestureState.dy) > 3) { - this.props.onDragStart(); - return true; - } - return false; + return (Math.abs(this.lastX - gestureState.moveX) > 5 || Math.abs(this.lastY - gestureState.moveY) > 5) }, onPanResponderGrant: (e, gestureState) => { - this.state.pan.setOffset({ x: this.state.pan.x._value, y: this.state.pan.y._value }); - this.state.pan.setValue({ x: 0, y: 0 }); + this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value}) + this.state.pan.setValue({x: 0, y: 0}) + }, + + onPanResponderTerminate: (e, gestureState) => { + console.log(gestureState) }, onPanResponderTerminationRequest: (evt, gestureState) => this.props.allowGestureTermination, onPanResponderMove: Animated.event([ - null, { dx: this.state.pan.x, dy: this.props.dragY ? this.state.pan.y : 0 }, + null, {dx: this.state.pan.x, dy: this.props.dragY ? this.state.pan.y : 0} ]), onPanResponderRelease: (e, {vx, vy, dx, dy}) => { - this.props.onDragRelease() - this.state.pan.flattenOffset(); - let velocity; - if (Math.abs(dx) <= 5 && Math.abs(dy) <= 5) //meaning the gesture did not cover any distance + this.state.pan.flattenOffset() + let velocity + if (Math.abs(dx) <= 5 && Math.abs(dy) <= 5) // meaning the gesture did not cover any distance { + // if (Platform.OS !== 'ios') { this.props.onClickHandler(this.state.card) - } - - if (vx > 0) { - velocity = clamp(vx, 3, 5); - } else if (vx < 0) { - velocity = clamp(vx * -1, 3, 5) * -1; - } else { - velocity = dx < 0 ? -3 : 3; + // } } const hasSwipedHorizontally = Math.abs(this.state.pan.x._value) > SWIPE_THRESHOLD const hasSwipedVertically = Math.abs(this.state.pan.y._value) > SWIPE_THRESHOLD if (hasSwipedHorizontally || (hasSwipedVertically && this.props.hasMaybeAction)) { - - let cancelled = false; + let cancelled = false const hasMovedRight = hasSwipedHorizontally && this.state.pan.x._value > 0 const hasMovedLeft = hasSwipedHorizontally && this.state.pan.x._value < 0 - const hasMovedUp = hasSwipedVertically && this.state.pan.y._value < 0 if (hasMovedRight) { - cancelled = this.props.handleYup(this.state.card); + cancelled = this.props.handleYup(this.state.card) } else if (hasMovedLeft) { - cancelled = this.props.handleNope(this.state.card); - } else if (hasMovedUp && this.props.hasMaybeAction) { - cancelled = this.props.handleMaybe(this.state.card); + cancelled = this.props.handleNope(this.state.card) } else { cancelled = true } - //Yup or nope was cancelled, return the card to normal. + // Yup or nope was cancelled, return the card to normal. if (cancelled) { - this._resetPan(); - return; + this._resetPan() + return }; - this.props.cardRemoved(currentIndex[this.guid]); + this.props.cardRemoved(currentIndex[this.guid]) if (this.props.smoothTransition) { - this._advanceState(); + this._advanceState() } else { - this.cardAnimation = Animated.decay(this.state.pan, { - velocity: { x: velocity, y: vy }, - deceleration: 0.98 - }); - this.cardAnimation.start(status => { - if (status.finished) this._advanceState(); - else this._resetState(); - - this.cardAnimation = null; - } - ); + this.cardAnimation = Animated.spring(this.state.pan, { + toValue: {x: hasMovedLeft ? -500 : 500, y: 0}, + friction: 4, + duration: 200 + }).start() + + const self = this + + setTimeout(function () { + self._advanceState() + self.cardAnimation = null + }, 200) } - } else { - this._resetPan(); + this._resetPan() } } - }); + }) } - _forceLeftSwipe() { + _forceLeftSwipe () { this.cardAnimation = Animated.timing(this.state.pan, { - toValue: { x: -500, y: 0 }, + toValue: {x: -500, y: 0} }).start(status => { - if (status.finished) this._advanceState(); - else this._resetState(); + if (status.finished) this._advanceState() + else this._resetState() - this.cardAnimation = null; - } - ); - this.props.cardRemoved(currentIndex[this.guid]); - } - - _forceUpSwipe() { - this.cardAnimation = Animated.timing(this.state.pan, { - toValue: { x: 0, y: 500 }, - }).start(status => { - if (status.finished) this._advanceState(); - else this._resetState(); - - this.cardAnimation = null; - } - ); - this.props.cardRemoved(currentIndex[this.guid]); + this.cardAnimation = null + } + ) + this.props.cardRemoved(currentIndex[this.guid]) } - _forceRightSwipe() { + _forceRightSwipe () { this.cardAnimation = Animated.timing(this.state.pan, { - toValue: { x: 500, y: 0 }, + toValue: {x: 500, y: 0} }).start(status => { - if (status.finished) this._advanceState(); - else this._resetState(); + if (status.finished) this._advanceState() + else this._resetState() - this.cardAnimation = null; - } - ); - this.props.cardRemoved(currentIndex[this.guid]); + this.cardAnimation = null + } + ) + this.props.cardRemoved(currentIndex[this.guid]) } - _goToNextCard() { - currentIndex[this.guid]++; + _goToNextCard () { + currentIndex[this.guid]++ // Checks to see if last card. // If props.loop=true, will start again from the first card. if (currentIndex[this.guid] > this.state.cards.length - 1 && this.props.loop) { - this.props.onLoop(); - currentIndex[this.guid] = 0; + currentIndex[this.guid] = 0 } this.setState({ card: this.state.cards[currentIndex[this.guid]] - }); + }) } - _goToPrevCard() { - this.state.pan.setValue({ x: 0, y: 0 }); - this.state.enter.setValue(0); - this._animateEntrance(); + _goToPrevCard () { + this.state.pan.setValue({x: 0, y: 0}) + this.state.enter.setValue(0) + this._animateEntrance() - currentIndex[this.guid]--; + currentIndex[this.guid]-- if (currentIndex[this.guid] < 0) { - currentIndex[this.guid] = 0; + currentIndex[this.guid] = 0 } this.setState({ card: this.state.cards[currentIndex[this.guid]] - }); + }) } - componentDidMount() { - this._animateEntrance(); + componentDidMount () { + this._animateEntrance() } - _animateEntrance() { + _animateEntrance () { Animated.spring( this.state.enter, - { toValue: 1, friction: 8 } - ).start(); + {toValue: 1, friction: 8} + ).start() } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps (nextProps) { if (nextProps.cards !== this.props.cards) { - if (this.cardAnimation) { - this.cardAnimation.stop(); - this.cardAnimation = null; + this.cardAnimation.stop() + this.cardAnimation = null } - currentIndex[this.guid] = 0; + currentIndex[this.guid] = 0 this.setState({ cards: [].concat(nextProps.cards), card: nextProps.cards[0] - }); + }) } } - _resetPan() { + _resetPan () { Animated.spring(this.state.pan, { - toValue: { x: 0, y: 0 }, + toValue: {x: 0, y: 0}, friction: 4 - }).start(); + }).start() } - _resetState() { - this.state.pan.setValue({ x: 0, y: 0 }); - this.state.enter.setValue(0); - this._animateEntrance(); + _resetState () { + this.state.pan.setValue({x: 0, y: 0}) + this.state.enter.setValue(0) + this._animateEntrance() } - _advanceState() { - this.state.pan.setValue({ x: 0, y: 0 }); - this.state.enter.setValue(0); - this._animateEntrance(); - this._goToNextCard(); + _advanceState () { + this.state.pan.setValue({x: 0, y: 0}) + this.state.enter.setValue(0) + this._animateEntrance() + this._goToNextCard() } /** * Returns current card object */ - getCurrentCard() { - return this.state.cards[currentIndex[this.guid]]; + getCurrentCard () { + return this.state.cards[currentIndex[this.guid]] } - renderNoMoreCards() { + renderNoMoreCards () { if (this.props.renderNoMoreCards) { - return this.props.renderNoMoreCards(); + return this.props.renderNoMoreCards() } - - return ; } /** * Renders the cards as a stack with props.stackDepth cards deep. */ - renderStack() { + renderStack () { + const self = this if (!this.state.card) { - return this.renderNoMoreCards(); + return this.renderNoMoreCards() } - //Get the next stack of cards to render. - let cards = this.state.cards.slice(currentIndex[this.guid], currentIndex[this.guid] + this.props.stackDepth).reverse(); + // Get the next stack of cards to render. + let cards = this.state.cards.slice(currentIndex[this.guid], currentIndex[this.guid] + this.props.stackDepth).reverse() return cards.map((card, i) => { + let offsetX = (this.props.stackOffsetX * cards.length - i * this.props.stackOffsetX) + this.props.stackX + let lastOffsetX = (offsetX + this.props.stackOffsetX) - let offsetX = this.props.stackOffsetX * cards.length - i * this.props.stackOffsetX; - let lastOffsetX = offsetX + this.props.stackOffsetX; - - let offsetY = this.props.stackOffsetY * cards.length - i * this.props.stackOffsetY; - let lastOffsetY = offsetY + this.props.stackOffsetY; + let offsetY = (this.props.stackOffsetY * cards.length - i * this.props.stackOffsetY) + this.props.stackY + let lastOffsetY = (offsetY + this.props.stackOffsetY) - let opacity = 0.25 + (0.75 / cards.length) * (i + 1); - let lastOpacity = 0.25 + (0.75 / cards.length) * i; + let opacity = 0.25 + (0.75 / cards.length) * (i + 1) + let lastOpacity = 0.25 + (0.75 / cards.length) * i - let scale = 0.85 + (0.15 / cards.length) * (i + 1); - let lastScale = 0.85 + (0.15 / cards.length) * i; + let scale = 0.85 + (0.15 / cards.length) * (i + 1) + let lastScale = 0.85 + (0.15 / cards.length) * i let style = { position: 'absolute', - top: this.state.enter.interpolate({ inputRange: [0, 1], outputRange: [lastOffsetY, offsetY] }), - left: this.state.enter.interpolate({ inputRange: [0, 1], outputRange: [lastOffsetX, offsetX] }), - opacity: this.props.smoothTransition ? 1 : this.state.enter.interpolate({ inputRange: [0, 1], outputRange: [lastOpacity, opacity] }), - transform: [{ scale: this.state.enter.interpolate({ inputRange: [0, 1], outputRange: [lastScale, scale] }) }], + top: this.state.enter.interpolate({inputRange: [0, 1], outputRange: [lastOffsetY, offsetY]}), + left: this.state.enter.interpolate({inputRange: [0, 1], outputRange: [lastOffsetX, offsetX]}), + + transform: [{ + scale: this.state.enter.interpolate({ + inputRange: [0, 1], + outputRange: [lastScale, scale] + }) + }], elevation: i * 10 - }; + } - //Is this the top card? If so animate it and hook up the pan handlers. + // Is this the top card? If so animate it and hook up the pan handlers. if (i + 1 === cards.length) { - let {pan} = this.state; - let [translateX, translateY] = [pan.x, pan.y]; + let {pan} = this.state + let [translateX, translateY] = [pan.x, pan.y] + + let rotate = pan.x.interpolate({inputRange: [-200, 0, 200], outputRange: ['-30deg', '0deg', '30deg']}) +// let opacity = pan.x.interpolate({inputRange: [-200, 0, 200], outputRange: [0.5, 1, 0.5]}); - let rotate = pan.x.interpolate({ inputRange: [-200, 0, 200], outputRange: ["-30deg", "0deg", "30deg"] }); - let opacity = this.props.smoothTransition ? 1 : pan.x.interpolate({ inputRange: [-200, 0, 200], outputRange: [0.5, 1, 0.5] }); + const transform = [ + {translateX: translateX}, + {translateY: translateY}, + {rotate: rotate}, + {scale: this.state.enter.interpolate({inputRange: [0, 1], outputRange: [lastScale, scale]})} + ] let animatedCardStyles = { ...style, - transform: [ - { translateX: translateX }, - { translateY: translateY }, - { rotate: rotate }, - { scale: this.state.enter.interpolate({ inputRange: [0, 1], outputRange: [lastScale, scale] }) } - ] - }; - - return - {this.props.renderCard(this.state.card)} - ; - } - - return {this.props.renderCard(card)}; - }); - } - - renderCard() { - if (!this.state.card) { - return this.renderNoMoreCards(); - } + transform: transform + } - let {pan, enter} = this.state; - let [translateX, translateY] = [pan.x, pan.y]; + const yupNopContent = ( + + + {this.renderYup()} + + + {this.renderNope()} + + + ) - let rotate = pan.x.interpolate({ inputRange: [-200, 0, 200], outputRange: ["-30deg", "0deg", "30deg"] }); - let opacity = pan.x.interpolate({ inputRange: [-200, 0, 200], outputRange: [0.5, 1, 0.5] }); + return - let scale = enter; + {this.cardContainer(this.state.card, self, true)} - let animatedCardStyles = { transform: [{ translateX }, { translateY }, { rotate }, { scale }], opacity }; + + } - return - {this.props.renderCard(this.state.card)} - ; + return + {this.cardContainer(card, self, false)} + + }) } - renderNope() { - let {pan} = this.state; + cardContainer (card, self, isTop) { + let yupNopContent = ( + + + {self.renderYup()} + + + {self.renderNope()} + + + ) - let nopeOpacity = pan.x.interpolate({ inputRange: [-SWIPE_THRESHOLD, -(SWIPE_THRESHOLD/2)], outputRange: [1, 0], extrapolate: 'clamp' }); - let nopeScale = pan.x.interpolate({ inputRange: [-SWIPE_THRESHOLD, 0], outputRange: [1, 0], extrapolate: 'clamp' }); - let animatedNopeStyles = { transform: [{ scale: nopeScale }], opacity: nopeOpacity }; + if (!isTop) { + yupNopContent = + }; - if (this.props.renderNope) { - return this.props.renderNope(pan); + if (Platform.OS === 'ios') { + return ( + { + self.props.onClickHandler(self.state.card) + console.log('didPress') + } + }> + + {self.props.renderCard(card, yupNopContent)} + + + ) + } else { + return self.props.renderCard(card, yupNopContent) } + } - if (this.props.showNope) { - - const inner = this.props.noView - ? this.props.noView - : {this.props.nopeText} - - return - {inner} - ; + renderCard () { + if (!this.state.card) { + return this.renderNoMoreCards() } - return null; - } + let {pan, enter} = this.state + let [translateX, translateY] = [pan.x, pan.y] - renderMaybe() { - if (!this.props.hasMaybeAction) return null; + let rotate = pan.x.interpolate({inputRange: [-200, 0, 200], outputRange: ['-30deg', '0deg', '30deg']}) + let opacity = pan.x.interpolate({inputRange: [-200, 0, 200], outputRange: [0.5, 1, 0.5]}) - let {pan} = this.state; + let scale = enter - let maybeOpacity = pan.y.interpolate({ inputRange: [-SWIPE_THRESHOLD, -(SWIPE_THRESHOLD/2)], outputRange: [1, 0], extrapolate: 'clamp' }); - let maybeScale = pan.x.interpolate({ inputRange: [-SWIPE_THRESHOLD, 0, SWIPE_THRESHOLD], outputRange: [0, 1, 0], extrapolate: 'clamp' }); - let animatedMaybeStyles = { transform: [{ scale: maybeScale }], opacity: maybeOpacity }; + let animatedCardStyles = {transform: [{translateX}, {translateY}, {rotate}, {scale}], opacity} - if (this.props.renderMaybe) { - return this.props.renderMaybe(pan); - } + return + {this.props.renderCard(this.state.card)} + + } + renderNope () { + let {pan} = this.state - if (this.props.showMaybe) { + let nopeOpacity = pan.x.interpolate({inputRange: [-30, 0], outputRange: [1, 0]}) + let nopeScale = pan.x.interpolate({inputRange: [-150, 0], outputRange: [1, 0.5], extrapolate: 'clamp'}) + let animatedNopeStyles = {transform: [{scale: nopeScale}], opacity: nopeOpacity} - const inner = this.props.maybeView - ? this.props.maybeView - : {this.props.maybeText} + if (this.props.renderNope) { + return this.props.renderNope(pan) + } - return - {inner} - ; + if (this.props.showNope) { + return + {this.props.nopeText} + } - return null; + return null } - renderYup() { - let {pan} = this.state; + renderYup () { + let {pan} = this.state - let yupOpacity = pan.x.interpolate({ inputRange: [(SWIPE_THRESHOLD/2), SWIPE_THRESHOLD], outputRange: [0, 1], extrapolate: 'clamp' }); - let yupScale = pan.x.interpolate({ inputRange: [0, SWIPE_THRESHOLD], outputRange: [0.5, 1], extrapolate: 'clamp' }); - let animatedYupStyles = { transform: [{ scale: yupScale }], opacity: yupOpacity }; + let yupOpacity = pan.x.interpolate({inputRange: [0, 30], outputRange: [0, 1]}) + let yupScale = pan.x.interpolate({inputRange: [0, 150], outputRange: [0.5, 1], extrapolate: 'clamp'}) + let animatedYupStyles = {transform: [{scale: yupScale}], opacity: yupOpacity} if (this.props.renderYup) { - return this.props.renderYup(pan); + return this.props.renderYup(pan) } if (this.props.showYup) { - - const inner = this.props.yupView - ? this.props.yupView - : {this.props.yupText}; - - return - {inner} - ; + return + {this.props.yupText} + } - return null; + return null } - render() { + render () { return ( - + {this.props.stack ? this.renderStack() : this.renderCard()} - {this.renderNope()} - {this.renderMaybe()} - {this.renderYup()} - ); + ) } }