├── .gitignore ├── .npmignore ├── dist ├── AppEntry.d.ts ├── AppEntry.js ├── OnboardingAnimate.d.ts ├── OnboardingAnimate.js ├── index.d.ts ├── index.js ├── styles.d.ts └── styles.js ├── docs ├── .nojekyll ├── README.md └── index.html ├── package.json ├── readme.md ├── src ├── AppEntry.ts ├── OnboardingAnimate.tsx ├── index.ts └── styles.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | src 3 | node_modules 4 | yarn.lock -------------------------------------------------------------------------------- /dist/AppEntry.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | declare type Props = { 3 | appComponent: any; 4 | onboardingComponent: any; 5 | isOnboardingCompleted?: () => boolean; 6 | }; 7 | /** 8 | * Component used as an entry point 9 | * to define whether to display actual app component or on boarding process 10 | */ 11 | export declare class AppEntry extends React.PureComponent { 12 | private _isOnboardingCompleted; 13 | reload: () => void; 14 | render(): React.CElement<{ 15 | children?: React.ReactNode; 16 | reloadAppEntry: () => void; 17 | }, React.Component<{ 18 | children?: React.ReactNode; 19 | reloadAppEntry: () => void; 20 | }, any, any>>; 21 | } 22 | export {}; 23 | -------------------------------------------------------------------------------- /dist/AppEntry.js: -------------------------------------------------------------------------------- 1 | var __rest = (this && this.__rest) || function (s, e) { 2 | var t = {}; 3 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 4 | t[p] = s[p]; 5 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 6 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 7 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 8 | t[p[i]] = s[p[i]]; 9 | } 10 | return t; 11 | }; 12 | import React from 'react'; 13 | /** 14 | * Component used as an entry point 15 | * to define whether to display actual app component or on boarding process 16 | */ 17 | export class AppEntry extends React.PureComponent { 18 | constructor() { 19 | super(...arguments); 20 | this._isOnboardingCompleted = () => { 21 | return true; 22 | }; 23 | this.reload = () => { 24 | this.forceUpdate(); 25 | }; 26 | } 27 | render() { 28 | const _a = this.props, { isOnboardingCompleted, appComponent, onboardingComponent } = _a, rest = __rest(_a, ["isOnboardingCompleted", "appComponent", "onboardingComponent"]); 29 | const activeComponent = (isOnboardingCompleted || this._isOnboardingCompleted)() ? 30 | appComponent : 31 | onboardingComponent; 32 | return React.createElement(activeComponent, Object.assign({ reloadAppEntry: this.reload }, rest)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dist/OnboardingAnimate.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollView, Animated, ViewStyle, NativeSyntheticEvent, NativeScrollEvent } from 'react-native'; 3 | /** 4 | * React Native Component for onboarding process, support animation 5 | * 6 | * @version 0.x 7 | * @author [Ambi Studio](https://github.com/ambistudio) 8 | */ 9 | export default class OnboardingAnimate extends React.Component { 10 | state: { 11 | isLastScene: boolean; 12 | }; 13 | static defaultProps: { 14 | minValueSwipeAccepted: number; 15 | }; 16 | _currentScene: number; 17 | _scrollView?: ScrollView | null; 18 | _translateXValue: Animated.Value; 19 | _currentX: number; 20 | UNSAFE_componentWillMount(): void; 21 | _handlePanResponderMove: (...args: any[]) => void; 22 | _handlePanResponderEnd: (handler: NativeSyntheticEvent) => void; 23 | _recenterScene: () => void; 24 | _prevScene: () => void; 25 | /** 26 | * Animate to next scene 27 | */ 28 | _nextScene: () => void; 29 | /** 30 | * Navigate to a given scene number 31 | * 32 | * @memberof OnboardingAnimate 33 | */ 34 | _animateToSceneNo: (sceneNo: number) => void; 35 | _updateState: () => void; 36 | _animateScene: (toValue: number) => void; 37 | /** 38 | * Get background colors from each scene, create then return an animated background style 39 | */ 40 | _getTransitionBackground: () => { 41 | backgroundColor: Animated.AnimatedInterpolation; 42 | }; 43 | _renderIndicator: (p: any, index: number) => JSX.Element; 44 | /** 45 | * Render actionable page 46 | * (page that is not included within on boarding scenes) 47 | */ 48 | _renderActionablePage: () => JSX.Element | undefined; 49 | _renderScene: (scene: Scene, index: number) => JSX.Element; 50 | render(): JSX.Element; 51 | } 52 | declare type Props = { 53 | minValueSwipeAccepted?: number; 54 | activeColor?: string; 55 | inactiveColor?: string; 56 | sceneContainerStyle?: ViewStyle; 57 | hideStatusBar?: boolean; 58 | navigateButtonTitle?: string; 59 | buttonActionableTitle?: string; 60 | onCompleted?: Function; 61 | navigateButtonCompletedTitle?: string; 62 | enableBackgroundColorTransition: boolean; 63 | scenes: Scene[]; 64 | actionableScene?: any; 65 | }; 66 | declare type Scene = { 67 | component: any; 68 | backgroundColor: string; 69 | }; 70 | export {}; 71 | -------------------------------------------------------------------------------- /dist/OnboardingAnimate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Animated, View, Text, Dimensions, TouchableWithoutFeedback, StatusBar } from 'react-native'; 3 | import Styles from './styles'; 4 | const windowWidth = Dimensions.get("window").width; 5 | /** 6 | * React Native Component for onboarding process, support animation 7 | * 8 | * @version 0.x 9 | * @author [Ambi Studio](https://github.com/ambistudio) 10 | */ 11 | export default class OnboardingAnimate extends React.Component { 12 | constructor() { 13 | super(...arguments); 14 | this.state = { 15 | isLastScene: false 16 | }; 17 | // Scene being displayed, change everytime user navigate (swipe/click) to new scene 18 | this._currentScene = 0; 19 | // Animated value 20 | this._translateXValue = new Animated.Value(0); 21 | this._currentX = 0; 22 | // When user is swiping, animate x cordinate accordingly 23 | this._handlePanResponderMove = Animated.event([ 24 | null, 25 | { dx: this._translateXValue } 26 | ], { useNativeDriver: false }); 27 | // Handler when user stop swiping action 28 | this._handlePanResponderEnd = (handler) => { 29 | let previousX = this._currentScene * windowWidth, dx = this._currentX - previousX; 30 | // Define swipe direction, then animate toward the nearest view and setOffset 31 | // @ts-ignore 32 | const minValueSwipeAccepted = this.props.minValueSwipeAccepted; 33 | if (dx > minValueSwipeAccepted) { 34 | // Swiped left, go to next scene 35 | this._nextScene(); 36 | } 37 | else if (dx < -1 * minValueSwipeAccepted) { 38 | // Swiped right, go to previous scene 39 | this._prevScene(); 40 | } 41 | else { 42 | // Not swipe strong enough, recenter scene 43 | this._recenterScene(); 44 | } 45 | }; 46 | this._recenterScene = () => { 47 | this._animateScene(this._currentScene * windowWidth); 48 | }; 49 | this._prevScene = () => { 50 | if (this._currentScene > 0) { 51 | this._animateToSceneNo(this._currentScene - 1); 52 | } 53 | else { 54 | this._recenterScene(); 55 | } 56 | }; 57 | /** 58 | * Animate to next scene 59 | */ 60 | this._nextScene = () => { 61 | let lastScene = this.props.scenes.length - 1; 62 | if (this._currentScene < lastScene) { 63 | // Animate to next scene and update currentScene index 64 | this._animateToSceneNo(this._currentScene + 1); 65 | } 66 | else if (this._currentScene == lastScene) { 67 | // Process to main application, or any callback after the last scene 68 | this.props.onCompleted && this.props.onCompleted(); 69 | } 70 | else { 71 | // Recenter scene in any other case, might never happend 72 | this._recenterScene(); 73 | } 74 | }; 75 | /** 76 | * Navigate to a given scene number 77 | * 78 | * @memberof OnboardingAnimate 79 | */ 80 | this._animateToSceneNo = (sceneNo) => { 81 | if (this._currentScene != sceneNo) { 82 | let toPosition = sceneNo * windowWidth; 83 | this._currentScene = sceneNo; 84 | this._animateScene(toPosition); 85 | this._updateState(); 86 | } 87 | }; 88 | // Check current scene and update isLastScene state 89 | this._updateState = () => { 90 | let { _currentScene } = this; 91 | if (_currentScene == this.props.scenes.length - 1) { 92 | this.setState({ isLastScene: true }); 93 | } 94 | else if (this.state.isLastScene) { 95 | this.setState({ isLastScene: false }); 96 | } 97 | }; 98 | this._animateScene = (toValue) => { 99 | if (toValue < this.props.scenes.length * windowWidth && this._scrollView) { 100 | this._scrollView.scrollTo({ x: toValue }); 101 | } 102 | }; 103 | /** 104 | * Get background colors from each scene, create then return an animated background style 105 | */ 106 | this._getTransitionBackground = () => { 107 | let { scenes } = this.props, inputRange = [], outputRange = [], i; 108 | for (i = 0; i < scenes.length; i++) { 109 | inputRange.push(i * windowWidth); 110 | outputRange.push(scenes[i].backgroundColor); 111 | } 112 | return { 113 | backgroundColor: this._translateXValue.interpolate({ 114 | inputRange, 115 | outputRange, 116 | extrapolate: 'clamp' 117 | }) 118 | }; 119 | }; 120 | this._renderIndicator = (p, index) => { 121 | let { activeColor, inactiveColor } = this.props, startPosition = index * windowWidth, animatedIndicatorBackground = { 122 | backgroundColor: this._translateXValue.interpolate({ 123 | inputRange: [startPosition - (windowWidth / 2), startPosition, startPosition + (windowWidth / 2)], 124 | outputRange: [inactiveColor, activeColor, inactiveColor], 125 | extrapolate: 'clamp' 126 | }) 127 | }; 128 | return React.createElement(TouchableWithoutFeedback, { onPress: () => { this._animateToSceneNo(index); }, key: `obs_pageindicator-${index}` }, 129 | React.createElement(View, { style: Styles.activePageIndicator }, 130 | React.createElement(Animated.View, { style: [Styles.indicator, animatedIndicatorBackground] }))); 131 | }; 132 | /** 133 | * Render actionable page 134 | * (page that is not included within on boarding scenes) 135 | */ 136 | this._renderActionablePage = () => { 137 | let { actionableScene, sceneContainerStyle } = this.props; 138 | if (actionableScene) { 139 | return React.createElement(View, { style: sceneContainerStyle, collapsable: false }, React.createElement(actionableScene)); 140 | } 141 | }; 142 | this._renderScene = (scene, index) => { 143 | let { sceneContainerStyle } = this.props, animatedValue = this._translateXValue.interpolate({ 144 | inputRange: [index * windowWidth, (index + 1) * windowWidth], 145 | outputRange: [0, windowWidth] 146 | }); 147 | return React.createElement(View, { key: `onboarding_scene_${index}`, style: sceneContainerStyle, collapsable: false }, React.createElement(scene.component, { animatedValue })); 148 | }; 149 | } 150 | UNSAFE_componentWillMount() { 151 | this._translateXValue.addListener(({ value }) => { 152 | // Keep track of transition X value, used to difine swipe direction 153 | this._currentX = value; 154 | }); 155 | } 156 | render() { 157 | let { scenes, enableBackgroundColorTransition, actionableScene, activeColor, hideStatusBar, navigateButtonTitle, navigateButtonCompletedTitle, buttonActionableTitle } = this.props, containerStyle = [ 158 | Styles.container 159 | ], sceneLength = scenes.length + (actionableScene ? 1 : 0); 160 | if (enableBackgroundColorTransition) { 161 | // @ts-ignore 162 | containerStyle.push(this._getTransitionBackground()); 163 | } 164 | return (React.createElement(Animated.View, { style: containerStyle }, 165 | React.createElement(Animated.ScrollView, { ref: (ref) => this._scrollView = ref, style: { flex: 1 }, horizontal: true, showsHorizontalScrollIndicator: false, scrollEventThrottle: 14, onScroll: Animated.event([{ 166 | nativeEvent: { 167 | contentOffset: { 168 | x: this._translateXValue 169 | } 170 | } 171 | }], { 172 | useNativeDriver: false 173 | }), onScrollEndDrag: this._handlePanResponderEnd }, 174 | React.createElement(View, { style: [ 175 | Styles.animatedContainer, 176 | { 177 | width: windowWidth * scenes.length 178 | } 179 | ] }, 180 | scenes.map(this._renderScene), 181 | this._renderActionablePage())), 182 | React.createElement(View, { style: [Styles.controllerWrapper] }, 183 | React.createElement(View, { style: Styles.activePageIndicatorWrapper }, scenes.map(this._renderIndicator)), 184 | !actionableScene ? null : 185 | React.createElement(TouchableWithoutFeedback, { onPress: () => { 186 | this._animateToSceneNo(sceneLength - 1); 187 | } }, 188 | React.createElement(View, { style: { padding: 10, marginBottom: 5 } }, 189 | React.createElement(Text, { style: [Styles.btnText, { color: activeColor }] }, buttonActionableTitle))), 190 | React.createElement(TouchableWithoutFeedback, { onPress: () => this._nextScene() }, 191 | React.createElement(View, { style: [Styles.btnPositive, { width: windowWidth * .7, backgroundColor: activeColor }] }, 192 | React.createElement(Text, { style: Styles.btnPositiveText }, this.state.isLastScene ? navigateButtonCompletedTitle : navigateButtonTitle)))), 193 | React.createElement(StatusBar, { hidden: hideStatusBar }))); 194 | } 195 | } 196 | OnboardingAnimate.defaultProps = { 197 | minValueSwipeAccepted: 50 198 | }; 199 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import OnboardingAnimate from "./OnboardingAnimate"; 2 | export default OnboardingAnimate; 3 | export { AppEntry } from "./AppEntry"; 4 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | import OnboardingAnimate from "./OnboardingAnimate"; 2 | export default OnboardingAnimate; 3 | export { AppEntry } from "./AppEntry"; 4 | -------------------------------------------------------------------------------- /dist/styles.d.ts: -------------------------------------------------------------------------------- 1 | declare const Colors: { 2 | activeColor: string; 3 | inactiveColor: string; 4 | }; 5 | declare const _default: any; 6 | export default _default; 7 | export { Colors }; 8 | -------------------------------------------------------------------------------- /dist/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Dimensions } from 'react-native'; 2 | const Colors = { 3 | activeColor: '#2077E2', 4 | inactiveColor: 'rgba(0, 0, 0, 0.2)' 5 | }, { width } = Dimensions.get('window'), Styles = { 6 | // 7 | container: { 8 | flex: 1 9 | }, 10 | // 11 | animatedContainer: { 12 | display: 'flex', 13 | flexDirection: 'row', 14 | flex: 1 15 | }, 16 | // Scene Container 17 | sceneContainer: { 18 | flex: 1, 19 | paddingBottom: 100 20 | }, 21 | // Navigation Buttons 22 | controllerWrapper: { 23 | position: 'absolute', 24 | bottom: 0, 25 | left: 0, 26 | width: width, 27 | paddingBottom: 20, 28 | alignItems: 'center', 29 | justifyContent: 'flex-start' 30 | }, 31 | activePageIndicatorWrapper: { 32 | display: 'flex', 33 | flexDirection: 'row', 34 | marginBottom: 10 35 | }, 36 | activePageIndicator: { 37 | padding: 5 38 | }, 39 | indicator: { 40 | width: 10, 41 | height: 10, 42 | borderRadius: 5, 43 | backgroundColor: Colors.inactiveColor 44 | }, 45 | indicatorActive: { 46 | backgroundColor: Colors.activeColor 47 | }, 48 | btnText: { 49 | fontSize: 13, 50 | fontWeight: '600', 51 | color: Colors.activeColor 52 | }, 53 | btnTextHiddden: { 54 | color: 'transparent' 55 | }, 56 | btnPositive: { 57 | padding: 15, 58 | display: 'flex', 59 | alignItems: 'center', 60 | borderRadius: 5 61 | }, 62 | btnPositiveText: { 63 | color: 'white', 64 | fontWeight: '600' 65 | } 66 | }; 67 | export default StyleSheet.create(Styles); 68 | export { Colors }; 69 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambistudio/react-native-onboarding-animate/26042af2bf7e671a3a7b1f86956ba10795f149bc/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # React Native Onboarding Animate 2 | React native component for onboarding processes with animation 3 | 4 | ## Demo 5 | 6 | [Onboarding Animation Expo's snack](https://snack.expo.io/@hieunc/on-boarding-animation) 7 | 8 | ## Install 9 | 10 | Install `react-native-onboarding-animate` package and save into `package.json`: 11 | ```ssh 12 | $ npm install react-native-onboarding-animate --save 13 | ``` 14 | 15 | ## How to use? 16 | 17 | ```javascript 18 | import React, { Component } from 'react'; 19 | 20 | import OnboardingAnimate from 'react-native-onboarding-animate'; 21 | import { 22 | FirstScene, 23 | SecondScene, 24 | ThirdScene 25 | } from './ExampleScenes'; 26 | 27 | export default class App extends Component { 28 | 29 | render() { 30 | 31 | // Define scenes, it will be displayed in order 32 | let scenes = [ 33 | { 34 | component: FirstScene, 35 | backgroundColor: 'yellow' 36 | }, { 37 | component: SecondScene, 38 | backgroundColor: 'orange' 39 | }, { 40 | component: ThirdScene, 41 | backgroundColor: 'red' 42 | } 43 | ]; 44 | 45 | return ; 49 | } 50 | } 51 | 52 | ``` 53 | 54 | ### animatedValue 55 | 56 | Each of scence will be injected in `this.props` with an `animatedValue` with `inputRange = [0, windowWidth]`. This can be used for any animation within the scence by using interpolate. For example: 57 | 58 | ```javascript 59 | var animateValue = this.props.animatedValue.interpolate({ 60 | inputRange: [0, windowWidth], 61 | outputRange: [0, 10] 62 | }) 63 | ``` 64 | 65 | (Please see actual code in the Expo example, file `./ExampleScenes/FirstScene.js` line `19` ) 66 | 67 | ## Properties 68 | 69 | | Name | Type | Default Value | Definition | 70 | | ---- | ---- | ------------- | ---------- | 71 | | scenes | array of object { component: (required), backgroundColor: (optional) } | - | component: the view that will be displayed, backgroundColor: color of the view's background that will be animated 72 | | enableBackgroundColorTransition | boolean | undefined | Set to `true` to animate background color when transitining view/component 73 | | activeColor | string (hex, rgba, etc.) | `rgba(32, 119, 336, 1)` | color of active indicator, `Continue` button background color 74 | | inactiveColor | string (hex, rgba, etc.) | `rgba(0, 0, 0, 0.2)` | color of inactive indicator 75 | 76 | 77 | ### Property injected in each scence `props` 78 | 79 | | Name | Type | Default Value | Definition | 80 | | ---- | ---- | ------------- | ---------- | 81 | | animatedValue | interpolate value of Animated.Value | inputRange: [0, windowWidth] | an animated value, use for animation within a page by using `this.props.animatedValue.interpolate` 82 | 83 | ## Todo 84 | 85 | - Test on android 86 | - Create tests -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react-native-onboarding-animate - On boarding component with animation 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-onboarding-animate", 3 | "version": "1.2.4", 4 | "description": "On boarding component with animation", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ambistudio/react-native-onboarding-animate.git" 13 | }, 14 | "keywords": [ 15 | "react-native", 16 | "onboarding", 17 | "animation", 18 | "onboarding", 19 | "animate", 20 | "onboarding" 21 | ], 22 | "author": "Ambi Studio", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/ambistudio/react-native-onboarding-animate/issues" 26 | }, 27 | "homepage": "https://github.com/ambistudio/react-native-onboarding-animate#readme", 28 | "peerDependencies": { 29 | "react": "^17.0.1", 30 | "react-native": "^0.63.4" 31 | }, 32 | "devDependencies": { 33 | "@types/react": "^17.0.0", 34 | "@types/react-native": "^0.63.46" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # React Native Onboarding Animate 2 | React native component for onboarding processes with animation 3 | 4 | ## Demo 5 | 6 | [Onboarding Animation Expo's snack](https://snack.expo.io/@hieunc/on-boarding-animation) 7 | 8 | ## Install 9 | 10 | Install `react-native-onboarding-animate` package and save into `package.json`: 11 | ```ssh 12 | $ npm install react-native-onboarding-animate --save 13 | ``` 14 | 15 | ## How to use? 16 | 17 | ```javascript 18 | import React, { Component } from 'react'; 19 | 20 | import OnboardingAnimate from 'react-native-onboarding-animate'; 21 | import { 22 | FirstScene, 23 | SecondScene, 24 | ThirdScene 25 | } from './ExampleScenes'; 26 | 27 | export default class App extends Component { 28 | 29 | render() { 30 | 31 | // Define scenes, it will be displayed in order 32 | let scenes = [ 33 | { 34 | component: FirstScene, 35 | backgroundColor: 'yellow' 36 | }, { 37 | component: SecondScene, 38 | backgroundColor: 'orange' 39 | }, { 40 | component: ThirdScene, 41 | backgroundColor: 'red' 42 | } 43 | ]; 44 | 45 | return ; 49 | } 50 | } 51 | 52 | ``` 53 | 54 | ### animatedValue 55 | 56 | Each of scence will be injected in `this.props` with an `animatedValue` with `inputRange = [0, windowWidth]`. This can be used for any animation within the scence by using interpolate. For example: 57 | 58 | ```javascript 59 | var animateValue = this.props.animatedValue.interpolate({ 60 | inputRange: [0, windowWidth], 61 | outputRange: [0, 10] 62 | }) 63 | ``` 64 | 65 | (Please see actual code in the Expo example, file `./ExampleScenes/FirstScene.js` line `19` ) 66 | 67 | ## Properties 68 | 69 | | Name | Type | Default Value | Definition | 70 | | ---- | ---- | ------------- | ---------- | 71 | | scenes | array of object { component: (required), backgroundColor: (optional) } | - | component: the view that will be displayed, backgroundColor: color of the view's background that will be animated 72 | | enableBackgroundColorTransition | boolean | undefined | Set to `true` to animate background color when transitining view/component 73 | | activeColor | string (hex, rgba, etc.) | `rgba(32, 119, 336, 1)` | color of active indicator, `Continue` button background color 74 | | inactiveColor | string (hex, rgba, etc.) | `rgba(0, 0, 0, 0.2)` | color of inactive indicator 75 | 76 | 77 | ### Property injected in each scence `props` 78 | 79 | | Name | Type | Default Value | Definition | 80 | | ---- | ---- | ------------- | ---------- | 81 | | animatedValue | interpolate value of Animated.Value | inputRange: [0, windowWidth] | an animated value, use for animation within a page by using `this.props.animatedValue.interpolate` 82 | 83 | ## Transpile 84 | 85 | Run the below command to transpile typescript to javascript 86 | 87 | ``` 88 | $ tsc 89 | 90 | // yarn or npm 91 | $ yarn build 92 | $ npm run build 93 | ``` 94 | 95 | ## Todo 96 | 97 | - Test on android 98 | - Create tests -------------------------------------------------------------------------------- /src/AppEntry.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | appComponent: any, 5 | onboardingComponent: any, 6 | isOnboardingCompleted?: () => boolean 7 | } 8 | 9 | /** 10 | * Component used as an entry point 11 | * to define whether to display actual app component or on boarding process 12 | */ 13 | export class AppEntry extends React.PureComponent { 14 | 15 | 16 | 17 | private _isOnboardingCompleted = () => { 18 | return true 19 | }; 20 | 21 | reload = () => { 22 | this.forceUpdate() 23 | } 24 | 25 | render() { 26 | const { 27 | isOnboardingCompleted, 28 | appComponent, 29 | onboardingComponent, 30 | ...rest 31 | } = this.props; 32 | 33 | const activeComponent = (isOnboardingCompleted || this._isOnboardingCompleted)() ? 34 | appComponent : 35 | onboardingComponent; 36 | 37 | return React.createElement(activeComponent, { reloadAppEntry: this.reload, ...rest }); 38 | } 39 | } -------------------------------------------------------------------------------- /src/OnboardingAnimate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ScrollView, 4 | Animated, 5 | View, 6 | Text, 7 | Dimensions, 8 | TouchableWithoutFeedback, 9 | StatusBar, 10 | ViewStyle, 11 | NativeSyntheticEvent, 12 | NativeScrollEvent 13 | } from 'react-native'; 14 | 15 | import Styles from './styles'; 16 | 17 | const windowWidth = Dimensions.get("window").width; 18 | 19 | /** 20 | * React Native Component for onboarding process, support animation 21 | * 22 | * @version 0.x 23 | * @author [Ambi Studio](https://github.com/ambistudio) 24 | */ 25 | export default class OnboardingAnimate extends React.Component { 26 | 27 | state = { 28 | isLastScene: false 29 | } 30 | 31 | static defaultProps = { 32 | minValueSwipeAccepted: 50 33 | } 34 | 35 | // Scene being displayed, change everytime user navigate (swipe/click) to new scene 36 | _currentScene = 0; 37 | _scrollView?: ScrollView | null; 38 | 39 | // Animated value 40 | _translateXValue = new Animated.Value(0); 41 | _currentX = 0; 42 | 43 | UNSAFE_componentWillMount() { 44 | 45 | this._translateXValue.addListener(({ value }) => { 46 | 47 | // Keep track of transition X value, used to difine swipe direction 48 | this._currentX = value; 49 | }) 50 | 51 | } 52 | 53 | // When user is swiping, animate x cordinate accordingly 54 | _handlePanResponderMove = Animated.event([ 55 | null, 56 | { dx: this._translateXValue } 57 | ], { useNativeDriver: false }); 58 | 59 | // Handler when user stop swiping action 60 | _handlePanResponderEnd = (handler: NativeSyntheticEvent) => { 61 | 62 | 63 | let previousX = this._currentScene * windowWidth 64 | , dx = this._currentX - previousX; 65 | 66 | // Define swipe direction, then animate toward the nearest view and setOffset 67 | 68 | // @ts-ignore 69 | const minValueSwipeAccepted: number = this.props.minValueSwipeAccepted; 70 | 71 | if (dx > minValueSwipeAccepted) { 72 | 73 | // Swiped left, go to next scene 74 | this._nextScene(); 75 | } else if (dx < -1 * minValueSwipeAccepted) { 76 | 77 | // Swiped right, go to previous scene 78 | this._prevScene(); 79 | } else { 80 | 81 | // Not swipe strong enough, recenter scene 82 | this._recenterScene(); 83 | } 84 | } 85 | 86 | _recenterScene = () => { 87 | this._animateScene(this._currentScene * windowWidth) 88 | } 89 | 90 | _prevScene = () => { 91 | if (this._currentScene > 0) { 92 | this._animateToSceneNo(this._currentScene - 1); 93 | } else { 94 | this._recenterScene() 95 | } 96 | } 97 | 98 | /** 99 | * Animate to next scene 100 | */ 101 | _nextScene = () => { 102 | 103 | let lastScene = this.props.scenes.length - 1; 104 | if (this._currentScene < lastScene) { 105 | 106 | // Animate to next scene and update currentScene index 107 | this._animateToSceneNo(this._currentScene + 1) 108 | 109 | } else if (this._currentScene == lastScene) { 110 | 111 | // Process to main application, or any callback after the last scene 112 | this.props.onCompleted && this.props.onCompleted(); 113 | 114 | } else { 115 | 116 | // Recenter scene in any other case, might never happend 117 | this._recenterScene() 118 | 119 | } 120 | } 121 | 122 | /** 123 | * Navigate to a given scene number 124 | * 125 | * @memberof OnboardingAnimate 126 | */ 127 | _animateToSceneNo = (sceneNo: number) => { 128 | if (this._currentScene != sceneNo) { 129 | let toPosition = sceneNo * windowWidth; 130 | this._currentScene = sceneNo; 131 | this._animateScene(toPosition); 132 | this._updateState(); 133 | } 134 | } 135 | 136 | // Check current scene and update isLastScene state 137 | _updateState = () => { 138 | let { _currentScene } = this; 139 | if (_currentScene == this.props.scenes.length - 1) { 140 | this.setState({ isLastScene: true }) 141 | } else if (this.state.isLastScene) { 142 | this.setState({ isLastScene: false }) 143 | } 144 | 145 | } 146 | 147 | _animateScene = (toValue: number) => { 148 | if (toValue < this.props.scenes.length * windowWidth && this._scrollView) { 149 | this._scrollView.scrollTo({ x: toValue }) 150 | } 151 | } 152 | 153 | /** 154 | * Get background colors from each scene, create then return an animated background style 155 | */ 156 | _getTransitionBackground = () => { 157 | let { scenes } = this.props 158 | , inputRange = [], outputRange = [] 159 | , i; 160 | 161 | for (i = 0; i < scenes.length; i++) { 162 | inputRange.push(i * windowWidth) 163 | outputRange.push(scenes[i].backgroundColor) 164 | } 165 | 166 | return { 167 | backgroundColor: this._translateXValue.interpolate({ 168 | inputRange, 169 | outputRange, 170 | extrapolate: 'clamp' 171 | }) 172 | }; 173 | } 174 | 175 | _renderIndicator = (p: any, index: number) => { 176 | 177 | let { activeColor, inactiveColor } = this.props 178 | , startPosition = index * windowWidth 179 | , animatedIndicatorBackground = { 180 | backgroundColor: this._translateXValue.interpolate({ 181 | inputRange: [startPosition - (windowWidth / 2), startPosition, startPosition + (windowWidth / 2)], 182 | outputRange: [inactiveColor as string, activeColor as string, inactiveColor as string], 183 | extrapolate: 'clamp' 184 | }) 185 | }; 186 | 187 | return { this._animateToSceneNo(index) }} 189 | key={`obs_pageindicator-${index}`} 190 | > 191 | 192 | 193 | 194 | ; 195 | } 196 | 197 | /** 198 | * Render actionable page 199 | * (page that is not included within on boarding scenes) 200 | */ 201 | _renderActionablePage = () => { 202 | let { actionableScene, sceneContainerStyle } = this.props; 203 | 204 | if (actionableScene) { 205 | return 209 | {React.createElement(actionableScene)} 210 | 211 | } 212 | } 213 | 214 | _renderScene = (scene: Scene, index: number) => { 215 | let { sceneContainerStyle } = this.props 216 | , animatedValue = this._translateXValue.interpolate({ 217 | inputRange: [index * windowWidth, (index + 1) * windowWidth], 218 | outputRange: [0, windowWidth] 219 | }); 220 | 221 | return 226 | {React.createElement(scene.component, { animatedValue })} 227 | 228 | } 229 | 230 | render() { 231 | let { 232 | scenes, 233 | enableBackgroundColorTransition, 234 | actionableScene, 235 | activeColor, 236 | hideStatusBar, 237 | navigateButtonTitle, 238 | navigateButtonCompletedTitle, 239 | buttonActionableTitle 240 | } = this.props 241 | 242 | , containerStyle = [ 243 | Styles.container 244 | ] 245 | , sceneLength = scenes.length + (actionableScene ? 1 : 0); 246 | 247 | if (enableBackgroundColorTransition) { 248 | // @ts-ignore 249 | containerStyle.push(this._getTransitionBackground()); 250 | } 251 | 252 | return ( 253 | 256 | this._scrollView = ref} 258 | style={{ flex: 1 }} 259 | horizontal={true} 260 | showsHorizontalScrollIndicator={false} 261 | scrollEventThrottle={14} 262 | onScroll={Animated.event([{ 263 | nativeEvent: { 264 | contentOffset: { 265 | x: this._translateXValue 266 | } 267 | } 268 | }], { 269 | useNativeDriver: false 270 | })} 271 | onScrollEndDrag={this._handlePanResponderEnd} 272 | > 273 | 279 | {scenes.map(this._renderScene)} 280 | {this._renderActionablePage()} 281 | 282 | 283 | 284 | {/* Navigation Area */} 285 | 286 | 287 | {/* Slider active scene indicator */} 288 | 289 | {scenes.map(this._renderIndicator)} 290 | 291 | {/* END: Slider active scene indicator */} 292 | 293 | {/* Text button for actionable scene */} 294 | {!actionableScene ? null : 295 | { 296 | this._animateToSceneNo(sceneLength - 1) 297 | }}> 298 | 299 | {buttonActionableTitle} 300 | 301 | 302 | } 303 | {/* END: Text button for actionable scene */} 304 | 305 | {/* Navigate to next page button */} 306 | this._nextScene()}> 307 | 308 | 309 | {this.state.isLastScene ? navigateButtonCompletedTitle : navigateButtonTitle} 310 | 311 | 312 | 313 | {/* END: Navigate to next page button */} 314 | 315 | 316 | {/* END: Navigation Area */} 317 | 318 | 320 | ); 321 | } 322 | } 323 | 324 | type Props = { 325 | // Mininum acceptable value for allowing navigate to next or previous scene 326 | minValueSwipeAccepted?: number, 327 | // Color when indicator is showing active state 328 | activeColor?: string, 329 | // Color when indicator is showing inactive state 330 | inactiveColor?: string, 331 | // Style of each scene container 332 | sceneContainerStyle?: ViewStyle, 333 | // Hide statusbar 334 | hideStatusBar?: boolean, 335 | // Text title of the navigate to next scene button 336 | navigateButtonTitle?: string, 337 | // Text title of the link to navigate to the actionable scene 338 | buttonActionableTitle?: string, 339 | // Callback when click on 'Completed' butotn in the last scene 340 | onCompleted?: Function, 341 | 342 | navigateButtonCompletedTitle?: string, 343 | enableBackgroundColorTransition: boolean, 344 | 345 | scenes: Scene[], 346 | actionableScene?: any 347 | }; 348 | 349 | type Scene = { 350 | component: any, 351 | backgroundColor: string, 352 | } 353 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import OnboardingAnimate from "./OnboardingAnimate"; 2 | export default OnboardingAnimate; 3 | 4 | export { 5 | AppEntry 6 | } from "./AppEntry"; -------------------------------------------------------------------------------- /src/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Dimensions } from 'react-native'; 2 | 3 | const Colors = { 4 | activeColor: '#2077E2', 5 | inactiveColor: 'rgba(0, 0, 0, 0.2)' 6 | }, 7 | { width } = Dimensions.get('window'), 8 | Styles = { 9 | 10 | // 11 | container: { 12 | flex: 1 13 | }, 14 | 15 | // 16 | animatedContainer: { 17 | display: 'flex', 18 | flexDirection: 'row', 19 | flex: 1 20 | }, 21 | 22 | // Scene Container 23 | sceneContainer: { 24 | flex: 1, 25 | paddingBottom: 100 26 | }, 27 | 28 | // Navigation Buttons 29 | controllerWrapper: { 30 | position: 'absolute', 31 | bottom: 0, 32 | left: 0, 33 | width: width, 34 | paddingBottom: 20, 35 | alignItems: 'center', 36 | justifyContent: 'flex-start' 37 | }, 38 | activePageIndicatorWrapper: { 39 | display: 'flex', 40 | flexDirection: 'row', 41 | marginBottom: 10 42 | }, 43 | activePageIndicator: { 44 | padding: 5 45 | }, 46 | indicator: { 47 | width: 10, 48 | height: 10, 49 | borderRadius: 5, 50 | backgroundColor: Colors.inactiveColor 51 | }, 52 | indicatorActive: { 53 | backgroundColor: Colors.activeColor 54 | }, 55 | btnText: { 56 | fontSize: 13, 57 | fontWeight: '600', 58 | color: Colors.activeColor 59 | }, 60 | btnTextHiddden: { 61 | color: 'transparent' 62 | }, 63 | btnPositive: { 64 | padding: 15, 65 | display: 'flex', 66 | alignItems: 'center', 67 | borderRadius: 5 68 | }, 69 | btnPositiveText: { 70 | color: 'white', 71 | fontWeight: '600' 72 | } 73 | }; 74 | 75 | export default StyleSheet.create(Styles as any); 76 | export { 77 | Colors 78 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | // "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | "noEmit": false, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "exclude": [ 70 | "node_modules", 71 | "babel.config.js", 72 | "metro.config.js", 73 | "jest.config.js" 74 | ] 75 | } 76 | --------------------------------------------------------------------------------