1 | import React, { Component } from 'react';
|
2 | import PropTypes from 'prop-types';
|
3 | import { Animated, Easing } from 'react-native';
|
4 | import wrapStyleTransforms from './wrapStyleTransforms';
|
5 | import getStyleValues from './getStyleValues';
|
6 | import flattenStyle from './flattenStyle';
|
7 | import createAnimation from './createAnimation';
|
8 | import { getAnimationByName, getAnimationNames } from './registry';
|
9 | import EASING_FUNCTIONS from './easing';
|
10 |
|
11 |
|
12 | const INTERPOLATION_STYLE_PROPERTIES = [
|
13 |
|
14 | 'rotate',
|
15 | 'rotateX',
|
16 | 'rotateY',
|
17 | 'rotateZ',
|
18 | 'skewX',
|
19 | 'skewY',
|
20 | 'transformMatrix',
|
21 |
|
22 | 'backgroundColor',
|
23 | 'borderColor',
|
24 | 'borderTopColor',
|
25 | 'borderRightColor',
|
26 | 'borderBottomColor',
|
27 | 'borderLeftColor',
|
28 | 'shadowColor',
|
29 |
|
30 | 'color',
|
31 | 'textDecorationColor',
|
32 |
|
33 | 'tintColor',
|
34 | ];
|
35 |
|
36 | const ZERO_CLAMPED_STYLE_PROPERTIES = ['width', 'height'];
|
37 |
|
38 |
|
39 | function omit(keys, source) {
|
40 | const filtered = {};
|
41 | Object.keys(source).forEach(key => {
|
42 | if (keys.indexOf(key) === -1) {
|
43 | filtered[key] = source[key];
|
44 | }
|
45 | });
|
46 | return filtered;
|
47 | }
|
48 |
|
49 |
|
50 | function deepEquals(a, b) {
|
51 | return a === b || JSON.stringify(a) === JSON.stringify(b);
|
52 | }
|
53 |
|
54 |
|
55 | function getAnimationTarget(iteration, direction) {
|
56 | switch (direction) {
|
57 | case 'reverse':
|
58 | return 0;
|
59 | case 'alternate':
|
60 | return iteration % 2 ? 0 : 1;
|
61 | case 'alternate-reverse':
|
62 | return iteration % 2 ? 1 : 0;
|
63 | case 'normal':
|
64 | default:
|
65 | return 1;
|
66 | }
|
67 | }
|
68 |
|
69 |
|
70 | function getAnimationOrigin(iteration, direction) {
|
71 | return getAnimationTarget(iteration, direction) ? 0 : 1;
|
72 | }
|
73 |
|
74 | function getCompiledAnimation(animation) {
|
75 | if (typeof animation === 'string') {
|
76 | const compiledAnimation = getAnimationByName(animation);
|
77 | if (!compiledAnimation) {
|
78 | throw new Error(`No animation registred by the name of ${animation}`);
|
79 | }
|
80 | return compiledAnimation;
|
81 | }
|
82 | return createAnimation(animation);
|
83 | }
|
84 |
|
85 | function makeInterpolatedStyle(compiledAnimation, animationValue) {
|
86 | const style = {};
|
87 | Object.keys(compiledAnimation).forEach(key => {
|
88 | if (key === 'style') {
|
89 | Object.assign(style, compiledAnimation.style);
|
90 | } else if (key !== 'easing') {
|
91 | style[key] = animationValue.interpolate(compiledAnimation[key]);
|
92 | }
|
93 | });
|
94 | return wrapStyleTransforms(style);
|
95 | }
|
96 |
|
97 | function transitionToValue(
|
98 | property,
|
99 | transitionValue,
|
100 | toValue,
|
101 | duration,
|
102 | easing,
|
103 | useNativeDriver = false,
|
104 | delay,
|
105 | onTransitionBegin,
|
106 | onTransitionEnd,
|
107 | ) {
|
108 | const animation =
|
109 | duration || easing || delay
|
110 | ? Animated.timing(transitionValue, {
|
111 | toValue,
|
112 | delay,
|
113 | duration: duration || 1000,
|
114 | easing:
|
115 | typeof easing === 'function'
|
116 | ? easing
|
117 | : EASING_FUNCTIONS[easing || 'ease'],
|
118 | useNativeDriver,
|
119 | })
|
120 | : Animated.spring(transitionValue, { toValue, useNativeDriver });
|
121 | setTimeout(() => onTransitionBegin(property), delay);
|
122 | animation.start(() => onTransitionEnd(property));
|
123 | }
|
124 |
|
125 |
|
126 | export default function createAnimatableComponent(WrappedComponent) {
|
127 | const wrappedComponentName =
|
128 | WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
129 |
|
130 | const Animatable = Animated.createAnimatedComponent(WrappedComponent);
|
131 |
|
132 | return class AnimatableComponent extends Component {
|
133 | static displayName = `withAnimatable(${wrappedComponentName})`;
|
134 |
|
135 | static propTypes = {
|
136 | animation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
137 | duration: PropTypes.number,
|
138 | direction: PropTypes.oneOf([
|
139 | 'normal',
|
140 | 'reverse',
|
141 | 'alternate',
|
142 | 'alternate-reverse',
|
143 | ]),
|
144 | delay: PropTypes.number,
|
145 | easing: PropTypes.oneOfType([
|
146 | PropTypes.oneOf(Object.keys(EASING_FUNCTIONS)),
|
147 | PropTypes.func,
|
148 | ]),
|
149 | iterationCount(props, propName) {
|
150 | const val = props[propName];
|
151 | if (val !== 'infinite' && !(typeof val === 'number' && val >= 1)) {
|
152 | return new Error(
|
153 | 'iterationCount must be a positive number or "infinite"',
|
154 | );
|
155 | }
|
156 | return null;
|
157 | },
|
158 | iterationDelay: PropTypes.number,
|
159 | onAnimationBegin: PropTypes.func,
|
160 | onAnimationEnd: PropTypes.func,
|
161 | onTransitionBegin: PropTypes.func,
|
162 | onTransitionEnd: PropTypes.func,
|
163 | style: PropTypes.oneOfType([
|
164 | PropTypes.number,
|
165 | PropTypes.array,
|
166 | PropTypes.object,
|
167 | ]),
|
168 | transition: PropTypes.oneOfType([
|
169 | PropTypes.string,
|
170 | PropTypes.arrayOf(PropTypes.string),
|
171 | ]),
|
172 | useNativeDriver: PropTypes.bool,
|
173 | };
|
174 |
|
175 | static defaultProps = {
|
176 | animation: undefined,
|
177 | delay: 0,
|
178 | direction: 'normal',
|
179 | duration: undefined,
|
180 | easing: undefined,
|
181 | iterationCount: 1,
|
182 | iterationDelay: 0,
|
183 | onAnimationBegin() {},
|
184 | onAnimationEnd() {},
|
185 | onTransitionBegin() {},
|
186 | onTransitionEnd() {},
|
187 | style: undefined,
|
188 | transition: undefined,
|
189 | useNativeDriver: false,
|
190 | };
|
191 |
|
192 | constructor(props) {
|
193 | super(props);
|
194 |
|
195 | const animationValue = new Animated.Value(
|
196 | getAnimationOrigin(0, this.props.direction),
|
197 | );
|
198 | let animationStyle = {};
|
199 | let compiledAnimation = {};
|
200 | if (props.animation) {
|
201 | compiledAnimation = getCompiledAnimation(props.animation);
|
202 | animationStyle = makeInterpolatedStyle(
|
203 | compiledAnimation,
|
204 | animationValue,
|
205 | );
|
206 | }
|
207 | this.state = {
|
208 | animationValue,
|
209 | animationStyle,
|
210 | compiledAnimation,
|
211 | transitionStyle: {},
|
212 | transitionValues: {},
|
213 | currentTransitionValues: {},
|
214 | };
|
215 |
|
216 | if (props.transition) {
|
217 | this.state = {
|
218 | ...this.state,
|
219 | ...this.initializeTransitionState(props.transition),
|
220 | };
|
221 | }
|
222 | this.delayTimer = null;
|
223 |
|
224 |
|
225 | getAnimationNames().forEach(animationName => {
|
226 | if (!(animationName in this)) {
|
227 | this[animationName] = this.animate.bind(this, animationName);
|
228 | }
|
229 | });
|
230 | }
|
231 |
|
232 | initializeTransitionState(transitionKeys) {
|
233 | const transitionValues = {};
|
234 | const styleValues = {};
|
235 |
|
236 | const currentTransitionValues = getStyleValues(
|
237 | transitionKeys,
|
238 | this.props.style,
|
239 | );
|
240 | Object.keys(currentTransitionValues).forEach(key => {
|
241 | const value = currentTransitionValues[key];
|
242 | if (INTERPOLATION_STYLE_PROPERTIES.indexOf(key) !== -1 || typeof(value) !== 'number') {
|
243 | transitionValues[key] = new Animated.Value(0);
|
244 | styleValues[key] = value;
|
245 | } else {
|
246 | const animationValue = new Animated.Value(value);
|
247 | transitionValues[key] = animationValue;
|
248 | styleValues[key] = animationValue;
|
249 | }
|
250 | });
|
251 |
|
252 | return {
|
253 | currentTransitionValues,
|
254 | transitionStyle: styleValues,
|
255 | transitionValues,
|
256 | };
|
257 | }
|
258 |
|
259 | getTransitionState(keys) {
|
260 | const transitionKeys = typeof keys === 'string' ? [keys] : keys;
|
261 | let {
|
262 | transitionValues,
|
263 | currentTransitionValues,
|
264 | transitionStyle,
|
265 | } = this.state;
|
266 | const missingKeys = transitionKeys.filter(
|
267 | key => !this.state.transitionValues[key],
|
268 | );
|
269 | if (missingKeys.length) {
|
270 | const transitionState = this.initializeTransitionState(missingKeys);
|
271 | transitionValues = {
|
272 | ...transitionValues,
|
273 | ...transitionState.transitionValues,
|
274 | };
|
275 | currentTransitionValues = {
|
276 | ...currentTransitionValues,
|
277 | ...transitionState.currentTransitionValues,
|
278 | };
|
279 | transitionStyle = {
|
280 | ...transitionStyle,
|
281 | ...transitionState.transitionStyle,
|
282 | };
|
283 | }
|
284 | return { transitionValues, currentTransitionValues, transitionStyle };
|
285 | }
|
286 |
|
287 | ref = null;
|
288 | handleRef = ref => {
|
289 | this.ref = ref;
|
290 | };
|
291 |
|
292 | setNativeProps(nativeProps) {
|
293 | if (this.ref) {
|
294 | this.ref.setNativeProps(nativeProps);
|
295 | }
|
296 | }
|
297 |
|
298 | componentDidMount() {
|
299 | const {
|
300 | animation,
|
301 | duration,
|
302 | delay,
|
303 | onAnimationBegin,
|
304 | iterationDelay,
|
305 | } = this.props;
|
306 | if (animation) {
|
307 | const startAnimation = () => {
|
308 | onAnimationBegin();
|
309 | this.startAnimation(duration, 0, iterationDelay, endState =>
|
310 | this.props.onAnimationEnd(endState),
|
311 | );
|
312 | this.delayTimer = null;
|
313 | };
|
314 | if (delay) {
|
315 | this.delayTimer = setTimeout(startAnimation, delay);
|
316 | } else {
|
317 | startAnimation();
|
318 | }
|
319 | }
|
320 | }
|
321 |
|
322 | componentWillReceiveProps(props) {
|
323 | const {
|
324 | animation,
|
325 | delay,
|
326 | duration,
|
327 | easing,
|
328 | transition,
|
329 | onAnimationBegin,
|
330 | } = props;
|
331 |
|
332 | if (transition) {
|
333 | const values = getStyleValues(transition, props.style);
|
334 | this.transitionTo(values, duration, easing, delay);
|
335 | } else if (!deepEquals(animation, this.props.animation)) {
|
336 | if (animation) {
|
337 | if (this.delayTimer) {
|
338 | this.setAnimation(animation);
|
339 | } else {
|
340 | onAnimationBegin();
|
341 | this.animate(animation, duration).then(endState =>
|
342 | this.props.onAnimationEnd(endState),
|
343 | );
|
344 | }
|
345 | } else {
|
346 | this.stopAnimation();
|
347 | }
|
348 | }
|
349 | }
|
350 |
|
351 | componentWillUnmount() {
|
352 | if (this.delayTimer) {
|
353 | clearTimeout(this.delayTimer);
|
354 | }
|
355 | }
|
356 |
|
357 | setAnimation(animation, callback) {
|
358 | const compiledAnimation = getCompiledAnimation(animation);
|
359 | const animationStyle = makeInterpolatedStyle(
|
360 | compiledAnimation,
|
361 | this.state.animationValue,
|
362 | );
|
363 | this.setState({ animationStyle, compiledAnimation }, callback);
|
364 | }
|
365 |
|
366 | animate(animation, duration, iterationDelay) {
|
367 | return new Promise(resolve => {
|
368 | this.setAnimation(animation, () => {
|
369 | this.startAnimation(duration, 0, iterationDelay, resolve);
|
370 | });
|
371 | });
|
372 | }
|
373 |
|
374 | stopAnimation() {
|
375 | this.setState({
|
376 | scheduledAnimation: false,
|
377 | animationStyle: {},
|
378 | });
|
379 | this.state.animationValue.stopAnimation();
|
380 | if (this.delayTimer) {
|
381 | clearTimeout(this.delayTimer);
|
382 | this.delayTimer = null;
|
383 | }
|
384 | }
|
385 |
|
386 | startAnimation(duration, iteration, iterationDelay, callback) {
|
387 | const { animationValue, compiledAnimation } = this.state;
|
388 | const { direction, iterationCount, useNativeDriver } = this.props;
|
389 | let easing = this.props.easing || compiledAnimation.easing || 'ease';
|
390 | let currentIteration = iteration || 0;
|
391 | const fromValue = getAnimationOrigin(currentIteration, direction);
|
392 | const toValue = getAnimationTarget(currentIteration, direction);
|
393 | animationValue.setValue(fromValue);
|
394 |
|
395 | if (typeof easing === 'string') {
|
396 | easing = EASING_FUNCTIONS[easing];
|
397 | }
|
398 |
|
399 | const reversed =
|
400 | direction === 'reverse' ||
|
401 | (direction === 'alternate' && !toValue) ||
|
402 | (direction === 'alternate-reverse' && !toValue);
|
403 | if (reversed) {
|
404 | easing = Easing.out(easing);
|
405 | }
|
406 | const config = {
|
407 | toValue,
|
408 | easing,
|
409 | isInteraction: iterationCount <= 1,
|
410 | duration: duration || this.props.duration || 1000,
|
411 | useNativeDriver,
|
412 | delay: iterationDelay || 0,
|
413 | };
|
414 |
|
415 | Animated.timing(animationValue, config).start(endState => {
|
416 | currentIteration += 1;
|
417 | if (
|
418 | endState.finished &&
|
419 | this.props.animation &&
|
420 | (iterationCount === 'infinite' || currentIteration < iterationCount)
|
421 | ) {
|
422 | this.startAnimation(
|
423 | duration,
|
424 | currentIteration,
|
425 | iterationDelay,
|
426 | callback,
|
427 | );
|
428 | } else if (callback) {
|
429 | callback(endState);
|
430 | }
|
431 | });
|
432 | }
|
433 |
|
434 | transition(fromValues, toValues, duration, easing) {
|
435 | const fromValuesFlat = flattenStyle(fromValues);
|
436 | const toValuesFlat = flattenStyle(toValues);
|
437 | const transitionKeys = Object.keys(toValuesFlat);
|
438 | const {
|
439 | transitionValues,
|
440 | currentTransitionValues,
|
441 | transitionStyle,
|
442 | } = this.getTransitionState(transitionKeys);
|
443 |
|
444 | transitionKeys.forEach(property => {
|
445 | const fromValue = fromValuesFlat[property];
|
446 | const toValue = toValuesFlat[property];
|
447 | let transitionValue = transitionValues[property];
|
448 | if (!transitionValue) {
|
449 | transitionValue = new Animated.Value(0);
|
450 | }
|
451 | const needsInterpolation =
|
452 | INTERPOLATION_STYLE_PROPERTIES.indexOf(property) !== -1 || typeof(value) !== 'number';
|
453 | const needsZeroClamping =
|
454 | ZERO_CLAMPED_STYLE_PROPERTIES.indexOf(property) !== -1;
|
455 | if (needsInterpolation) {
|
456 | transitionValue.setValue(0);
|
457 | transitionStyle[property] = transitionValue.interpolate({
|
458 | inputRange: [0, 1],
|
459 | outputRange: [fromValue, toValue],
|
460 | });
|
461 | currentTransitionValues[property] = toValue;
|
462 | toValuesFlat[property] = 1;
|
463 | } else {
|
464 | if (needsZeroClamping) {
|
465 | transitionStyle[property] = transitionValue.interpolate({
|
466 | inputRange: [0, 1],
|
467 | outputRange: [0, 1],
|
468 | extrapolateLeft: 'clamp',
|
469 | });
|
470 | currentTransitionValues[property] = toValue;
|
471 | } else {
|
472 | transitionStyle[property] = transitionValue;
|
473 | }
|
474 | transitionValue.setValue(fromValue);
|
475 | }
|
476 | });
|
477 | this.setState(
|
478 | { transitionValues, transitionStyle, currentTransitionValues },
|
479 | () => {
|
480 | this.transitionToValues(
|
481 | toValuesFlat,
|
482 | duration || this.props.duration,
|
483 | easing,
|
484 | this.props.delay,
|
485 | );
|
486 | },
|
487 | );
|
488 | }
|
489 |
|
490 | transitionTo(toValues, duration, easing, delay) {
|
491 | const { currentTransitionValues } = this.state;
|
492 | const toValuesFlat = flattenStyle(toValues);
|
493 | const transitions = {
|
494 | from: {},
|
495 | to: {},
|
496 | };
|
497 |
|
498 | Object.keys(toValuesFlat).forEach(property => {
|
499 | const toValue = toValuesFlat[property];
|
500 | const needsInterpolation =
|
501 | INTERPOLATION_STYLE_PROPERTIES.indexOf(property) !== -1 || typeof(value) !== 'number';
|
502 | const needsZeroClamping =
|
503 | ZERO_CLAMPED_STYLE_PROPERTIES.indexOf(property) !== -1;
|
504 | const transitionStyle = this.state.transitionStyle[property];
|
505 | const transitionValue = this.state.transitionValues[property];
|
506 | if (
|
507 | !needsInterpolation &&
|
508 | !needsZeroClamping &&
|
509 | transitionStyle &&
|
510 | transitionStyle === transitionValue
|
511 | ) {
|
512 | transitionToValue(
|
513 | property,
|
514 | transitionValue,
|
515 | toValue,
|
516 | duration,
|
517 | easing,
|
518 | this.props.useNativeDriver,
|
519 | delay,
|
520 | prop => this.props.onTransitionBegin(prop),
|
521 | prop => this.props.onTransitionEnd(prop),
|
522 | );
|
523 | } else {
|
524 | let currentTransitionValue = currentTransitionValues[property];
|
525 | if (
|
526 | typeof currentTransitionValue === 'undefined' &&
|
527 | this.props.style
|
528 | ) {
|
529 | const style = getStyleValues(property, this.props.style);
|
530 | currentTransitionValue = style[property];
|
531 | }
|
532 | transitions.from[property] = currentTransitionValue;
|
533 | transitions.to[property] = toValue;
|
534 | }
|
535 | });
|
536 |
|
537 | if (Object.keys(transitions.from).length) {
|
538 | this.transition(transitions.from, transitions.to, duration, easing);
|
539 | }
|
540 | }
|
541 |
|
542 | transitionToValues(toValues, duration, easing, delay) {
|
543 | Object.keys(toValues).forEach(property => {
|
544 | const transitionValue = this.state.transitionValues[property];
|
545 | const toValue = toValues[property];
|
546 | transitionToValue(
|
547 | property,
|
548 | transitionValue,
|
549 | toValue,
|
550 | duration,
|
551 | easing,
|
552 | this.props.useNativeDriver,
|
553 | delay,
|
554 | prop => this.props.onTransitionBegin(prop),
|
555 | prop => this.props.onTransitionEnd(prop),
|
556 | );
|
557 | });
|
558 | }
|
559 |
|
560 | render() {
|
561 | const { style, animation, transition } = this.props;
|
562 | if (animation && transition) {
|
563 | throw new Error('You cannot combine animation and transition props');
|
564 | }
|
565 | const restProps = omit(
|
566 | ['animation', 'duration', 'direction', 'delay', 'easing', 'iterationCount', 'iterationDelay', 'onAnimationBegin', 'onAnimationEnd', 'onTransitionBegin', 'onTransitionEnd', 'style', 'transition', 'useNativeDriver'],
|
567 | this.props,
|
568 | );
|
569 |
|
570 | return (
|
571 | <Animatable
|
572 | ref={this.handleRef}
|
573 | style={[
|
574 | style,
|
575 | this.state.animationStyle,
|
576 | wrapStyleTransforms(this.state.transitionStyle),
|
577 | ]}
|
578 | {...restProps}
|
579 | />
|
580 | );
|
581 | }
|
582 | };
|
583 | }
|