├── .eslintrc ├── README.md ├── index.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "extends" : [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "env" : { 8 | "browser" : true 9 | }, 10 | "globals": { 11 | "__DEV__": false 12 | }, 13 | "rules": { 14 | "indent": [2, 4], 15 | "generator-star-spacing": 0, 16 | "react/jsx-indent": [0, 4], 17 | "jsx-indent-props": [0, 4], 18 | "react/jsx-curly-spacing": [0, "never"], 19 | "react/jsx-boolean-value": [0, "never"], 20 | "semi" : [2, "always"], 21 | "operator-linebreak": [2, "after"], 22 | "no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-animated-steps 2 | A component that helps you render "cards" step by step with animations. 3 | 4 | ![](http://g.recordit.co/bgxfWVQlCg.gif) 5 | 6 | ## Usage 7 | 8 | ```bash 9 | npm install react-native-animated-steps 10 | ``` 11 | 12 | ```javascript 13 | import AnimatedSteps from 'react-native-animated-steps'; 14 | 15 | get cards () { 16 | return [ 17 | ( 18 | 19 | This is the first step. 20 | 21 | ), 22 | ( 23 | 24 | And this is the second. 25 | 26 | ), 27 | ( 28 | 29 | Success ! This is the last step. 30 | 31 | ), 32 | ]; 33 | } 34 | 35 | render () { 36 | return ( 37 | 41 | ); 42 | } 43 | ``` 44 | 45 | ## Props 46 | 47 | Prop | Description | Type | Default 48 | ------ | ------ | ------ | ------ 49 | cards | Array of React Elements | `array` | Required 50 | containerStyle | Style of the view wrapping cards | `object` | Check styles in `index.js` 51 | firstCard | First card to display | `number` | `0` 52 | getNextCard | Override default behaviour [see below](#overriding-default-behaviour) | `function` | `undefined` 53 | getPrevCard | Override default behaviour [see below](#overriding-default-behaviour) | `function` | `undefined` 54 | prevButton | React element to render [see below](#rendering-custom-navigation-buttons) | `object` | Default plain button 55 | nextButton | React element to render [see below](#rendering-custom-navigation-buttons) | `object` | Default plain button 56 | onTransitionStart | Called when transition starts | `function` | `undefined` 57 | onTransitionEnd | Called when transition ends | `function` | `undefined` 58 | onChangeCard | Called when navigating to a new card with the card index as 1st param | `function` | `undefined` 59 | 60 | ## Rendering custom navigation buttons 61 | 62 | Altough `prevButton` and `nextButton` are not required in any way, you will probably want to render your own elements. 63 | You can do that by passing a function returning a React element that will receive the following parameters : 64 | 65 | Parameter | Description | Type 66 | ------ | ------ | ------ 67 | `getCardPos(index)` | Returns the Y position of the card at the supplied `index` | `function` 68 | `currentCard` | Index of the current displayed card | `number` 69 | `prevCard` | Index of the previous card **(in prevButton only)** | `number` 70 | `showPrev() or showNext()` | Call this function to navigate | `function` 71 | 72 | By default, `prevButton` won't be rendered on the first `index`, and `nextButton` won't be on the last. These functions are a great way of displaying them conditionally. For instance : 73 | 74 | ```javascript 75 | nextButton (getCardPos, currentCard, showNext) { 76 | const nextCard = currentCard + 1; 77 | 78 | if (nextCard === 3 && this.state.geolocationStatus !== 2) { 79 | // Don't render the nextButton until the user has been located 80 | return false; 81 | } 82 | 83 | return ( 84 | ... 85 | ); 86 | } 87 | ``` 88 | 89 | ## Overriding default behaviour 90 | 91 | By default, the `showPrev` and `showNext` functions supplied to your custom buttons will navigate to the `index - 1` and `index + 1` cards. 92 | However, you might need to override this behaviour if you want to skip a step for instance. 93 | 94 | In order to do that, you have to supply `getPrevCard` and/or `getNextCard` in your props. These functions need to return the index of the previous or the next card you want to navigate to. 95 | They both receive `getCardPos()` and `currentCard` as their parameters, (see above for their description). 96 | 97 | You should be able to implement any kind of customed logic with these. Here's an example : 98 | 99 | ```javascript 100 | getNextCard (getCardPos, currentCard) { 101 | const { town } = this.state; 102 | const nextCard = currentCard + 1; 103 | 104 | // Handle special cases 105 | if (nextCard === 1) { 106 | if (!town.hoods) { 107 | // Skip the hoodpicker (step 2) if the selected town 108 | // doesn't have any hood anyway 109 | return 2; 110 | } else { 111 | // We need to display the second step 112 | return 1; 113 | } 114 | } 115 | 116 | // In other cases, just display the `currentCard + 1` index 117 | return nextCard; 118 | } 119 | ``` 120 | 121 | ## TODO 122 | 123 | - [ ] Customize transitions 124 | - [ ] Implement `shouldComponentRender` to improve perfs 125 | - [ ] Thoroughly test the component -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { 3 | StyleSheet, 4 | View, 5 | Text, 6 | TouchableOpacity, 7 | Animated, 8 | Dimensions, 9 | InteractionManager, 10 | BackAndroid 11 | } from 'react-native'; 12 | 13 | export default class CardNavigation extends Component { 14 | 15 | // TODO : Provide a custom animation prop. 16 | // It would need two Animated.x() that would be started in this.animate() 17 | // so the callback props (onTransitionStart...) could be fired easily. 18 | // The hard thing would be to provide another prop to set the default 19 | // Animated.Value in each cardPos in the constructor and to pass these values 20 | // in the renderCards loop. 21 | 22 | static propTypes () { 23 | return { 24 | cards: PropTypes.array.isRequired, 25 | containerStyle: PropTypes.object.isRequired, 26 | firstCard: PropTypes.number, 27 | getNextCard: PropTypes.func, 28 | getPrevCard: PropTypes.func, 29 | prevButton: PropTypes.element, 30 | nextButton: PropTypes.element, 31 | onTransitionStart: PropTypes.func, 32 | onTransitionEnd: PropTypes.func, 33 | onChangeCard: PropTypes.func 34 | }; 35 | }; 36 | 37 | constructor (props) { 38 | super(props); 39 | this.state = { 40 | prevCard: false, 41 | currentCard: props.firstCard || 0, 42 | cardsPos: [] 43 | }; 44 | this.deviceHeight = Dimensions.get('window').height; 45 | this.firstCard = props.firstCard || 0; 46 | this.getCardPos = this.getCardPos.bind(this); 47 | this.showCard = this.showCard.bind(this); 48 | this.showNext = this.showNext.bind(this); 49 | this.showPrev = this.showPrev.bind(this); 50 | this.androidBack = this.androidBack.bind(this); 51 | // Init the array of animatable positions 52 | for (let i = 0; i < props.cards.length; i++) { 53 | this.state.cardsPos.push(new Animated.Value(i === this.firstCard ? 0 : -this.deviceHeight)); 54 | } 55 | // Android 56 | BackAndroid.addEventListener('hardwareBackPress', this.androidBack); 57 | } 58 | 59 | componentWillUnmount () { 60 | BackAndroid.removeEventListener('hardwareBackPress', this.androidBack); 61 | } 62 | 63 | shouldComponentUpdate (nextProps, nextState) { 64 | // return this.state.currentCard !== nextState.currentCard; 65 | return true; 66 | } 67 | 68 | androidBack () { 69 | // Using the render method here to know 70 | // easily if going back is possible 71 | if (this.prevButton) { 72 | this.showPrev(); 73 | return true; 74 | } 75 | } 76 | 77 | setCurrentCard (index) { 78 | this.setState({ currentCard: index }); 79 | } 80 | 81 | setPrevCard (index) { 82 | this.setState({ prevCard: index }); 83 | } 84 | 85 | getCardPos (index) { 86 | return this.state.cardsPos[index] || false; 87 | } 88 | 89 | animate (currentCard, nextCard) { 90 | const { onTransitionStart, onTransitionEnd, onChangeCard } = this.props; 91 | const currentVal = this.getCardPos(currentCard); 92 | const nextVal = this.getCardPos(nextCard); 93 | 94 | if (onTransitionStart) { 95 | onTransitionStart(); 96 | } 97 | 98 | Animated.timing( 99 | currentVal, 100 | { toValue: -this.deviceHeight } 101 | ).start(); 102 | 103 | Animated.timing( 104 | nextVal, 105 | { toValue: 0 } 106 | ).start(() => { 107 | if (onTransitionEnd) { 108 | onTransitionEnd(); 109 | } 110 | if (onChangeCard) { 111 | onChangeCard(nextCard); 112 | } 113 | this.setPrevCard(currentCard); 114 | this.setCurrentCard(nextCard); 115 | }); 116 | } 117 | 118 | showCard (index) { 119 | InteractionManager.runAfterInteractions(() => { 120 | const nextVal = this.getCardPos(index); 121 | const { currentCard } = this.state; 122 | 123 | if (!nextVal) { 124 | console.warn(`Trying to show unknown card ${index}`); 125 | return; 126 | } 127 | this.animate(currentCard, index); 128 | }); 129 | } 130 | 131 | showNext () { 132 | const { getNextCard } = this.props; 133 | const { currentCard } = this.state; 134 | const nextIndex = getNextCard ? 135 | getNextCard(this.getCardPos, currentCard) : 136 | this.state.currentCard + 1; 137 | 138 | this.showCard(nextIndex); 139 | } 140 | 141 | showPrev () { 142 | const { getPrevCard } = this.props; 143 | const { prevCard, currentCard } = this.state; 144 | const prevIndex = getPrevCard ? 145 | getPrevCard(this.getCardPos, currentCard, prevCard) : 146 | prevCard < currentCard ? prevCard : currentCard - 1; 147 | 148 | return this.showCard(prevIndex); 149 | } 150 | 151 | get prevButton () { 152 | const { prevButton } = this.props; 153 | const { prevCard, currentCard } = this.state; 154 | const prevVal = this.getCardPos(prevCard); 155 | 156 | if (currentCard === 0 || !prevVal) { 157 | return false; 158 | } 159 | 160 | // Provided button 161 | if (prevButton) { 162 | return prevButton(this.getCardPos, currentCard, prevCard, this.showPrev); 163 | } 164 | 165 | // Default button 166 | return ( 167 | 168 | Préc. 169 | 170 | ); 171 | } 172 | 173 | get nextButton () { 174 | const { nextButton } = this.props; 175 | const { currentCard } = this.state; 176 | 177 | const nextCard = currentCard + 1; 178 | const nextVal = this.getCardPos(nextCard); 179 | 180 | if (!nextVal) { 181 | return false; 182 | } 183 | 184 | // Provided button 185 | if (nextButton) { 186 | return nextButton(this.getCardPos, currentCard, this.showNext); 187 | } 188 | 189 | // Default button 190 | return ( 191 | 192 | Suiv. 193 | 194 | ); 195 | } 196 | 197 | get buttons () { 198 | const prevButton = this.prevButton; 199 | const nextButton = this.nextButton; 200 | 201 | if (!prevButton && !nextButton) { 202 | return false; 203 | } 204 | 205 | let justifyContent = 'space-between'; 206 | 207 | if (prevButton && !nextButton) { 208 | justifyContent = 'flex-start'; 209 | } else if (!prevButton && nextButton) { 210 | justifyContent = 'flex-end'; 211 | } 212 | 213 | return ( 214 | 215 | { prevButton } 216 | { nextButton } 217 | 218 | ); 219 | } 220 | 221 | get renderCards () { 222 | return this.props.cards.map((card, index) => { 223 | return ( 224 | 225 | { card } 226 | 227 | ); 228 | }); 229 | } 230 | 231 | render () { 232 | const { containerStyle } = this.props; 233 | 234 | return ( 235 | 236 | { this.renderCards } 237 | { this.buttons } 238 | 239 | ); 240 | } 241 | } 242 | 243 | const styles = StyleSheet.create({ 244 | container: { 245 | height: Dimensions.get('window').height, 246 | backgroundColor: 'grey' 247 | }, 248 | stepContainer: { 249 | height: Dimensions.get('window').height, 250 | width: Dimensions.get('window').width, 251 | position: 'absolute', 252 | bottom: 0, 253 | left: 0, 254 | right: 0 255 | }, 256 | buttonsContainer: { 257 | position: 'absolute', 258 | bottom: 0, 259 | left: 0, 260 | right: 0, 261 | paddingHorizontal: 10, 262 | paddingBottom: 10, 263 | flexDirection: 'row', 264 | alignItems: 'center', 265 | justifyContent: 'space-between' 266 | }, 267 | prevButtonContainer: { 268 | width: 100, 269 | height: 30, 270 | borderRadius: 50, 271 | alignItems: 'center', 272 | justifyContent: 'center' 273 | }, 274 | nextButtonContainer: { 275 | width: 100, 276 | height: 30, 277 | borderRadius: 50, 278 | alignItems: 'center', 279 | justifyContent: 'center' 280 | } 281 | }); 282 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-animated-steps", 3 | "version": "1.0.0", 4 | "description": "A component that helps you render 'cards' step by step with animations", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "github.com/archriss/react-native-animated-steps" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "native", 13 | "animated", 14 | "forms", 15 | "steps", 16 | "items", 17 | "android", 18 | "ios", 19 | "simple", 20 | "component" 21 | ], 22 | "author": "Archriss", 23 | "license": "ISC" 24 | } 25 | --------------------------------------------------------------------------------