UNPKG

react-native-reanimated

Version:

More powerful alternative to Animated library for React Native.

229 lines (196 loc) • 7.32 kB
'use strict'; import type { ShadowNodeWrapper } from '../../../commonTypes'; import type { CSSAnimationKeyframes, ExistingCSSAnimationProperties, ICSSAnimationsManager, } from '../../types'; import { cssKeyframesRegistry, CSSKeyframesRuleImpl } from '../keyframes'; import { createSingleCSSAnimationProperties, getAnimationSettingsUpdates, normalizeSingleCSSAnimationSettings, } from '../normalization'; import { applyCSSAnimations, unregisterCSSAnimations } from '../proxy'; import type { CSSAnimationUpdates, NormalizedSingleCSSAnimationSettings, } from '../types'; type ProcessedAnimation = { normalizedSettings: NormalizedSingleCSSAnimationSettings; keyframesRule: CSSKeyframesRuleImpl; }; export default class CSSAnimationsManager implements ICSSAnimationsManager { private readonly shadowNodeWrapper: ShadowNodeWrapper; private readonly viewName: string; private readonly viewTag: number; private attachedAnimations: ProcessedAnimation[] = []; constructor( shadowNodeWrapper: ShadowNodeWrapper, viewName: string, viewTag: number ) { this.shadowNodeWrapper = shadowNodeWrapper; this.viewName = viewName; this.viewTag = viewTag; } update(animationProperties: ExistingCSSAnimationProperties | null): void { if (!animationProperties) { this.detach(); return; } const processedAnimations = this.processAnimations(animationProperties); this.registerKeyframesUsage(processedAnimations); const animationUpdates = this.getAnimationUpdates(processedAnimations); this.attachedAnimations = processedAnimations; if (animationUpdates) { if ( animationUpdates.animationNames && animationUpdates.animationNames.length === 0 ) { this.detach(); return; } applyCSSAnimations(this.shadowNodeWrapper, animationUpdates); } } unmountCleanup(): void { this.unregisterKeyframesUsage(); } private detach() { if (this.attachedAnimations.length > 0) { unregisterCSSAnimations(this.viewTag); this.unregisterKeyframesUsage(); this.attachedAnimations = []; } } private registerKeyframesUsage(processedAnimations: ProcessedAnimation[]) { const newAnimationNames = new Set(); // Register keyframes for all new animations processedAnimations.forEach(({ keyframesRule }) => { cssKeyframesRegistry.add(keyframesRule, this.viewName, this.viewTag); newAnimationNames.add(keyframesRule.name); }); // Unregister keyframes for all old animations that are no longer attached // to the view this.attachedAnimations.forEach(({ keyframesRule: { name } }) => { if (!newAnimationNames.has(name)) { cssKeyframesRegistry.remove(name, this.viewName, this.viewTag); } }); } private unregisterKeyframesUsage() { // Unregister keyframes usage by the view (it is necessary to clean up // keyframes from the CPP registry once all views that use them are unmounted) this.attachedAnimations.forEach(({ keyframesRule: { name } }) => { cssKeyframesRegistry.remove(name, this.viewName, this.viewTag); }); } private processAnimations( animationProperties: ExistingCSSAnimationProperties ): ProcessedAnimation[] { const singleAnimationPropertiesArray = createSingleCSSAnimationProperties(animationProperties); const processedAnimations = singleAnimationPropertiesArray.map( (properties) => { const keyframes = properties.animationName; let keyframesRule: CSSKeyframesRuleImpl; if (keyframes instanceof CSSKeyframesRuleImpl) { // If the instance of the CSSKeyframesRule class was passed, we can just compare // references to the instance (css.keyframes() call should be memoized in order // to preserve the same animation. If used inline, it will restart the animation // on every component re-render) keyframesRule = keyframes; } else { // If the keyframes are not an instance of the CSSKeyframesRule class (e.g. someone // passes a keyframes object inline in the component's style without using css.keyframes() // function), we don't want to restart the animation on every component re-render. // In this case, we need to check if the animation with the same keyframes is already // registered in the registry. If it is, we can just use the existing keyframes rule. // Otherwise, we need to create a new keyframes rule. const cssText = JSON.stringify(keyframes); keyframesRule = cssKeyframesRegistry.get(cssText) ?? new CSSKeyframesRuleImpl( keyframes as CSSAnimationKeyframes, cssText ); } return { normalizedSettings: normalizeSingleCSSAnimationSettings(properties), keyframesRule, }; } ); return processedAnimations; } private buildAnimationsMap(animations: ProcessedAnimation[]) { // Iterate over attached animations from last to first for faster pop from // the end of the array when removing used animations return animations.reduceRight<Record<string, ProcessedAnimation[]>>( (acc, animation) => { const name = animation.keyframesRule.name; if (!acc[name]) { acc[name] = [animation]; } else { acc[name].push(animation); } return acc; }, {} ); } private getAnimationUpdates( processedAnimations: ProcessedAnimation[] ): CSSAnimationUpdates | null { const newAnimationSettings: Record< number, NormalizedSingleCSSAnimationSettings > = {}; const settingsUpdates: Record< number, Partial<NormalizedSingleCSSAnimationSettings> > = {}; let animationsArrayChanged = this.attachedAnimations.length !== processedAnimations.length; let hasNewAnimations = false; let hasSettingsUpdates = false; const oldAnimations = this.buildAnimationsMap(this.attachedAnimations); processedAnimations.forEach(({ keyframesRule, normalizedSettings }, i) => { const oldAnimation = oldAnimations[keyframesRule.name]?.pop(); if (!oldAnimation) { hasNewAnimations = true; animationsArrayChanged = true; newAnimationSettings[i] = normalizedSettings; return; } const updates = getAnimationSettingsUpdates( oldAnimation.normalizedSettings, normalizedSettings ); if (Object.keys(updates).length > 0) { hasSettingsUpdates = true; settingsUpdates[i] = updates; } if (oldAnimation.keyframesRule.name !== keyframesRule.name) { animationsArrayChanged = true; } }); const result: CSSAnimationUpdates = {}; if (animationsArrayChanged) { result.animationNames = processedAnimations.map( ({ keyframesRule }) => keyframesRule.name ); } if (hasNewAnimations) { result.newAnimationSettings = newAnimationSettings; } if (hasSettingsUpdates) { result.settingsUpdates = settingsUpdates; } if (hasNewAnimations || hasSettingsUpdates || animationsArrayChanged) { return result; } return null; } }