UNPKG

react-native-reanimated

Version:

More powerful alternative to Animated library for React Native.

162 lines (157 loc) • 6.71 kB
'use strict'; import { ReanimatedError } from "../../errors.js"; import { logger } from "../../logger/index.js"; import { isWindowAvailable } from "../../PlatformChecker.js"; import { setElementPosition, snapshots } from "./componentStyle.js"; import { Animations } from "./config.js"; const PREDEFINED_WEB_ANIMATIONS_ID = 'ReanimatedPredefinedWebAnimationsStyle'; const CUSTOM_WEB_ANIMATIONS_ID = 'ReanimatedCustomWebAnimationsStyle'; // Since we cannot remove keyframe from DOM by its name, we have to store its id const animationNameToIndex = new Map(); const animationNameList = []; let isObserverSet = false; /** * Creates `HTMLStyleElement`, inserts it into DOM and then inserts CSS rules * into the stylesheet. If style element already exists, nothing happens. */ export function configureWebLayoutAnimations() { if (!isWindowAvailable() || // Without this check SSR crashes because document is undefined (NextExample on CI) document.getElementById(PREDEFINED_WEB_ANIMATIONS_ID) !== null) { return; } const predefinedAnimationsStyleTag = document.createElement('style'); predefinedAnimationsStyleTag.id = PREDEFINED_WEB_ANIMATIONS_ID; predefinedAnimationsStyleTag.onload = () => { if (!predefinedAnimationsStyleTag.sheet) { logger.error('Failed to create layout animations stylesheet.'); return; } for (const animationName in Animations) { predefinedAnimationsStyleTag.sheet.insertRule(Animations[animationName].style); } }; const customAnimationsStyleTag = document.createElement('style'); customAnimationsStyleTag.id = CUSTOM_WEB_ANIMATIONS_ID; document.head.appendChild(predefinedAnimationsStyleTag); document.head.appendChild(customAnimationsStyleTag); } export function insertWebAnimation(animationName, keyframe) { // Without this check SSR crashes because document is undefined (NextExample on CI) if (!isWindowAvailable()) { return; } const styleTag = document.getElementById(CUSTOM_WEB_ANIMATIONS_ID); if (!styleTag.sheet) { logger.error('Failed to create layout animations stylesheet.'); return; } styleTag.sheet.insertRule(keyframe, 0); animationNameList.unshift(animationName); animationNameToIndex.set(animationName, 0); for (let i = 1; i < animationNameList.length; ++i) { const nextAnimationName = animationNameList[i]; const nextAnimationIndex = animationNameToIndex.get(nextAnimationName); if (nextAnimationIndex === undefined) { throw new ReanimatedError('Failed to obtain animation index.'); } animationNameToIndex.set(animationNameList[i], nextAnimationIndex + 1); } } function removeWebAnimation(animationName, animationRemoveCallback) { // Without this check SSR crashes because document is undefined (NextExample on CI) if (!isWindowAvailable()) { return; } const styleTag = document.getElementById(CUSTOM_WEB_ANIMATIONS_ID); const currentAnimationIndex = animationNameToIndex.get(animationName); if (currentAnimationIndex === undefined) { throw new ReanimatedError('Failed to obtain animation index.'); } animationRemoveCallback(); styleTag.sheet?.deleteRule(currentAnimationIndex); animationNameList.splice(currentAnimationIndex, 1); animationNameToIndex.delete(animationName); for (let i = currentAnimationIndex; i < animationNameList.length; ++i) { const nextAnimationName = animationNameList[i]; const nextAnimationIndex = animationNameToIndex.get(nextAnimationName); if (nextAnimationIndex === undefined) { throw new ReanimatedError('Failed to obtain animation index.'); } animationNameToIndex.set(animationNameList[i], nextAnimationIndex - 1); } } const timeoutScale = 1.25; // We use this value to enlarge timeout duration. It can prove useful if animation lags. const frameDurationMs = 16; // Just an approximation. const minimumFrames = 10; export function scheduleAnimationCleanup(animationName, animationDuration, animationRemoveCallback) { // If duration is very short, we want to keep remove delay to at least 10 frames // In our case it is exactly 160/1099 s, which is approximately 0.15s const timeoutValue = Math.max(animationDuration * timeoutScale * 1000, animationDuration + frameDurationMs * minimumFrames); setTimeout(() => removeWebAnimation(animationName, animationRemoveCallback), timeoutValue); } function reattachElementToAncestor(child, parent) { const childSnapshot = snapshots.get(child); if (!childSnapshot) { logger.error('Failed to obtain snapshot.'); return; } // We use that so we don't end up in infinite loop child.removedAfterAnimation = true; parent.appendChild(child); setElementPosition(child, childSnapshot); const originalOnAnimationEnd = child.onanimationend; child.onanimationend = function (event) { parent.removeChild(child); // Given that this function overrides onAnimationEnd, it won't be null originalOnAnimationEnd?.call(this, event); }; } function findDescendantWithExitingAnimation(node, root) { // Node could be something else than HTMLElement, for example TextNode (treated as plain text, not as HTML object), // therefore it won't have children prop and calling Array.from(node.children) will cause error. if (!(node instanceof HTMLElement)) { return; } if (node.reanimatedDummy && node.removedAfterAnimation === undefined) { reattachElementToAncestor(node, root); } const children = Array.from(node.children); for (let i = 0; i < children.length; ++i) { findDescendantWithExitingAnimation(children[i], root); } } function checkIfScreenWasChanged(mutationTarget) { let reactFiberKey = '__reactFiber'; for (const key of Object.keys(mutationTarget)) { if (key.startsWith('__reactFiber')) { reactFiberKey = key; break; } } return mutationTarget[reactFiberKey]?.child?.memoizedProps?.navigation !== undefined; } export function addHTMLMutationObserver() { if (isObserverSet || !isWindowAvailable()) { return; } isObserverSet = true; const observer = new MutationObserver(mutationsList => { const rootMutation = mutationsList[mutationsList.length - 1]; if (checkIfScreenWasChanged(rootMutation.target)) { return; } for (let i = 0; i < rootMutation.removedNodes.length; ++i) { findDescendantWithExitingAnimation(rootMutation.removedNodes[i], rootMutation.target); } }); observer.observe(document.body, { childList: true, subtree: true }); } export function areDOMRectsEqual(r1, r2) { // There are 4 more fields, but checking these should suffice return r1.x === r2.x && r1.y === r2.y && r1.width === r2.width && r1.height === r2.height; } //# sourceMappingURL=domUtils.js.map