├── 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 | 
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 |
--------------------------------------------------------------------------------