From d571490b442028a59a018c44b69a6d2eb274246f Mon Sep 17 00:00:00 2001 From: Tushar Korde Date: Tue, 9 Nov 2021 20:10:47 +0530 Subject: [PATCH 1/3] added isScrollable & Ability to scoll inside Card --- Card.js | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/Card.js b/Card.js index 4d89c12..7da73dc 100644 --- a/Card.js +++ b/Card.js @@ -1,30 +1,40 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types' import { - View, + ScrollView, View } from 'react-native'; -const Card = ({ style, children }) => ( - - {children} - ); +const Card = ({ style, children, isScrollable, ...props }) => { + if (isScrollable) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +} Card.propTypes = { - children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, - style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]), - onSwipedLeft: PropTypes.func, - onSwipedRight:PropTypes.func, - onSwipedTop: PropTypes.func, - onSwipedBottom: PropTypes.func, - onSwiped: PropTypes.func, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, + style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]), + onSwipedLeft: PropTypes.func, + onSwipedRight: PropTypes.func, + onSwipedTop: PropTypes.func, + onSwipedBottom: PropTypes.func, + onSwiped: PropTypes.func, } Card.defaultProps = { - style:{}, - onSwiped: () => {}, - onSwipedLeft: () => {}, - onSwipedRight: () => {}, - onSwipedTop: () => {}, - onSwipedBottom: () => {}, + style: {}, + onSwiped: () => { }, + onSwipedLeft: () => { }, + onSwipedRight: () => { }, + onSwipedTop: () => { }, + onSwipedBottom: () => { }, } export default Card; From 6d1a45f29a09b45fb9e6f6afa5374d2eac4edfcd Mon Sep 17 00:00:00 2001 From: Tushar Korde Date: Wed, 10 Nov 2021 14:11:10 +0530 Subject: [PATCH 2/3] Changes for android --- Card.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Card.js b/Card.js index 7da73dc..7db0e4d 100644 --- a/Card.js +++ b/Card.js @@ -7,9 +7,11 @@ import { const Card = ({ style, children, isScrollable, ...props }) => { if (isScrollable) { return ( - - {children} - + + + {children} + + ); } return ( From 52ab31ec0a50451842d2e9b3ac5c5884156bf8df Mon Sep 17 00:00:00 2001 From: Tushar Korde Date: Wed, 24 Nov 2021 15:28:31 +0530 Subject: [PATCH 3/3] onPanGestureTerminate handled stuck card --- CardStack.js | 1067 ++++++++++++++++++++++++++------------------------ 1 file changed, 558 insertions(+), 509 deletions(-) diff --git a/CardStack.js b/CardStack.js index b83f65c..e4923f7 100644 --- a/CardStack.js +++ b/CardStack.js @@ -2,529 +2,578 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types' import { polyfill } from 'react-lifecycles-compat'; import { - View, - Animated, - PanResponder, - Dimensions, - Text, - Platform + View, + Animated, + PanResponder, + Dimensions, + Text, + Platform } from 'react-native'; const { height, width } = Dimensions.get('window'); class CardStack extends Component { - static distance(x, y) { - return Math.hypot(x, y); - } - - constructor(props) { - super(props); - this.state = { - drag: new Animated.ValueXY({ x: 0, y: 0 }), - dragDistance: new Animated.Value(0), - sindex: 0, // index to the next card to be renderd mod card.length - cardA: null, - cardB: null, - topCard: 'cardA', - cards: [], - touchStart: 0, - }; - this.distance = this.constructor.distance; - this._panResponder = PanResponder.create({ - onStartShouldSetPanResponder: (evt, gestureState) => false, - onStartShouldSetPanResponderCapture: (evt, gestureState) => false, - onMoveShouldSetPanResponder: (evt, gestureState) => { - const isVerticalSwipe = Math.sqrt( - Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2) - ) - if (!this.props.verticalSwipe && isVerticalSwipe) { - return false - } - return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10 - }, - onMoveShouldSetPanResponderCapture: (evt, gestureState) => { - const isVerticalSwipe = Math.sqrt( - Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2) - ) - if (!this.props.verticalSwipe && isVerticalSwipe) { - return false - } - return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10 - }, - onPanResponderGrant: (evt, gestureState) => { - this.props.onSwipeStart(); - this.setState({ touchStart: new Date().getTime() }); - }, - onPanResponderMove: (evt, gestureState) => { - const movedX = gestureState.moveX - gestureState.x0; - const movedY = gestureState.moveY - gestureState.y0; - this.props.onSwipe(movedX, movedY); - const { verticalSwipe, horizontalSwipe } = this.props; - const dragDistance = this.distance((horizontalSwipe) ? gestureState.dx : 0, (verticalSwipe) ? gestureState.dy : 0); - this.state.dragDistance.setValue(dragDistance); - this.state.drag.setValue({ x: (horizontalSwipe) ? gestureState.dx : 0, y: (verticalSwipe) ? gestureState.dy : 0 }); - }, - onPanResponderTerminationRequest: (evt, gestureState) => true, - onPanResponderRelease: (evt, gestureState) => { - this.props.onSwipeEnd(); - const currentTime = new Date().getTime(); - const swipeDuration = currentTime - this.state.touchStart; - const { - verticalThreshold, - horizontalThreshold, - disableTopSwipe, - disableLeftSwipe, - disableRightSwipe, - disableBottomSwipe, - } = this.props; - - if (((Math.abs(gestureState.dx) > horizontalThreshold) || - (Math.abs(gestureState.dx) > horizontalThreshold * 0.6 && - swipeDuration < 150) - ) && this.props.horizontalSwipe) { - - const swipeDirection = (gestureState.dx < 0) ? width * -1.5 : width * 1.5; - if (swipeDirection < 0 && !disableLeftSwipe) { - this._nextCard('left', swipeDirection, gestureState.dy, this.props.duration); - } - else if (swipeDirection > 0 && !disableRightSwipe) { - this._nextCard('right', swipeDirection, gestureState.dy, this.props.duration); - } - else { - this._resetCard(); - } - } else if (((Math.abs(gestureState.dy) > verticalThreshold) || - (Math.abs(gestureState.dy) > verticalThreshold * 0.8 && - swipeDuration < 150) - ) && this.props.verticalSwipe) { - - const swipeDirection = (gestureState.dy < 0) ? height * -1 : height; - if (swipeDirection < 0 && !disableTopSwipe) { - - this._nextCard('top', gestureState.dx, swipeDirection, this.props.duration); - } - else if (swipeDirection > 0 && !disableBottomSwipe) { - this._nextCard('bottom', gestureState.dx, swipeDirection, this.props.duration); - } - else { - this._resetCard(); - } - } - else { - this._resetCard(); - } - }, - onPanResponderTerminate: (evt, gestureState) => { - }, - onShouldBlockNativeResponder: (evt, gestureState) => { - return true; - }, - }); - } - - componentDidUpdate(prevProps) { - if (typeof this.props.children === 'undefined') return; - if (!this._isSameChildren(this.props.children, prevProps.children)) { - const children = Array.isArray(this.props.children) ? this.props.children : [this.props.children]; - let aIndex = (this.state.topCard == 'cardA') ? - this._getIndex(this.state.sindex - 2, children.length) : - this._getIndex(this.state.sindex - 1, children.length); - let bIndex = (this.state.topCard == 'cardB') ? - this._getIndex(this.state.sindex - 2, children.length) : - this._getIndex(this.state.sindex - 1, children.length); - this.setState({ - cards: children, - cardA: children[aIndex] || null, - cardB: children[bIndex] || null - }); - } - } - - _getIndex(index, cards){ - return this.props.loop ? - this.mod(index, cards): - index; - } - - componentDidMount() { - this.initDeck(); - } - - _isSameChildren(a, b) { - if (typeof a != typeof b) return false; - if (typeof a === 'undefined') return false; - if (Array.isArray(a) && Array.isArray(b)) { - if (a.length != b.length) return false; - for (let i in a) { - if (a[i].key != b[i].key) { return false } - } - return true; - } - if (a.key !== b.key) return false; - - return true - } - - initDeck() { - if (typeof this.props.children === 'undefined') return; - const { children, loop } = this.props; - const cards = Array.isArray(children) ? children : [children]; - const initialIndexA = this.props.initialIndex < cards.length ? this.props.initialIndex : 0; - const initialIndexB = loop ? this.mod(initialIndexA + 1, cards.length) : initialIndexA + 1; - const cardA = cards[initialIndexA] || null; - const cardB = cards[initialIndexB] || null; - this.setState({ - cards, - cardA, - cardB, - sindex: initialIndexB + 1, - }); - } - - _resetCard() { - Animated.timing( - this.state.dragDistance, - { - toValue: 0, - duration: this.props.duration, - useNativeDriver: this.props.useNativeDriver || false, - } - ).start(); - Animated.spring( - this.state.drag, - { - toValue: { x: 0, y: 0 }, - duration: this.props.duration, - useNativeDriver: this.props.useNativeDriver || false, - } - ).start(); - } - - goBackFromTop() { - this._goBack('top'); - } - - goBackFromRight() { - this._goBack('right'); - } - - goBackFromLeft() { - this._goBack('left'); - } - - goBackFromBottom() { - this._goBack('bottom'); - } - - mod(n, m) { - return ((n % m) + m) % m; - } - - _goBack(direction) { - const { cards, sindex, topCard } = this.state; - - if ((sindex - 3) < 0 && !this.props.loop) return; - - const previusCardIndex = this.mod(sindex - 3, cards.length) - let update = {}; - if (topCard === 'cardA') { - update = { - ...update, - cardB: cards[previusCardIndex] - - } - } else { - update = { - ...update, - cardA: cards[previusCardIndex], - } - } - - this.setState({ - ...update, - topCard: (topCard === 'cardA') ? 'cardB' : 'cardA', - sindex: sindex - 1 - }, () => { - - switch (direction) { - case 'top': - this.state.drag.setValue({ x: 0, y: -height }); - this.state.dragDistance.setValue(height); - break; - case 'left': - this.state.drag.setValue({ x: -width, y: 0 }); - this.state.dragDistance.setValue(width); - break; - case 'right': - this.state.drag.setValue({ x: width, y: 0 }); - this.state.dragDistance.setValue(width); - break; - case 'bottom': - this.state.drag.setValue({ x: 0, y: height }); - this.state.dragDistance.setValue(width); - break; - default: - - } - - Animated.spring( - this.state.dragDistance, - { - toValue: 0, - duration: this.props.duration, - useNativeDriver: this.props.useNativeDriver || false, - } - ).start(); - - Animated.spring( - this.state.drag, - { - toValue: { x: 0, y: 0 }, - duration: this.props.duration, - useNativeDriver: this.props.useNativeDriver || false, - } - ).start(); - }) - } - - swipeTop(d = null) { - this._nextCard('top', 0, -height, d || this.props.duration); - } - - swipeBottom(d = null) { - this._nextCard('bottom', 0, height, d || this.props.duration); - } - - swipeRight(d = null) { - this._nextCard('right', width * 1.5, 0, d || this.props.duration); - } - - swipeLeft(d = null) { - this._nextCard('left', -width * 1.5, 0, d || this.props.duration); - } - - _nextCard(direction, x, y, duration = 400) { - const { verticalSwipe, horizontalSwipe, loop } = this.props; - const { sindex, cards, topCard } = this.state; - - // index for the next card to be renderd - const nextCard = (loop) ? (Math.abs(sindex) % cards.length) : sindex; - - // index of the swiped card - const index = (loop) ? this.mod(nextCard - 2, cards.length) : nextCard - 2; - - if (index === cards.length - 1) { - this.props.onSwipedAll(); - } - - if ((sindex - 2 < cards.length) || (loop)) { - Animated.spring( - this.state.dragDistance, - { - toValue: 220, - duration, - useNativeDriver: this.props.useNativeDriver || false, - } - ).start(); - - Animated.timing( - this.state.drag, - { - toValue: { x: (horizontalSwipe) ? x : 0, y: (verticalSwipe) ? y : 0 }, - duration, - useNativeDriver: this.props.useNativeDriver || false, - } - ).start(() => { - - const newTopCard = (topCard === 'cardA') ? 'cardB' : 'cardA'; - - let update = {}; - if (newTopCard === 'cardA') { - update = { - ...update, - cardB: cards[nextCard] - }; - } - if (newTopCard === 'cardB') { - update = { - ...update, - cardA: cards[nextCard], - }; - } - this.state.drag.setValue({ x: 0, y: 0 }); - this.state.dragDistance.setValue(0); - this.setState({ - ...update, - topCard: newTopCard, - sindex: nextCard + 1 - }); - - this.props.onSwiped(index); - switch (direction) { - case 'left': - this.props.onSwipedLeft(index); - if (this.state.cards[index] && this.state.cards[index].props.onSwipedLeft) - this.state.cards[index] && this.state.cards[index].props.onSwipedLeft(); - break; - case 'right': - this.props.onSwipedRight(index); - if (this.state.cards[index] && this.state.cards[index].props.onSwipedRight) - this.state.cards[index].props.onSwipedRight(); - break; - case 'top': - this.props.onSwipedTop(index); - if (this.state.cards[index] && this.state.cards[index].props.onSwipedTop) - this.state.cards[index].props.onSwipedTop(); - break; - case 'bottom': - this.props.onSwipedBottom(index); - if (this.state.cards[index] && this.state.cards[index].props.onSwipedBottom) - this.state.cards[index].props.onSwipedBottom(); - break; - default: - } - }); - - } - } - - - /** - * @description CardB’s click feature is trigger the CardA on the card stack. (Solved on Android) - * @see https://facebook.github.io/react-native/docs/view#pointerevents - */ - _setPointerEvents(topCard, topCardName) { - return { pointerEvents: topCard === topCardName ? "auto" : "none" } - } - - render() { - - const { secondCardZoom, renderNoMoreCards } = this.props; - const { drag, dragDistance, cardA, cardB, topCard, sindex } = this.state; - - const scale = dragDistance.interpolate({ - inputRange: [0, 10, 220], - outputRange: [secondCardZoom, secondCardZoom, 1], - extrapolate: 'clamp', - }); - const rotate = drag.x.interpolate({ - inputRange: [width * -1.5, 0, width * 1.5], - outputRange: this.props.outputRotationRange, - extrapolate: 'clamp', - }); - - return ( - - - {renderNoMoreCards()} - - - {cardB} - - - {cardA} - - - - ); - } + static distance(x, y) { + return Math.hypot(x, y); + } + + constructor(props) { + super(props); + this.state = { + drag: new Animated.ValueXY({ x: 0, y: 0 }), + dragDistance: new Animated.Value(0), + sindex: 0, // index to the next card to be renderd mod card.length + cardA: null, + cardB: null, + topCard: 'cardA', + cards: [], + touchStart: 0, + }; + this.distance = this.constructor.distance; + this._panResponder = PanResponder.create({ + onStartShouldSetPanResponder: (evt, gestureState) => false, + onStartShouldSetPanResponderCapture: (evt, gestureState) => false, + onMoveShouldSetPanResponder: (evt, gestureState) => { + const isVerticalSwipe = Math.sqrt( + Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2) + ) + if (!this.props.verticalSwipe && isVerticalSwipe) { + return false + } + return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10 + }, + onMoveShouldSetPanResponderCapture: (evt, gestureState) => { + const isVerticalSwipe = Math.sqrt( + Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2) + ) + if (!this.props.verticalSwipe && isVerticalSwipe) { + return false + } + return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10 + }, + onPanResponderGrant: (evt, gestureState) => { + this.props.onSwipeStart(); + this.setState({ touchStart: new Date().getTime() }); + }, + onPanResponderMove: (evt, gestureState) => { + const movedX = gestureState.moveX - gestureState.x0; + const movedY = gestureState.moveY - gestureState.y0; + this.props.onSwipe(movedX, movedY); + const { verticalSwipe, horizontalSwipe } = this.props; + const dragDistance = this.distance((horizontalSwipe) ? gestureState.dx : 0, (verticalSwipe) ? gestureState.dy : 0); + this.state.dragDistance.setValue(dragDistance); + this.state.drag.setValue({ x: (horizontalSwipe) ? gestureState.dx : 0, y: (verticalSwipe) ? gestureState.dy : 0 }); + }, + onPanResponderTerminationRequest: (evt, gestureState) => { + return false; + }, + onPanResponderRelease: (evt, gestureState) => { + this.props.onSwipeEnd(); + const currentTime = new Date().getTime(); + const swipeDuration = currentTime - this.state.touchStart; + const { + verticalThreshold, + horizontalThreshold, + disableTopSwipe, + disableLeftSwipe, + disableRightSwipe, + disableBottomSwipe, + } = this.props; + + if (((Math.abs(gestureState.dx) > horizontalThreshold) || + (Math.abs(gestureState.dx) > horizontalThreshold * 0.6 && + swipeDuration < 150) + ) && this.props.horizontalSwipe) { + + const swipeDirection = (gestureState.dx < 0) ? width * -1.5 : width * 1.5; + if (swipeDirection < 0 && !disableLeftSwipe) { + this._nextCard('left', swipeDirection, gestureState.dy, this.props.duration); + } + else if (swipeDirection > 0 && !disableRightSwipe) { + this._nextCard('right', swipeDirection, gestureState.dy, this.props.duration); + } + else { + this._resetCard(); + } + } else if (((Math.abs(gestureState.dy) > verticalThreshold) || + (Math.abs(gestureState.dy) > verticalThreshold * 0.8 && + swipeDuration < 150) + ) && this.props.verticalSwipe) { + + const swipeDirection = (gestureState.dy < 0) ? height * -1 : height; + if (swipeDirection < 0 && !disableTopSwipe) { + + this._nextCard('top', gestureState.dx, swipeDirection, this.props.duration); + } + else if (swipeDirection > 0 && !disableBottomSwipe) { + this._nextCard('bottom', gestureState.dx, swipeDirection, this.props.duration); + } + else { + this._resetCard(); + } + } + else { + this._resetCard(); + } + }, + onPanResponderTerminate: (evt, gestureState) => { + this.props.onSwipeEnd(); + const currentTime = new Date().getTime(); + const swipeDuration = currentTime - this.state.touchStart; + const { + verticalThreshold, + horizontalThreshold, + disableTopSwipe, + disableLeftSwipe, + disableRightSwipe, + disableBottomSwipe, + } = this.props; + + if (((Math.abs(gestureState.dx) > horizontalThreshold) || + (Math.abs(gestureState.dx) > horizontalThreshold * 0.6 && + swipeDuration < 150) + ) && this.props.horizontalSwipe) { + + const swipeDirection = (gestureState.dx < 0) ? width * -1.5 : width * 1.5; + if (swipeDirection < 0 && !disableLeftSwipe) { + this._nextCard('left', swipeDirection, gestureState.dy, this.props.duration); + } + else if (swipeDirection > 0 && !disableRightSwipe) { + this._nextCard('right', swipeDirection, gestureState.dy, this.props.duration); + } + else { + this._resetCard(); + } + } else if (((Math.abs(gestureState.dy) > verticalThreshold) || + (Math.abs(gestureState.dy) > verticalThreshold * 0.8 && + swipeDuration < 150) + ) && this.props.verticalSwipe) { + + const swipeDirection = (gestureState.dy < 0) ? height * -1 : height; + if (swipeDirection < 0 && !disableTopSwipe) { + + this._nextCard('top', gestureState.dx, swipeDirection, this.props.duration); + } + else if (swipeDirection > 0 && !disableBottomSwipe) { + this._nextCard('bottom', gestureState.dx, swipeDirection, this.props.duration); + } + else { + this._resetCard(); + } + } + else { + this._resetCard(); + } + }, + onShouldBlockNativeResponder: (evt, gestureState) => { + return true; + }, + }); + } + + componentDidUpdate(prevProps) { + if (typeof this.props.children === 'undefined') return; + if (!this._isSameChildren(this.props.children, prevProps.children)) { + const children = Array.isArray(this.props.children) ? this.props.children : [this.props.children]; + let aIndex = (this.state.topCard == 'cardA') ? + this._getIndex(this.state.sindex - 2, children.length) : + this._getIndex(this.state.sindex - 1, children.length); + let bIndex = (this.state.topCard == 'cardB') ? + this._getIndex(this.state.sindex - 2, children.length) : + this._getIndex(this.state.sindex - 1, children.length); + this.setState({ + cards: children, + cardA: children[aIndex] || null, + cardB: children[bIndex] || null + }); + } + } + + _getIndex(index, cards) { + return this.props.loop ? + this.mod(index, cards) : + index; + } + + componentDidMount() { + this.initDeck(); + } + + _isSameChildren(a, b) { + if (typeof a != typeof b) return false; + if (typeof a === 'undefined') return false; + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length != b.length) return false; + for (let i in a) { + if (a[i].key != b[i].key) { return false } + } + return true; + } + if (a.key !== b.key) return false; + + return true + } + + initDeck() { + if (typeof this.props.children === 'undefined') return; + const { children, loop } = this.props; + const cards = Array.isArray(children) ? children : [children]; + const initialIndexA = this.props.initialIndex < cards.length ? this.props.initialIndex : 0; + const initialIndexB = loop ? this.mod(initialIndexA + 1, cards.length) : initialIndexA + 1; + const cardA = cards[initialIndexA] || null; + const cardB = cards[initialIndexB] || null; + this.setState({ + cards, + cardA, + cardB, + sindex: initialIndexB + 1, + }); + } + + _resetCard() { + Animated.timing( + this.state.dragDistance, + { + toValue: 0, + duration: this.props.duration, + useNativeDriver: this.props.useNativeDriver || false, + } + ).start(); + Animated.spring( + this.state.drag, + { + toValue: { x: 0, y: 0 }, + duration: this.props.duration, + useNativeDriver: this.props.useNativeDriver || false, + } + ).start(); + } + + goBackFromTop() { + this._goBack('top'); + } + + goBackFromRight() { + this._goBack('right'); + } + + goBackFromLeft() { + this._goBack('left'); + } + + goBackFromBottom() { + this._goBack('bottom'); + } + + mod(n, m) { + return ((n % m) + m) % m; + } + + _goBack(direction) { + const { cards, sindex, topCard } = this.state; + + if ((sindex - 3) < 0 && !this.props.loop) return; + + const previusCardIndex = this.mod(sindex - 3, cards.length) + let update = {}; + if (topCard === 'cardA') { + update = { + ...update, + cardB: cards[previusCardIndex] + + } + } else { + update = { + ...update, + cardA: cards[previusCardIndex], + } + } + + this.setState({ + ...update, + topCard: (topCard === 'cardA') ? 'cardB' : 'cardA', + sindex: sindex - 1 + }, () => { + + switch (direction) { + case 'top': + this.state.drag.setValue({ x: 0, y: -height }); + this.state.dragDistance.setValue(height); + break; + case 'left': + this.state.drag.setValue({ x: -width, y: 0 }); + this.state.dragDistance.setValue(width); + break; + case 'right': + this.state.drag.setValue({ x: width, y: 0 }); + this.state.dragDistance.setValue(width); + break; + case 'bottom': + this.state.drag.setValue({ x: 0, y: height }); + this.state.dragDistance.setValue(width); + break; + default: + + } + + Animated.spring( + this.state.dragDistance, + { + toValue: 0, + duration: this.props.duration, + useNativeDriver: this.props.useNativeDriver || false, + } + ).start(); + + Animated.spring( + this.state.drag, + { + toValue: { x: 0, y: 0 }, + duration: this.props.duration, + useNativeDriver: this.props.useNativeDriver || false, + } + ).start(); + }) + } + + swipeTop(d = null) { + this._nextCard('top', 0, -height, d || this.props.duration); + } + + swipeBottom(d = null) { + this._nextCard('bottom', 0, height, d || this.props.duration); + } + + swipeRight(d = null) { + this._nextCard('right', width * 1.5, 0, d || this.props.duration); + } + + swipeLeft(d = null) { + this._nextCard('left', -width * 1.5, 0, d || this.props.duration); + } + + _nextCard(direction, x, y, duration = 400) { + const { verticalSwipe, horizontalSwipe, loop } = this.props; + const { sindex, cards, topCard } = this.state; + + // index for the next card to be renderd + const nextCard = (loop) ? (Math.abs(sindex) % cards.length) : sindex; + + // index of the swiped card + const index = (loop) ? this.mod(nextCard - 2, cards.length) : nextCard - 2; + + if (index === cards.length - 1) { + this.props.onSwipedAll(); + } + + if ((sindex - 2 < cards.length) || (loop)) { + Animated.spring( + this.state.dragDistance, + { + toValue: 220, + duration, + useNativeDriver: this.props.useNativeDriver || false, + } + ).start(); + + Animated.timing( + this.state.drag, + { + toValue: { x: (horizontalSwipe) ? x : 0, y: (verticalSwipe) ? y : 0 }, + duration, + useNativeDriver: this.props.useNativeDriver || false, + } + ).start(() => { + + const newTopCard = (topCard === 'cardA') ? 'cardB' : 'cardA'; + + let update = {}; + if (newTopCard === 'cardA') { + update = { + ...update, + cardB: cards[nextCard] + }; + } + if (newTopCard === 'cardB') { + update = { + ...update, + cardA: cards[nextCard], + }; + } + this.state.drag.setValue({ x: 0, y: 0 }); + this.state.dragDistance.setValue(0); + this.setState({ + ...update, + topCard: newTopCard, + sindex: nextCard + 1 + }); + + this.props.onSwiped(index); + switch (direction) { + case 'left': + this.props.onSwipedLeft(index); + if (this.state.cards[index] && this.state.cards[index].props.onSwipedLeft) + this.state.cards[index] && this.state.cards[index].props.onSwipedLeft(); + break; + case 'right': + this.props.onSwipedRight(index); + if (this.state.cards[index] && this.state.cards[index].props.onSwipedRight) + this.state.cards[index].props.onSwipedRight(); + break; + case 'top': + this.props.onSwipedTop(index); + if (this.state.cards[index] && this.state.cards[index].props.onSwipedTop) + this.state.cards[index].props.onSwipedTop(); + break; + case 'bottom': + this.props.onSwipedBottom(index); + if (this.state.cards[index] && this.state.cards[index].props.onSwipedBottom) + this.state.cards[index].props.onSwipedBottom(); + break; + default: + } + }); + + } + } + + + /** + * @description CardB’s click feature is trigger the CardA on the card stack. (Solved on Android) + * @see https://facebook.github.io/react-native/docs/view#pointerevents + */ + _setPointerEvents(topCard, topCardName) { + return { pointerEvents: topCard === topCardName ? "auto" : "none" } + } + + render() { + + const { secondCardZoom, renderNoMoreCards } = this.props; + const { drag, dragDistance, cardA, cardB, topCard, sindex } = this.state; + + const scale = dragDistance.interpolate({ + inputRange: [0, 10, 220], + outputRange: [secondCardZoom, secondCardZoom, 1], + extrapolate: 'clamp', + }); + const rotate = drag.x.interpolate({ + inputRange: [width * -1.5, 0, width * 1.5], + outputRange: this.props.outputRotationRange, + extrapolate: 'clamp', + }); + + return ( + + + {renderNoMoreCards()} + + + {cardB} + + + {cardA} + + + + ); + } } CardStack.propTypes = { - children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, - - style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]), - cardContainerStyle: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]), - secondCardZoom: PropTypes.number, - loop: PropTypes.bool, - initialIndex: PropTypes.number, - renderNoMoreCards: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - onSwipeStart: PropTypes.func, - onSwipeEnd: PropTypes.func, - onSwiped: PropTypes.func, - onSwipedLeft: PropTypes.func, - onSwipedRight: PropTypes.func, - onSwipedTop: PropTypes.func, - onSwipedBottom: PropTypes.func, - onSwiped: PropTypes.func, - onSwipedAll: PropTypes.func, - onSwipe: PropTypes.func, - - disableBottomSwipe: PropTypes.bool, - disableLeftSwipe: PropTypes.bool, - disableRightSwipe: PropTypes.bool, - disableTopSwipe: PropTypes.bool, - verticalSwipe: PropTypes.bool, - verticalThreshold: PropTypes.number, - - horizontalSwipe: PropTypes.bool, - horizontalThreshold: PropTypes.number, - outputRotationRange: PropTypes.array, - duration: PropTypes.number + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, + + style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]), + cardContainerStyle: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]), + secondCardZoom: PropTypes.number, + loop: PropTypes.bool, + initialIndex: PropTypes.number, + renderNoMoreCards: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), + onSwipeStart: PropTypes.func, + onSwipeEnd: PropTypes.func, + onSwiped: PropTypes.func, + onSwipedLeft: PropTypes.func, + onSwipedRight: PropTypes.func, + onSwipedTop: PropTypes.func, + onSwipedBottom: PropTypes.func, + onSwiped: PropTypes.func, + onSwipedAll: PropTypes.func, + onSwipe: PropTypes.func, + + disableBottomSwipe: PropTypes.bool, + disableLeftSwipe: PropTypes.bool, + disableRightSwipe: PropTypes.bool, + disableTopSwipe: PropTypes.bool, + verticalSwipe: PropTypes.bool, + verticalThreshold: PropTypes.number, + + horizontalSwipe: PropTypes.bool, + horizontalThreshold: PropTypes.number, + outputRotationRange: PropTypes.array, + duration: PropTypes.number } CardStack.defaultProps = { - style: {}, - cardContainerStyle: {}, - secondCardZoom: 0.95, - loop: false, - initialIndex: 0, - renderNoMoreCards: () => { return (No More Cards) }, - onSwipeStart: () => null, - onSwipeEnd: () => null, - onSwiped: () => { }, - onSwipedLeft: () => { }, - onSwipedRight: () => { }, - onSwipedTop: () => { }, - onSwipedBottom: () => { }, - onSwipedAll: async () => { }, - onSwipe: () => { }, - - disableBottomSwipe: false, - disableLeftSwipe: false, - disableRightSwipe: false, - disableTopSwipe: false, - verticalSwipe: true, - verticalThreshold: height / 4, - horizontalSwipe: true, - horizontalThreshold: width / 2, - outputRotationRange: ['-15deg', '0deg', '15deg'], - duration: 300 + style: {}, + cardContainerStyle: {}, + secondCardZoom: 0.95, + loop: false, + initialIndex: 0, + renderNoMoreCards: () => { return (No More Cards) }, + onSwipeStart: () => null, + onSwipeEnd: () => null, + onSwiped: () => { }, + onSwipedLeft: () => { }, + onSwipedRight: () => { }, + onSwipedTop: () => { }, + onSwipedBottom: () => { }, + onSwipedAll: async () => { }, + onSwipe: () => { }, + + disableBottomSwipe: false, + disableLeftSwipe: false, + disableRightSwipe: false, + disableTopSwipe: false, + verticalSwipe: true, + verticalThreshold: height / 4, + horizontalSwipe: true, + horizontalThreshold: width / 2, + outputRotationRange: ['-15deg', '0deg', '15deg'], + duration: 300 } polyfill(CardStack); export default CardStack;