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