├── .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 | ![](mock.gif?raw=true) -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------