react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
160 lines (157 loc) • 6.63 kB
JavaScript
'use strict';
import { convertPropertiesToArrays, kebabizeCamelCase, maybeAddSuffix } from '../../../common';
import { normalizeTimeUnit } from '../../utils';
import { processKeyframeDefinitions } from '../animationParser';
import { configureWebCSSAnimations, insertCSSAnimation, removeCSSAnimation } from '../domUtils';
import { CSSKeyframesRuleImpl } from '../keyframes';
import { maybeAddSuffixes, parseTimingFunction } from '../utils';
const isCSSKeyframesRuleImpl = keyframes => typeof keyframes === 'object' && 'processedKeyframes' in keyframes;
export default class CSSAnimationsManager {
// Keys are processed keyframes
attachedAnimations = {};
unmountCleanupCalled = false;
constructor(element) {
configureWebCSSAnimations();
this.element = element;
}
update(animationProperties) {
if (!animationProperties) {
this.detach();
return;
}
const {
animationName: definitions,
...animationSettings
} = convertPropertiesToArrays(animationProperties);
if (definitions.length === 0) {
this.detach();
return;
}
const timestamp = Date.now();
const processedAnimations = definitions.map(definition => {
let processedAnimation;
// If the CSSKeyframesRule instance was provided, we can just use it
if (isCSSKeyframesRuleImpl(definition)) {
processedAnimation = this.attachedAnimations[definition.processedKeyframes] ?? {
keyframesRule: definition,
removable: false,
creationTimestamp: timestamp
};
} else {
// If keyframes was defined as an object, the additional processing is needed
const keyframes = definition;
const processedKeyframes = processKeyframeDefinitions(keyframes);
// If the animation with the same keyframes was already attached, we can reuse it
// Otherwise, we need to create a new CSSKeyframesRule object
processedAnimation = this.attachedAnimations[processedKeyframes] ?? {
keyframesRule: new CSSKeyframesRuleImpl(keyframes, processedKeyframes),
removable: true,
creationTimestamp: timestamp
};
}
if (this.unmountCleanupCalled) {
// unmountCleanup is called not only when the component truly unmounts, but also
// when display property is set to 'none' (e.g. during navigation between screens)
// In such a case, we don't want to restart the animation after re-entering the
// screen so we have to shift its delay by the time elapsed since the animation
// was started for the first time.
processedAnimation.elapsedTime = timestamp - processedAnimation.creationTimestamp;
}
return processedAnimation;
});
this.unmountCleanupCalled = false;
this.updateAttachedAnimations(processedAnimations);
this.setElementAnimations(processedAnimations, animationSettings);
}
unmountCleanup() {
if (!this.unmountCleanupCalled) {
this.unmountCleanupCalled = true;
// We use setTimeout to ensure that the animation is removed after the
// component is unmounted (it puts the detach call at the end of the event loop)
// We just remove the animation definition from the style sheet as there is no
// need to clean up view props if it is removed from the DOM.
setTimeout(() => {
this.removeAnimationsFromStyleSheet(Object.values(this.attachedAnimations));
});
}
}
detach() {
const attachedAnimations = Object.values(this.attachedAnimations);
if (attachedAnimations.length === 0) {
return;
}
this.element.style.animationDuration = '';
this.element.style.animationDelay = '';
this.element.style.animationDirection = '';
this.element.style.animationFillMode = '';
this.element.style.animationPlayState = '';
this.element.style.animationTimingFunction = '';
this.removeAnimationsFromStyleSheet(attachedAnimations);
this.unmountCleanupCalled = false;
this.attachedAnimations = {};
}
updateAttachedAnimations(processedAnimations) {
const newAttachedAnimations = {};
processedAnimations.forEach(processedAnimation => {
const rule = processedAnimation.keyframesRule;
if (rule.processedKeyframes) {
// We always call insert as it will insert animation only if it doesn't exist
insertCSSAnimation(rule.name, rule.processedKeyframes);
}
newAttachedAnimations[rule.processedKeyframes] = processedAnimation;
});
Object.values(this.attachedAnimations).forEach(({
keyframesRule: rule,
removable
}) => {
if (removable && rule.processedKeyframes && !newAttachedAnimations[rule.processedKeyframes]) {
removeCSSAnimation(rule.name);
}
});
this.attachedAnimations = newAttachedAnimations;
}
setElementAnimations(processedAnimations, animationSettings) {
this.element.style.animationName = processedAnimations.map(({
keyframesRule: {
name
}
}) => name).join(',');
this.element.style.animationDuration = maybeAddSuffixes(animationSettings, 'animationDuration', 'ms').join(',');
const animationDelays = animationSettings.animationDelay ?? [];
this.element.style.animationDelay = processedAnimations.map(({
elapsedTime
}, i) => {
const providedDelay = animationDelays[i] ?? 0;
return maybeAddSuffix(elapsedTime ? (normalizeTimeUnit(providedDelay) ?? 0) - elapsedTime : providedDelay, 'ms');
}).join(',');
if (animationSettings.animationIterationCount) {
this.element.style.animationIterationCount = animationSettings.animationIterationCount.join(',');
}
if (animationSettings.animationDirection) {
this.element.style.animationDirection = animationSettings.animationDirection.map(kebabizeCamelCase).join(',');
}
if (animationSettings.animationFillMode) {
this.element.style.animationFillMode = animationSettings.animationFillMode.join(',');
}
if (animationSettings.animationPlayState) {
this.element.style.animationPlayState = animationSettings.animationPlayState.join(',');
}
if (animationSettings.animationTimingFunction) {
this.element.style.animationTimingFunction = parseTimingFunction(animationSettings.animationTimingFunction);
}
}
removeAnimationsFromStyleSheet(animations) {
animations.forEach(({
keyframesRule: {
name,
processedKeyframes
},
removable
}) => {
if (removable && processedKeyframes) {
removeCSSAnimation(name);
}
});
}
}
//# sourceMappingURL=CSSAnimationsManager.js.map