react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
202 lines (196 loc) • 7.96 kB
JavaScript
import { Animations, TransitionType, WebEasings } from './config';
import { TransitionGenerator } from './createAnimation';
import { scheduleAnimationCleanup } from './domUtils';
import { _updatePropsJS } from '../../js-reanimated';
import { ReduceMotion } from '../../commonTypes';
import { isReducedMotion } from '../../PlatformChecker';
import { LayoutAnimationType } from '../animationBuilder/commonTypes';
import { setDummyPosition, snapshots } from './componentStyle';
function getEasingFromConfig(config) {
const easingName = config.easingV && config.easingV.name in WebEasings ? config.easingV.name : 'linear';
return `cubic-bezier(${WebEasings[easingName].toString()})`;
}
function getRandomDelay(maxDelay = 1000) {
return Math.floor(Math.random() * (maxDelay + 1)) / 1000;
}
function getDelayFromConfig(config) {
const shouldRandomizeDelay = config.randomizeDelay;
const delay = shouldRandomizeDelay ? getRandomDelay() : 0;
if (!config.delayV) {
return delay;
}
return shouldRandomizeDelay ? getRandomDelay(config.delayV) : config.delayV / 1000;
}
export function getReducedMotionFromConfig(config) {
if (!config.reduceMotionV) {
return isReducedMotion();
}
switch (config.reduceMotionV) {
case ReduceMotion.Never:
return false;
case ReduceMotion.Always:
return true;
default:
return isReducedMotion();
}
}
function getDurationFromConfig(config, isLayoutTransition, animationName) {
const defaultDuration = isLayoutTransition ? 0.3 : Animations[animationName].duration;
return config.durationV !== undefined ? config.durationV / 1000 : defaultDuration;
}
function getCallbackFromConfig(config) {
return config.callbackV !== undefined ? config.callbackV : null;
}
function getReversedFromConfig(config) {
return !!config.reversed;
}
export function getProcessedConfig(animationName, animationType, config, initialAnimationName) {
return {
animationName,
animationType,
duration: getDurationFromConfig(config, animationType === LayoutAnimationType.LAYOUT, initialAnimationName),
delay: getDelayFromConfig(config),
easing: getEasingFromConfig(config),
callback: getCallbackFromConfig(config),
reversed: getReversedFromConfig(config)
};
}
export function saveSnapshot(element) {
const rect = element.getBoundingClientRect();
const snapshot = {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
scrollOffsets: getElementScrollValue(element)
};
snapshots.set(element, snapshot);
}
export function setElementAnimation(element, animationConfig) {
const {
animationName,
duration,
delay,
easing
} = animationConfig;
element.style.animationName = animationName;
element.style.animationDuration = `${duration}s`;
element.style.animationDelay = `${delay}s`;
element.style.animationTimingFunction = easing;
element.onanimationend = () => {
var _animationConfig$call;
(_animationConfig$call = animationConfig.callback) === null || _animationConfig$call === void 0 || _animationConfig$call.call(animationConfig, true);
element.removeEventListener('animationcancel', animationCancelHandler);
};
const animationCancelHandler = () => {
var _animationConfig$call2;
(_animationConfig$call2 = animationConfig.callback) === null || _animationConfig$call2 === void 0 || _animationConfig$call2.call(animationConfig, false);
element.removeEventListener('animationcancel', animationCancelHandler);
};
// Here we have to use `addEventListener` since element.onanimationcancel doesn't work on chrome
element.onanimationstart = () => {
if (animationConfig.animationType === LayoutAnimationType.ENTERING) {
_updatePropsJS({
visibility: 'initial'
}, {
_component: element
});
}
element.addEventListener('animationcancel', animationCancelHandler);
};
if (!(animationName in Animations)) {
scheduleAnimationCleanup(animationName, duration + delay);
}
}
export function handleLayoutTransition(element, animationConfig, transitionData) {
const {
animationName
} = animationConfig;
let animationType;
switch (animationName) {
case 'LinearTransition':
animationType = TransitionType.LINEAR;
break;
case 'SequencedTransition':
animationType = TransitionType.SEQUENCED;
break;
case 'FadingTransition':
animationType = TransitionType.FADING;
break;
default:
animationType = TransitionType.LINEAR;
break;
}
animationConfig.animationName = TransitionGenerator(animationType, transitionData);
setElementAnimation(element, animationConfig);
}
function getElementScrollValue(element) {
let current = element;
const scrollOffsets = {
scrollTopOffset: 0,
scrollLeftOffset: 0
};
while (current) {
if (current.scrollTop !== 0 && scrollOffsets.scrollTopOffset === 0) {
scrollOffsets.scrollTopOffset = current.scrollTop;
}
if (current.scrollLeft !== 0 && scrollOffsets.scrollLeftOffset === 0) {
scrollOffsets.scrollLeftOffset = current.scrollLeft;
}
current = current.parentElement;
}
return scrollOffsets;
}
export function handleExitingAnimation(element, animationConfig) {
const parent = element.offsetParent;
const dummy = element.cloneNode();
dummy.reanimatedDummy = true;
element.style.animationName = '';
// We hide current element so only its copy with proper animation will be displayed
element.style.visibility = 'hidden';
// After cloning the element, we want to move all children from original element to its clone. This is because original element
// will be unmounted, therefore when this code executes in child component, parent will be either empty or removed soon.
// Using element.cloneNode(true) doesn't solve the problem, because it creates copy of children and we won't be able to set their animations
//
// This loop works because appendChild() moves element into its new parent instead of copying it
while (element.firstChild) {
dummy.appendChild(element.firstChild);
}
setElementAnimation(dummy, animationConfig);
parent === null || parent === void 0 || parent.appendChild(dummy);
const snapshot = snapshots.get(element);
const scrollOffsets = getElementScrollValue(element);
// Scroll does not trigger snapshotting, therefore if we start exiting animation after
// scrolling through parent component, dummy will end up in wrong place. In order to fix that
// we keep last known scroll position in snapshot and then adjust dummy position based on
// last known scroll offset and current scroll offset
const currentScrollTopOffset = scrollOffsets.scrollTopOffset;
const lastScrollTopOffset = snapshot.scrollOffsets.scrollTopOffset;
if (currentScrollTopOffset !== lastScrollTopOffset) {
snapshot.top += lastScrollTopOffset - currentScrollTopOffset;
}
const currentScrollLeftOffset = scrollOffsets.scrollLeftOffset;
const lastScrollLeftOffset = snapshot.scrollOffsets.scrollLeftOffset;
if (currentScrollLeftOffset !== lastScrollLeftOffset) {
snapshot.left += lastScrollLeftOffset - currentScrollLeftOffset;
}
snapshots.set(dummy, snapshot);
setDummyPosition(dummy, snapshot);
const originalOnAnimationEnd = dummy.onanimationend;
dummy.onanimationend = function (event) {
if (parent !== null && parent !== void 0 && parent.contains(dummy)) {
dummy.removedAfterAnimation = true;
parent.removeChild(dummy);
}
// Given that this function overrides onAnimationEnd, it won't be null
originalOnAnimationEnd === null || originalOnAnimationEnd === void 0 || originalOnAnimationEnd.call(this, event);
};
dummy.addEventListener('animationcancel', () => {
if (parent !== null && parent !== void 0 && parent.contains(dummy)) {
dummy.removedAfterAnimation = true;
parent.removeChild(dummy);
}
});
}
//# sourceMappingURL=componentUtils.js.map
;