UNPKG

17.3 kBJavaScriptView Raw
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { Animated, Easing } from 'react-native';
4import wrapStyleTransforms from './wrapStyleTransforms';
5import getStyleValues from './getStyleValues';
6import flattenStyle from './flattenStyle';
7import createAnimation from './createAnimation';
8import { getAnimationByName, getAnimationNames } from './registry';
9import EASING_FUNCTIONS from './easing';
10
11// These styles are not number based and thus needs to be interpolated
12const INTERPOLATION_STYLE_PROPERTIES = [
13 // Transform styles
14 'rotate',
15 'rotateX',
16 'rotateY',
17 'rotateZ',
18 'skewX',
19 'skewY',
20 'transformMatrix',
21 // View styles
22 'backgroundColor',
23 'borderColor',
24 'borderTopColor',
25 'borderRightColor',
26 'borderBottomColor',
27 'borderLeftColor',
28 'shadowColor',
29 // Text styles
30 'color',
31 'textDecorationColor',
32 // Image styles
33 'tintColor',
34];
35
36const ZERO_CLAMPED_STYLE_PROPERTIES = ['width', 'height'];
37
38// Create a copy of `source` without `keys`
39function 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// Yes it's absurd, but actually fast
50function deepEquals(a, b) {
51 return a === b || JSON.stringify(a) === JSON.stringify(b);
52}
53
54// Determine to what value the animation should tween to
55function 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// Like getAnimationTarget but opposite
70function getAnimationOrigin(iteration, direction) {
71 return getAnimationTarget(iteration, direction) ? 0 : 1;
72}
73
74function 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
85function 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
97function 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// Make (almost) any component animatable, similar to Animated.createAnimatedComponent
126export 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 // Alias registered animations for backwards compatibility
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 // Reverse easing if on the way back
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}