├── .watchmanconfig
├── .gitignore
├── mock.gif
├── assets
└── icons
│ ├── app-icon.png
│ └── loading-icon.png
├── .babelrc
├── package.json
├── README.md
├── app.json
├── App.js
├── CircleTransition.js
└── Swipe.js
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | yarn.lock
--------------------------------------------------------------------------------
/mock.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/narendrashetty/onboarding-RN/HEAD/mock.gif
--------------------------------------------------------------------------------
/assets/icons/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/narendrashetty/onboarding-RN/HEAD/assets/icons/app-icon.png
--------------------------------------------------------------------------------
/assets/icons/loading-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/narendrashetty/onboarding-RN/HEAD/assets/icons/loading-icon.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-expo"],
3 | "env": {
4 | "development": {
5 | "plugins": ["transform-react-jsx-source"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "onboarding-blog",
3 | "version": "0.0.0",
4 | "description": "Hello Expo!",
5 | "author": null,
6 | "private": true,
7 | "main": "node_modules/expo/AppEntry.js",
8 | "dependencies": {
9 | "expo": "^19.0.0",
10 | "react": "16.0.0-alpha.12",
11 | "react-native": "https://github.com/expo/react-native/archive/sdk-19.0.0.tar.gz"
12 | }
13 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # onboarding-RN
2 |
3 | In this repo I mock an app interaction found in [Dribbble](https://dribbble.com/shots/2694049-Pagination-Controller-App-Interface). Details are mentioned in the blog [https://medium.com/@narendrashetty/bubble-animation-with-react-native-72674eab073a](https://medium.com/@narendrashetty/bubble-animation-with-react-native-72674eab073a)
4 |
5 | Expo: [https://expo.io/@narendrashetty/onboarding-blog](https://expo.io/@narendrashetty/onboarding-blog)
6 |
7 | 
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "onboarding-blog",
4 | "description": "An empty new project",
5 | "slug": "onboarding-blog",
6 | "privacy": "public",
7 | "sdkVersion": "19.0.0",
8 | "version": "1.0.0",
9 | "orientation": "portrait",
10 | "primaryColor": "#cccccc",
11 | "icon": "./assets/icons/app-icon.png",
12 | "loading": {
13 | "icon": "./assets/icons/loading-icon.png",
14 | "hideExponentText": false
15 | },
16 | "packagerOpts": {
17 | "assetExts": ["ttf", "mp4"]
18 | },
19 | "ios": {
20 | "supportsTablet": true
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | StyleSheet,
4 | View,
5 | TouchableWithoutFeedback
6 | } from 'react-native';
7 | import CircleTransition from './CircleTransition';
8 | import Swipe from './Swipe';
9 |
10 | const screens = [{
11 | id: 0,
12 | bgcolor: '#698FB2'
13 | }, {
14 | id: 1,
15 | bgcolor: '#68B0B3'
16 | }, {
17 | id: 2,
18 | bgcolor: '#9B91BA'
19 | }]
20 |
21 | export default class App extends React.Component {
22 |
23 | constructor(props) {
24 | super(props);
25 | this.state = {
26 | _counter: 0,
27 | currentbg: screens[0].bgcolor
28 | };
29 | }
30 |
31 | onSwipeLeft() {
32 | const { _counter } = this.state;
33 | let newCounter = _counter < screens.length - 1 ? _counter + 1 : 0;
34 | this.swipeTo(newCounter);
35 | }
36 |
37 | onSwipeRight() {
38 | const { _counter } = this.state;
39 | let newCounter = _counter === 0 ? screens.length - 1 : _counter - 1;
40 | this.swipeTo(newCounter);
41 | }
42 |
43 | swipeTo(counter) {
44 | const newColor = screens[counter].bgcolor;
45 | this.setState({
46 | _counter: counter
47 | }, () => {
48 | this.circleTransition.start(newColor, this.changeColor.bind(this, newColor));
49 | });
50 | }
51 |
52 | changeColor(newColor) {
53 | this.setState({
54 | currentbg: newColor,
55 | });
56 | }
57 |
58 | render() {
59 | return (
60 |
65 | { this.circleTransition = circle }}
67 | />
68 |
69 | );
70 | }
71 | }
72 |
73 | const styles = StyleSheet.create({
74 | container: {
75 | flex: 1,
76 | alignItems: 'center',
77 | justifyContent: 'center',
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/CircleTransition.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Easing, Modal, Dimensions, Animated } from 'react-native';
3 |
4 | const { width, height } = Dimensions.get('window');
5 |
6 | class CircleTransition extends Component {
7 | constructor (props) {
8 | super(props);
9 |
10 | this.state = {
11 | scale: new Animated.Value(0),
12 | color: '#ccc'
13 | };
14 | }
15 |
16 | start(color, callback) {
17 | this.setState({
18 | color: color
19 | }, () => {
20 | this.animate(callback);
21 | });
22 | }
23 |
24 | animate(callback) {
25 | Animated.timing(this.state.scale, {
26 | toValue: 4,
27 | duration: this.props.duration,
28 | easing: this.props.easing
29 | }).start(() => {
30 | callback();
31 | this.hideCircle();
32 | });
33 | }
34 |
35 | hideCircle () {
36 | this.setState({
37 | scale: new Animated.Value(0)
38 | });
39 | }
40 |
41 | getLeftPosition () {
42 | const halfSize = this.props.size / 2;
43 | const halfWidth = width / 2;
44 | let marginHorizontalTopLeft = -halfSize;
45 |
46 | return marginHorizontalTopLeft + halfWidth;
47 | }
48 |
49 | getTopPosition () {
50 | const halfSize = this.props.size / 2;
51 | let marginVerticalTopLeft = -halfSize;
52 |
53 | return marginVerticalTopLeft + height;
54 | }
55 |
56 | render () {
57 | const {scale, color} = this.state;
58 | const { size } = this.props;
59 | let topPosition = this.getTopPosition();
60 | let leftPosition = this.getLeftPosition();
61 | return (
62 |
74 | )
75 | }
76 | }
77 |
78 | CircleTransition.propTypes = {
79 | size: PropTypes.number,
80 | duration: PropTypes.number,
81 | easing: PropTypes.func,
82 | }
83 |
84 | CircleTransition.defaultProps = {
85 | size: Math.min(width, height) - 1,
86 | duration: 400,
87 | easing: Easing.linear
88 | }
89 |
90 | export default CircleTransition;
--------------------------------------------------------------------------------
/Swipe.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import {
3 | Easing,
4 | StyleSheet,
5 | Text,
6 | View,
7 | Animated,
8 | PanResponder
9 | } from 'react-native';
10 |
11 | const swipeDirections = {
12 | SWIPE_LEFT: 'SWIPE_LEFT',
13 | SWIPE_RIGHT: 'SWIPE_RIGHT'
14 | };
15 |
16 | function isValidSwipe(velocity, velocityThreshold, directionalOffset, directionalOffsetThreshold) {
17 | return Math.abs(velocity) >= velocityThreshold &&
18 | Math.abs(directionalOffset) < directionalOffsetThreshold;
19 | }
20 |
21 | class Swipe extends Component {
22 |
23 | constructor(props) {
24 | super(props);
25 |
26 | this.swipeConfig = {
27 | velocityThreshold: 0.3,
28 | directionalOffsetThreshold: 80
29 | };
30 | }
31 |
32 | componentWillMount() {
33 | this.panResponder = PanResponder.create({
34 | onStartShouldSetPanResponder: () => true,
35 | onPanResponderRelease: (evt, gestureState) => {
36 | const swipeDirection = this._getSwipeDirection(gestureState);
37 | this._triggerSwipeHandlers(swipeDirection, gestureState);
38 | }
39 | });
40 | }
41 |
42 | _triggerSwipeHandlers(swipeDirection, gestureState) {
43 | const {SWIPE_LEFT, SWIPE_RIGHT} = swipeDirections;
44 | switch (swipeDirection) {
45 | case SWIPE_LEFT:
46 | this.props.onSwipeLeft(gestureState);
47 | break;
48 | case SWIPE_RIGHT:
49 | this.props.onSwipeRight(gestureState);
50 | break;
51 | }
52 | }
53 |
54 | _getSwipeDirection(gestureState) {
55 | const {SWIPE_LEFT, SWIPE_RIGHT} = swipeDirections;
56 | const {dx, dy} = gestureState;
57 | if (this._isValidHorizontalSwipe(gestureState)) {
58 | return (dx > 0) ? SWIPE_RIGHT : SWIPE_LEFT;
59 | }
60 | }
61 |
62 | _isValidHorizontalSwipe(gestureState) {
63 | const {vx, dy} = gestureState;
64 | const {velocityThreshold, directionalOffsetThreshold} = this.swipeConfig;
65 | return isValidSwipe(vx, velocityThreshold, dy, directionalOffsetThreshold);
66 | }
67 |
68 | render() {
69 | return (
70 |
73 | {this.props.children}
74 |
75 | );
76 | }
77 | }
78 |
79 | Swipe.propTypes = {
80 | onSwipeLeft: PropTypes.func,
81 | onSwipeRight: PropTypes.func,
82 | }
83 |
84 | Swipe.defaultProps = {
85 | onSwipeLeft: () => {},
86 | onSwipeRight: () => {}
87 | }
88 |
89 | export default Swipe;
90 |
--------------------------------------------------------------------------------