├── index.js ├── example ├── index.js ├── img │ ├── sad.gif │ ├── wow.gif │ ├── angry.gif │ ├── haha.gif │ ├── like.gif │ └── love.gif ├── LikeApp.js ├── moods.js ├── FeedStyle.js ├── feed.json └── Feed.js ├── .gitignore ├── package.json ├── src ├── MoodStyle.js ├── MoodPopoverStyle.js ├── Mood.js ├── Swiper.js └── MoodPopover.js ├── LICENSE └── README.md /index.js: -------------------------------------------------------------------------------- 1 | import Swiper from './src/Swiper'; 2 | export default Swiper; 3 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import LikeApp from './LikeApp'; 2 | 3 | export default LikeApp; 4 | -------------------------------------------------------------------------------- /example/img/sad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httpdeveloper/react-native-tap-swipe-select/HEAD/example/img/sad.gif -------------------------------------------------------------------------------- /example/img/wow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httpdeveloper/react-native-tap-swipe-select/HEAD/example/img/wow.gif -------------------------------------------------------------------------------- /example/img/angry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httpdeveloper/react-native-tap-swipe-select/HEAD/example/img/angry.gif -------------------------------------------------------------------------------- /example/img/haha.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httpdeveloper/react-native-tap-swipe-select/HEAD/example/img/haha.gif -------------------------------------------------------------------------------- /example/img/like.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httpdeveloper/react-native-tap-swipe-select/HEAD/example/img/like.gif -------------------------------------------------------------------------------- /example/img/love.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httpdeveloper/react-native-tap-swipe-select/HEAD/example/img/love.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | 4 | # Android 5 | .idea 6 | .gradle 7 | 8 | # node.js 9 | npm-debug.log 10 | 11 | #Visual Studio 12 | .history 13 | -------------------------------------------------------------------------------- /example/LikeApp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | import React, { Component } from 'react'; 9 | import { View } from 'react-native'; 10 | import Feed from './Feed'; 11 | 12 | export default class LikeApp extends Component<{}> { 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/moods.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | const moods = [ 9 | { 10 | id: 1, 11 | icon: require('./img/like.gif'), 12 | text: 'Like' 13 | }, 14 | { 15 | id: 2, 16 | icon: require('./img/love.gif'), 17 | text: 'Love' 18 | }, 19 | { 20 | id: 3, 21 | icon: require('./img/haha.gif'), 22 | text: 'Haha' 23 | }, 24 | { 25 | id: 4, 26 | icon: require('./img/wow.gif'), 27 | text: 'Wow' 28 | }, 29 | { 30 | id: 5, 31 | icon: require('./img/sad.gif'), 32 | text: 'Sad' 33 | }, 34 | { 35 | id: 6, 36 | icon: require('./img/angry.gif'), 37 | text: 'Angry' 38 | } 39 | ]; 40 | 41 | export default moods; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-tap-swipe-select", 3 | "version": "0.0.1", 4 | "description": "Tab Swipe Select for React Native", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "keywords": [ 8 | "react-native", 9 | "react-native-tap-swipe-select", 10 | "fblike", 11 | "swipe", 12 | "tap-swipe-select", 13 | "facebook-like-tap-swipe-select" 14 | ], 15 | "author": { 16 | "name": "Dinesh Maharjan", 17 | "email": "httpdeveloper@gmail.com", 18 | "url": "https://github.com/httpdeveloper" 19 | }, 20 | "bugs": { 21 | "url": 22 | "https://github.com/httpdeveloper/react-native-tap-swipe-select/issues" 23 | }, 24 | "homepage": "https://github.com/httpdeveloper/react-native-tap-swipe-select", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/httpdeveloper/react-native-tap-swipe-select" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/MoodStyle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | import { StyleSheet } from 'react-native'; 9 | 10 | const styles = StyleSheet.create({ 11 | androidMoodContainer: { 12 | zIndex: 3, 13 | overflow: 'hidden', 14 | position: 'absolute', 15 | alignItems: 'center' 16 | }, 17 | 18 | scaleStyle: { 19 | alignItems: 'center', 20 | justifyContent: 'center' 21 | }, 22 | moodIcon: { 23 | width: 46, 24 | height: 46, 25 | borderRadius: 23 26 | }, 27 | moodText: { 28 | fontSize: 8, 29 | backgroundColor: '#000', 30 | paddingHorizontal: 2, 31 | paddingVertical: 1, 32 | color: '#fff', 33 | borderRadius: 2, 34 | borderWidth: 0.5, 35 | borderColor: '#000', 36 | opacity: 0.9, 37 | fontWeight: 'bold', 38 | overflow: 'hidden' 39 | } 40 | }); 41 | 42 | export default styles; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dinesh Maharjan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/MoodPopoverStyle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | import { StyleSheet, Dimensions } from 'react-native'; 9 | 10 | const { width, height } = Dimensions.get('window'); 11 | 12 | const styles = StyleSheet.create({ 13 | container: { 14 | flex: 1, 15 | left: 10, 16 | zIndex: 2, 17 | height: 45, 18 | borderRadius: 22, 19 | alignItems: 'center', 20 | position: 'absolute', 21 | flexDirection: 'row', 22 | backgroundColor: '#fff', 23 | justifyContent: 'space-around' 24 | }, 25 | androidContainer: { 26 | left: 10, 27 | zIndex: 2, 28 | overflow: 'visible', 29 | position: 'absolute', 30 | borderRadius: 25 31 | }, 32 | 33 | backgroundView: { 34 | width, 35 | height, 36 | top: 0, 37 | left: 0, 38 | zIndex: 1, 39 | position: 'absolute', 40 | backgroundColor: 'transparent' 41 | }, 42 | 43 | androidMoodWrapper: { 44 | left: 10, 45 | zIndex: 1, 46 | height: 45, 47 | borderRadius: 22, 48 | backgroundColor: '#fff' 49 | } 50 | }); 51 | 52 | export default styles; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-tap-swipe-select 2 | React Native Tap Swipe Select Module - Similar to Facebook like app module that support on both platforms IOS and Android 3 | 4 | ## Usage 5 | ``` 6 | import Swiper from 'react-native-tap-swipe-select'; 7 | 8 | 9 | 10 | 11 | 12 | ``` 13 | ## Demo 14 | ``` 15 | //Edit index.js on the root folder 16 | 17 | import { AppRegistry } from 'react-native'; 18 | import LikeAppExample from 'react-native-tap-swipe-select/example'; 19 | 20 | AppRegistry.registerComponent('YourAppName', () => LikeAppExample); 21 | 22 | ``` 23 | ## Screenshot 24 | ![alt text](https://media.giphy.com/media/l4pT9X4z65bEn5l4I/giphy.gif) 25 | 26 | ## Installation 27 | `npm install react-native-tap-swipe-select@https://github.com/httpdeveloper/react-native-tap-swipe-select.git --save 28 | ` 29 | 30 | ## Props 31 | | Prop | Type | Description | Default | 32 | | --- | --- | --- | --- | 33 | | moods | array | Array of moods (Required) | - | 34 | | initialPosition | object | Initial position of touchable item (Required) | {x, y, width, height} | 35 | | swipeArea | object | Swipeable area. If provided then specific area will be cosidered as swipeable otherwise, It takes whole area (optional) | {x, y, width, height} | 36 | | swipeEnabled | boolean | Enable swipeable area | false | 37 | | onSwipeRelease | function | Callback function after finish the swiping | - | 38 | | onSwipe | function | Callback function during swiping | - | 39 | 40 | # License 41 | MIT 42 | -------------------------------------------------------------------------------- /example/FeedStyle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | import { StyleSheet, Dimensions, Platform } from 'react-native'; 9 | 10 | const { width } = Dimensions.get('window'); 11 | 12 | const styles = StyleSheet.create({ 13 | container: { 14 | flex: 1, 15 | paddingTop: Platform.OS === 'ios' ? 20 : 0, 16 | backgroundColor: '#ddd' 17 | }, 18 | row: { 19 | padding: 10, 20 | marginBottom: 10, 21 | backgroundColor: '#fff' 22 | }, 23 | image: { 24 | height: 150, 25 | width: undefined, 26 | marginVertical: 5 27 | }, 28 | profilePic: { 29 | width: 30, 30 | height: 30, 31 | borderWidth: 1, 32 | marginRight: 10, 33 | borderRadius: 15, 34 | marginBottom: 10, 35 | borderColor: '#ccc', 36 | backgroundColor: '#ddd' 37 | }, 38 | url: { 39 | fontSize: 12, 40 | color: '#888' 41 | }, 42 | time: { 43 | fontSize: 12, 44 | color: '#888' 45 | }, 46 | line: { 47 | height: 1, 48 | width: width - 20, 49 | marginVertical: 10, 50 | backgroundColor: '#eee' 51 | }, 52 | likeOverlay: { 53 | position: 'absolute', 54 | left: 0, 55 | top: -10, 56 | width, 57 | height: 30, 58 | backgroundColor: '#fff', 59 | alignItems: 'center', 60 | justifyContent: 'center' 61 | }, 62 | selectedReactions: { 63 | flexDirection: 'row', 64 | alignItems: 'center', 65 | justifyContent: 'space-around', 66 | paddingVertical: 5 67 | }, 68 | shareLabelWithCounterTxt: { 69 | fontSize: 12 70 | }, 71 | commentLabelWithCounterTxt: { 72 | fontSize: 12 73 | }, 74 | selectedItemReaction: { 75 | width: 20, 76 | height: 20, 77 | borderRadius: 10 78 | }, 79 | reactionTxt: {}, 80 | reactionTxtSelected: { 81 | fontWeight: 'bold', 82 | color: '#4054b2' 83 | } 84 | }); 85 | 86 | export default styles; 87 | -------------------------------------------------------------------------------- /example/feed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "title": "Bloodhound supersonic carries out first public runs", 5 | "desc": 6 | "The Bloodhound car got up to 200mph in about eight seconds. The Bloodhound Super Sonic Car has conducted its first public runs. ... Ultimately, Bloodhound will be fitted with a rocket motor as well so that it can go 1,000mph (1,610km/h)", 7 | "image": "http://placehold.it/350x200/f5f5f5?text=Bloodhound supersonic ", 8 | "link": "bbc.com", 9 | "company": "BBC News", 10 | "time": "1 min", 11 | "reactions": [] 12 | }, 13 | { 14 | "id": 2, 15 | "title": "US Shutdown: Government services closed as working weeks", 16 | "desc": 17 | "Efforts to reach a compromise ahead of the working week failed in a rare Senate session late on Sunday. A vote to end the shutdown was postponed until midday on Monday, meaning many federal government offices will not open as the shutdown enters its third day. Why the US government has shut down", 18 | "image": "http://placehold.it/350x200/f5f5f5?text=US Shutdown", 19 | "link": "gbcghana.com", 20 | "company": "Gbcghana News", 21 | "time": "1 hr", 22 | "reactions": [] 23 | }, 24 | { 25 | "id": 3, 26 | "title": "Coincheck promises 46bn yen refund after cryptocurrency theft", 27 | "desc": 28 | "Coincheck has promised to use its own funds to reimburse more than 46bn yen ($423m) to customers who lost their NEM cryptocurrency coins on Friday.The Tokyo-based company suspended trading after detecting unauthorised access of its digital exchange.Some 260,000 customers are said to be affected by the reported theft.Coincheck said on Sunday that the amount it has promised to return covers nearly 90% of the 58bn yen worth of NEM coins lost in the attack.", 29 | "image": "http://placehold.it/350x200/f5f5f5?text=Cryptocurrency Theft", 30 | "link": "cnn.com", 31 | "company": "CNN News", 32 | "time": "9 hr", 33 | "reactions": [] 34 | }, 35 | { 36 | "id": 4, 37 | "title": "Calls to clean off Banksy mural in Hull", 38 | "desc": 39 | "Hundreds of people have been turning up to see a work by the world-famous graffiti artist Banksy, on a disused bridge in Hull. Banksy has appeared to confirm it is his creation on social media - but not everyone is happy about it.", 40 | "image": "http://placehold.it/350x200/f5f5f5?text=Clean Off", 41 | "link": "abc.com", 42 | "company": "ABC News", 43 | "time": "Yesterday", 44 | "reactions": [] 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /src/Mood.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | import React, { Component } from 'react'; 9 | import { 10 | View, 11 | Text, 12 | Image, 13 | Animated, 14 | Platform, 15 | TouchableWithoutFeedback 16 | } from 'react-native'; 17 | 18 | import PropTypes from 'prop-types'; 19 | import styles from './MoodStyle'; 20 | 21 | const MIN_SCALE = 0.9; 22 | const MAX_SCALE = 1.5; 23 | 24 | const defaultProps = { 25 | id: 0, 26 | icon: {}, 27 | text: '', 28 | selected: false, 29 | onMoodLayout: () => {}, 30 | onSelectMood: () => {}, 31 | style: {} 32 | }; 33 | 34 | class Mood extends Component<{}> { 35 | constructor(props) { 36 | super(props); 37 | 38 | this.state = { 39 | scale: MIN_SCALE, 40 | marginTop: 0, 41 | showText: false 42 | }; 43 | 44 | this._animateMood = new Animated.Value(MIN_SCALE); 45 | this._selectedMood = null; 46 | } 47 | 48 | componentDidUpdate() { 49 | this._moodRef.measure((_, __, width, height, pageX, pageY) => { 50 | const layoutPosition = { 51 | x: pageX, 52 | y: pageY, 53 | width, 54 | height, 55 | id: this.props.id 56 | }; 57 | 58 | this.props.onMoodLayout && this.props.onMoodLayout(layoutPosition); 59 | }); 60 | } 61 | 62 | componentWillReceiveProps(nextProps) { 63 | if (nextProps.selected) { 64 | if (this._selectedMood === nextProps.id) return; 65 | 66 | const marginTop = 67 | Platform.OS === 'ios' 68 | ? nextProps.text ? -45 : -28 69 | : nextProps.text ? -30 : -12; 70 | 71 | this.animate(MIN_SCALE, MAX_SCALE, marginTop, finished => { 72 | if (finished) { 73 | this._animateMood = new Animated.Value(MIN_SCALE); 74 | this._selectedMood = nextProps.id; 75 | } 76 | }); 77 | } else { 78 | if (this._selectedMood) { 79 | this.animate(MAX_SCALE, MIN_SCALE, 0, finished => { 80 | if (finished) { 81 | this._selectedMood = null; 82 | this._animateMood = new Animated.Value(MIN_SCALE); 83 | } 84 | }); 85 | } else { 86 | this._animateMood = new Animated.Value(MIN_SCALE); 87 | 88 | this._selectedMood = null; 89 | this.setState({ 90 | scale: MIN_SCALE, 91 | showText: false, 92 | marginTop: 0 93 | }); 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Scaling starts as soon as mood selects 100 | * 101 | * @param {number} minScale 102 | * @param {number} maxScale 103 | * @param {number} marginTop 104 | * @param {func} callback 105 | * @returns {undefined} 106 | */ 107 | 108 | animate(minScale, maxScale, marginTop, callback) { 109 | this.setState( 110 | { 111 | marginTop, 112 | showText: marginTop !== 0, 113 | scale: this._animateMood.interpolate({ 114 | inputRange: [0, 10], 115 | outputRange: [minScale, maxScale], 116 | extrapolate: 'clamp' 117 | }) 118 | }, 119 | () => { 120 | Animated.spring(this._animateMood, { 121 | toValue: 10, 122 | duration: 50, 123 | speed: 80, 124 | velocity: 8, 125 | bounciness: 0 126 | }).start(({ finished }) => { 127 | if (!finished) return; 128 | callback(finished); 129 | }); 130 | } 131 | ); 132 | } 133 | 134 | render() { 135 | const { icon, perspective, style, text } = this.props; 136 | 137 | const transform = Platform.select({ 138 | ios: [{ scale: this.state.scale }], 139 | android: [ 140 | { scale: this.state.scale }, 141 | { perspective: perspective || 1000 } 142 | ] 143 | }); 144 | 145 | return ( 146 | this.props.onSelectMood(this.props.id)} 148 | hitSlop={{ top: 10, right: 0, bottom: 10, left: 0 }} 149 | > 150 | (this._moodRef = ref)} 153 | style={ 154 | Platform.OS === 'android' 155 | ? [styles.androidMoodContainer, style] 156 | : null 157 | } 158 | > 159 | 168 | {this.state.showText && 169 | text.length > 0 && {text}} 170 | 171 | 172 | 173 | 174 | ); 175 | } 176 | } 177 | 178 | Mood.propTypes = { 179 | id: PropTypes.number.isRequired, 180 | icon: PropTypes.oneOfType([Image.propTypes.source, PropTypes.object]), 181 | text: PropTypes.string, 182 | selected: PropTypes.bool, 183 | onMoodLayout: PropTypes.func, 184 | onSelectMood: PropTypes.func, 185 | style: View.propTypes.style 186 | }; 187 | 188 | Mood.defaultProps = defaultProps; 189 | 190 | export default Mood; 191 | -------------------------------------------------------------------------------- /src/Swiper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | import React, { Component } from 'react'; 9 | import { View, PanResponder, Animated } from 'react-native'; 10 | import PropTypes from 'prop-types'; 11 | 12 | import MoodPopover from './MoodPopover'; 13 | 14 | const defaultProps = { 15 | moods: [], 16 | swipeArea: {}, 17 | initialPosition: {}, 18 | swipeEnabled: false, 19 | onSwipe: () => {}, 20 | onSwipeRelease: () => {} 21 | }; 22 | 23 | class Swiper extends Component<{}> { 24 | constructor(props) { 25 | super(props); 26 | 27 | const position = new Animated.ValueXY(); 28 | this.state = { 29 | position, 30 | selectedMood: null, 31 | containerWidth: null, 32 | containerHeight: null 33 | }; 34 | 35 | this._moodLayouts = []; 36 | this._handleMoving = this._handleMoving.bind(this); 37 | this._listener = position.addListener(this._handleMoving); 38 | } 39 | 40 | componentWillMount() { 41 | this._panResponder = PanResponder.create({ 42 | onStartShouldSetPanResponder: () => this.props.swipeEnabled, 43 | onMoveShouldSetPanResponder: (evt, gestureState) => 44 | !!this.props.swipeEnabled, 45 | onPanResponderMove: (...args) => 46 | Animated.event([ 47 | null, 48 | { 49 | dx: this.state.position.x, 50 | dy: this.state.position.y 51 | } 52 | ]).apply(this, args), 53 | onPanResponderTerminationRequest: (evt, gestureState) => true, 54 | onPanResponderRelease: (evt, gestureState) => { 55 | if (!this.props.swipeEnabled) return; 56 | 57 | this._handleRelease(); 58 | } 59 | }); 60 | } 61 | 62 | componentWillUnmount() { 63 | if (this._listener) this.state.position.removeListener(this._listener); 64 | } 65 | 66 | /** 67 | * Check the swipe area based on moveable point with initial position of touchable area 68 | * Return true either moveable points are within boundry points or swipe area is not defined 69 | * else false. If swipe area is not defined then whole area is considered as swipeable 70 | * 71 | * @param {object} point 72 | * @returns {boolean} 73 | */ 74 | 75 | _checkSwipeArea(point) { 76 | const { initialPosition, swipeArea } = this.props; 77 | 78 | if (!swipeArea || Object.keys(swipeArea).length === 0) return true; 79 | 80 | const _pointY = point.y + initialPosition.y; 81 | 82 | if (_pointY > swipeArea.y && _pointY <= swipeArea.y + swipeArea.height) { 83 | return true; 84 | } 85 | 86 | return false; 87 | } 88 | 89 | /** 90 | * Find the selected mood based on the points during swiping. Store the selected mood on the state if 91 | * the moveable points are within swipe area or boundry points otherwise null will be stored 92 | * 93 | * @param {object} point 94 | * @returns {undefined} 95 | */ 96 | 97 | _handleMoving(point) { 98 | if (!this.props.swipeEnabled) return; 99 | 100 | let hasSelected = false; 101 | 102 | this._moodLayouts.forEach(mood => { 103 | if ( 104 | !hasSelected && 105 | mood.x + mood.width >= point.x + this.props.initialPosition.x 106 | ) { 107 | if (this._checkSwipeArea(point)) { 108 | this.setState({ selectedMood: mood.id }); 109 | hasSelected = true; 110 | } else { 111 | this.setState({ selectedMood: null }); 112 | hasSelected = false; 113 | } 114 | } 115 | }); 116 | 117 | this.props.onSwipe && this.props.onSwipe(true, hasSelected); 118 | } 119 | 120 | /** 121 | * Store the swipeable's container dimension 122 | * 123 | * @param {object} nativeEvent 124 | * @returns {undefined} 125 | */ 126 | 127 | _setSwiperContainerLayout(nativeEvent) { 128 | this.setState({ 129 | containerWidth: nativeEvent.layout.width, 130 | containerHeight: nativeEvent.layout.height 131 | }); 132 | } 133 | 134 | /** 135 | * Release with the selected mood and reset to null 136 | * 137 | * @returns {undefined} 138 | */ 139 | 140 | _handleRelease() { 141 | this.props.onSwipe && this.props.onSwipe(false, false); 142 | 143 | this.props.onSwipeRelease && 144 | this.props.onSwipeRelease(this.state.selectedMood); 145 | 146 | this.setState({ 147 | selectedMood: null 148 | }); 149 | } 150 | 151 | /** 152 | * Store the mood layouts on array stacks 153 | * 154 | * @param {object} moodLayout 155 | * @returns {undefined} 156 | */ 157 | 158 | _updateMoodLayout(moodLayout) { 159 | const matchedIndex = this._moodLayouts.findIndex( 160 | mood => mood.id === moodLayout.id 161 | ); 162 | 163 | if (matchedIndex !== -1) { 164 | this._moodLayouts[matchedIndex] = moodLayout; 165 | } else { 166 | this._moodLayouts.push(moodLayout); 167 | } 168 | } 169 | 170 | /** 171 | * Update selected mood via tab select and close automatically 172 | * 173 | * @param {number|null} selectedMood 174 | * @returns {undefined} 175 | */ 176 | 177 | _updateSelectedMood(selectedMood) { 178 | this.setState({ selectedMood }, () => { 179 | this._handleRelease(); 180 | }); 181 | } 182 | 183 | /** 184 | * Release if background press is triggered 185 | * 186 | * @returns {undefined} 187 | */ 188 | 189 | _onBackgroundPress() { 190 | this._handleRelease(); 191 | } 192 | 193 | render() { 194 | const { swipeEnabled, children, moods, initialPosition } = this.props; 195 | 196 | return ( 197 | 199 | this._setSwiperContainerLayout(nativeEvent) 200 | } 201 | style={{ flex: 1 }} 202 | {...this._panResponder.panHandlers} 203 | > 204 | {children} 205 | {swipeEnabled ? ( 206 | this._updateMoodLayout(moodLayout)} 209 | onBackgroundPress={() => this._onBackgroundPress()} 210 | onSelectMood={selectedMood => 211 | this._updateSelectedMood(selectedMood) 212 | } 213 | moods={moods} 214 | initialPosition={initialPosition} 215 | /> 216 | ) : null} 217 | 218 | ); 219 | } 220 | } 221 | 222 | Swiper.propTypes = { 223 | moods: PropTypes.array.isRequired, 224 | initialPosition: PropTypes.object.isRequired, 225 | swipeArea: PropTypes.object, 226 | swipeEnabled: PropTypes.bool.isRequired, 227 | onSwipe: PropTypes.func, 228 | onSwipeRelease: PropTypes.func 229 | }; 230 | 231 | Swiper.defaultProps = defaultProps; 232 | 233 | export default Swiper; 234 | -------------------------------------------------------------------------------- /example/Feed.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | import React, { Component } from 'react'; 9 | import { Text, View, Image, FlatList, TouchableOpacity } from 'react-native'; 10 | import Swiper from 'react-native-tap-swipe-select'; 11 | import moods from './moods'; 12 | import styles from './FeedStyle'; 13 | 14 | const feeds = require('./feed'); 15 | 16 | const SLIDE_VALID_MSG = 'Slide Finger Across'; 17 | const SLIDE_INVALID_MSG = 'Relase to Cancel'; 18 | const TAP_SELECT_MSG = 'Tap to Select a Reaction'; 19 | 20 | export default class Feed extends Component<{}> { 21 | constructor(props) { 22 | super(props); 23 | 24 | this.state = { 25 | swipeArea: {}, 26 | isSwiping: false, 27 | swipeEnabled: false, 28 | initialPosition: {}, 29 | selectedSwipeItem: null, 30 | textMsg: SLIDE_VALID_MSG, 31 | myReactions: [] 32 | }; 33 | 34 | this._pressRefs = {}; 35 | this._wrapperRefs = {}; 36 | this._isSwiping = false; 37 | } 38 | 39 | _keyExtractor = (item, index) => item.id; 40 | 41 | /** 42 | * Measures the initial position and swipe area of the list item. 43 | * 44 | * @param {number} key 45 | * @returns {undefined} 46 | */ 47 | 48 | _onLongPress(key) { 49 | this._pressRefs[key].measure((_, __, width, height, pageX, pageY) => { 50 | this.setState({ 51 | swipeEnabled: true, 52 | selectedSwipeItem: key, 53 | textMsg: SLIDE_VALID_MSG, 54 | initialPosition: { x: pageX, y: pageY, width, height } 55 | }); 56 | }); 57 | 58 | this._wrapperRefs[key].measure((x, y, width, height, pageX, pageY) => { 59 | this.setState({ 60 | swipeArea: { x: pageX, y: pageY, width, height } 61 | }); 62 | }); 63 | } 64 | 65 | _addToMyReaction = moodId => { 66 | if (moodId) { 67 | const selectedItemReactionIndex = this.state.myReactions.findIndex( 68 | myReaction => myReaction.itemId === this.state.selectedSwipeItem 69 | ); 70 | 71 | if (selectedItemReactionIndex !== -1) { 72 | this.state.myReactions[selectedItemReactionIndex] = Object.assign( 73 | {}, 74 | this.state.myReactions[selectedItemReactionIndex], 75 | { moodId } 76 | ); 77 | } else { 78 | this.state.myReactions.push({ 79 | itemId: this.state.selectedSwipeItem, 80 | moodId 81 | }); 82 | } 83 | } 84 | }; 85 | 86 | /** 87 | * Callback function during swiping 88 | * 89 | * @param {boolean} isSwiping 90 | * @param {boolean} isSwipeableArea 91 | * 92 | * @return {undefined} 93 | */ 94 | 95 | _onSwipe = (isSwiping, isSwipeableArea) => { 96 | if (isSwiping) { 97 | if (this.pressTimeOut) { 98 | clearTimeout(this.pressTimeOut); 99 | } 100 | this._isSwiping = isSwiping; 101 | if (isSwipeableArea) { 102 | this.state.textMsg !== SLIDE_VALID_MSG && 103 | this.setState({ textMsg: SLIDE_VALID_MSG }); 104 | } else { 105 | this.state.textMsg !== SLIDE_INVALID_MSG && 106 | this.setState({ textMsg: SLIDE_INVALID_MSG }); 107 | } 108 | } 109 | }; 110 | 111 | /** 112 | * Callback function after swiping finish 113 | * 114 | * @param {number|null} selectedMood 115 | * @returns {undefined} 116 | */ 117 | 118 | _onSwipeRelease = selectedMood => { 119 | this._addToMyReaction(selectedMood); 120 | 121 | this.setState({ 122 | swipeEnabled: false, 123 | selectedSwipeItem: null 124 | }); 125 | }; 126 | 127 | _onPressOut() { 128 | this.pressTimeOut = setTimeout( 129 | () => this.setState({ textMsg: TAP_SELECT_MSG }), 130 | 50 131 | ); 132 | } 133 | 134 | _getMood = moodId => { 135 | return moods.find(mood => mood.id === moodId); 136 | }; 137 | 138 | _getItemReacions = (id, reactions) => { 139 | const reactionArr = []; 140 | if (reactions.length > 0) { 141 | reactions.forEach((reaction, i) => { 142 | const selectedMood = this._getMood(reaction.moodId); 143 | if (selectedMood) { 144 | reactionArr.push( 145 | 150 | ); 151 | } 152 | }); 153 | } 154 | 155 | if (this.state.myReactions.length > 0) { 156 | const myReaction = this.state.myReactions.find( 157 | reaction => reaction.itemId === id 158 | ); 159 | 160 | if (myReaction) { 161 | const selectedMood = this._getMood(myReaction.moodId); 162 | if (selectedMood) { 163 | reactionArr.push( 164 | 169 | ); 170 | } 171 | } 172 | } 173 | 174 | return reactionArr; 175 | }; 176 | 177 | _getItemReacionText = id => { 178 | let text = 'Like'; 179 | 180 | if (this.state.myReactions.length > 0) { 181 | const myReaction = this.state.myReactions.find( 182 | reaction => reaction.itemId === id 183 | ); 184 | 185 | if (myReaction) { 186 | const selectedMood = this._getMood(myReaction.moodId); 187 | if (selectedMood) { 188 | if (selectedMood.text && selectedMood.text.length > 0) { 189 | text = selectedMood.text; 190 | } 191 | } 192 | } 193 | } 194 | 195 | return text; 196 | }; 197 | 198 | _hasItemReaction = id => { 199 | let hasItemReaction = false; 200 | 201 | if (this.state.myReactions.length > 0) { 202 | const myReaction = this.state.myReactions.find( 203 | reaction => reaction.itemId === id 204 | ); 205 | 206 | if (myReaction) { 207 | hasItemReaction = true; 208 | } 209 | } 210 | 211 | return hasItemReaction; 212 | }; 213 | 214 | _renderItem = ({ item }) => ( 215 | (this._wrapperRefs[item.id] = ref)} style={styles.row}> 216 | 217 | 218 | 219 | {item.company} 220 | {item.time} 221 | 222 | 223 | {item.title} 224 | 225 | {item.desc} 226 | {item.link} 227 | 228 | {this._getItemReacions(item.id, item.reactions)} 229 | 230 | 1 share 231 | 2 Comments 232 | 233 | 234 | 235 | 236 | (this._pressRefs[item.id] = ref)} 239 | onLongPress={() => this._onLongPress(item.id)} 240 | onPressOut={() => this._onPressOut()} 241 | > 242 | 243 | 250 | {this._getItemReacionText(item.id)} 251 | 252 | 253 | 254 | Comment 255 | Share 256 | 257 | {this.state.swipeEnabled && 258 | this.state.selectedSwipeItem === item.id && ( 259 | 260 | {this.state.textMsg} 261 | 262 | )} 263 | 264 | 265 | ); 266 | 267 | render() { 268 | return ( 269 | 277 | 278 | 285 | 286 | 287 | ); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/MoodPopover.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Dinesh Maharjan 3 | * @Email: httpdeveloper@gmail.com 4 | * @Github: https://github.com/httpdeveloper 5 | * @Web: http://dineshmaharjan.com.np 6 | */ 7 | 8 | import React, { Component } from 'react'; 9 | import { 10 | View, 11 | Modal, 12 | Easing, 13 | Animated, 14 | Platform, 15 | Dimensions, 16 | TouchableWithoutFeedback 17 | } from 'react-native'; 18 | 19 | import PropTypes from 'prop-types'; 20 | import Mood from './Mood'; 21 | import styles from './MoodPopoverStyle'; 22 | 23 | const { width } = Dimensions.get('window'); 24 | const POPUP_DISPLAY_DISTANCE = 100; // How far to be displayed from press item 25 | const POPUP_WIDTH = width - 20; 26 | 27 | const defaultProps = { 28 | moods: [], 29 | initialPosition: {}, 30 | selectedMood: () => {}, 31 | onMoodLayout: () => {}, 32 | onBackgroundPress: () => {}, 33 | onSelectMood: () => {} 34 | }; 35 | 36 | class MoodPopover extends Component<{}> { 37 | constructor(props) { 38 | super(props); 39 | 40 | this.state = { 41 | scale: 1, 42 | modalVisible: true, 43 | popupVisible: false, 44 | selectedSmiley: false, 45 | animatePopupWidth: 50 46 | }; 47 | 48 | this._animatePopupView = new Animated.Value(0); 49 | this._animatePopupWidth = new Animated.Value(50); 50 | this._animatePopupScale = new Animated.Value(1); 51 | this._selectedMood = null; 52 | } 53 | 54 | componentDidMount() { 55 | this._animateSlide(); 56 | } 57 | 58 | /** 59 | * Scaling starts as soon as selected mood receives 60 | * 61 | */ 62 | 63 | componentWillReceiveProps(nextProps) { 64 | if (!nextProps.selectedMood) { 65 | this._selectedMood = null; 66 | } 67 | 68 | if (this._selectedMood) return; 69 | 70 | if ( 71 | this.props.selectedMood !== nextProps.selectedMood && 72 | !this._selectedMood 73 | ) { 74 | this._animatePopupScale = new Animated.Value(1); 75 | } 76 | 77 | this._selectedMood = nextProps.selectedMood; 78 | const minScale = nextProps.selectedMood ? 1 : 0.95; 79 | const maxScale = nextProps.selectedMood ? 0.95 : 1; 80 | 81 | this.setState( 82 | { 83 | scale: this._animatePopupScale.interpolate({ 84 | inputRange: [0, 10], 85 | outputRange: [minScale, maxScale], 86 | extrapolate: 'clamp' 87 | }) 88 | }, 89 | () => { 90 | Animated.spring(this._animatePopupScale, { 91 | toValue: 10, 92 | duration: 50, 93 | speed: 50, 94 | velocity: 8, 95 | bounciness: 0 96 | }).start(() => {}); 97 | } 98 | ); 99 | } 100 | 101 | /** 102 | * Starts width animation transition after vertical slide animation finish 103 | * 104 | * @returns {undefined} 105 | */ 106 | 107 | _animateWidth = () => { 108 | this.setState( 109 | { 110 | animatePopupWidth: this._animatePopupWidth.interpolate({ 111 | inputRange: [2, 500], 112 | outputRange: [50, POPUP_WIDTH], 113 | extrapolate: 'clamp' 114 | }) 115 | }, 116 | () => { 117 | Animated.timing(this._animatePopupWidth, { 118 | toValue: 500, 119 | duration: 160, 120 | easing: Easing.easeInOutQuad 121 | }).start(({ finished }) => { 122 | if (!finished) return; 123 | this.setState({ popupVisible: true }); 124 | }); 125 | } 126 | ); 127 | }; 128 | 129 | /** 130 | * Starts vertical slide animation after popup display 131 | * 132 | * @returns {undefined} 133 | */ 134 | 135 | _animateSlide = () => { 136 | Animated.timing(this._animatePopupView, { 137 | toValue: 500, 138 | duration: 200, 139 | easing: Easing.easeInOutQuart 140 | }).start(({ finished }) => { 141 | if (!finished) return; 142 | this._animateWidth(); 143 | }); 144 | }; 145 | 146 | /** 147 | * Setup vertical slide animation with opacity based on direction. 1 for bottom to top and -1 for top to bottom 148 | * 149 | * @param {number} direction 150 | * @returns {object} 151 | */ 152 | 153 | _slideView = direction => { 154 | return { 155 | opacity: this._animatePopupView.interpolate({ 156 | inputRange: [0, 500], 157 | outputRange: [0, 1], 158 | extrapolate: 'clamp' 159 | }), 160 | transform: [ 161 | { 162 | translateY: this._animatePopupView.interpolate({ 163 | inputRange: [0, 500], 164 | outputRange: [direction * 50, 0], 165 | extrapolate: 'clamp' 166 | }) 167 | }, 168 | { perspective: 1000 } 169 | ] 170 | }; 171 | }; 172 | 173 | /** 174 | * Close request modal for Android 175 | * 176 | * @returns {undefined} 177 | */ 178 | 179 | _closeModal() { 180 | this.setState({ modalVisible: false }); 181 | } 182 | 183 | /** 184 | * Get popup's top position based on initial position 185 | * 186 | * @param {object} initialPosition 187 | * @param {boolean} includeExtraHeight 188 | * @return {number} 189 | */ 190 | 191 | getPopoverTop(initialPosition, includeExtraHeight) { 192 | return initialPosition.y > POPUP_DISPLAY_DISTANCE 193 | ? initialPosition.y - 194 | POPUP_DISPLAY_DISTANCE - 195 | (includeExtraHeight && Platform.OS === 'android' ? 60 : 0) 196 | : POPUP_DISPLAY_DISTANCE; 197 | } 198 | 199 | render() { 200 | const { initialPosition, moods, selectedMood } = this.props; 201 | if (!moods) return; 202 | 203 | const popoverTopDisplay = initialPosition.y > POPUP_DISPLAY_DISTANCE; 204 | const direction = popoverTopDisplay ? 1 : -1; 205 | 206 | const moodArr = []; 207 | const moodWidth = parseInt(POPUP_WIDTH / moods.length); 208 | 209 | moods.forEach((mood, index) => { 210 | moodArr.push( 211 | this.props.onMoodLayout(layout)} 218 | onSelectMood={id => this.props.onSelectMood(id)} 219 | style={{ 220 | width: moodWidth + 0.8, 221 | left: index * moodWidth, 222 | top: this.state.popupVisible ? -60 : 0, 223 | height: this.state.popupVisible ? 165 : 45, 224 | paddingTop: this.state.popupVisible ? 120 : 0 225 | }} 226 | /> 227 | ); 228 | }); 229 | 230 | return ( 231 | this._closeModal()} 235 | > 236 | 267 | {moodArr} 268 | 269 | 270 | {Platform.OS === 'android' && ( 271 | 295 | )} 296 | this.props.onBackgroundPress()} 298 | > 299 | 300 | 301 | 302 | ); 303 | } 304 | } 305 | 306 | MoodPopover.propTypes = { 307 | moods: PropTypes.array.isRequired, 308 | initialPosition: PropTypes.object.isRequired, 309 | selectedMood: PropTypes.oneOfType([null, PropTypes.number]), 310 | onMoodLayout: PropTypes.func, 311 | onBackgroundPress: PropTypes.func, 312 | onSelectMood: PropTypes.func 313 | }; 314 | 315 | MoodPopover.defaultProps = defaultProps; 316 | 317 | export default MoodPopover; 318 | --------------------------------------------------------------------------------