expo-image
Version:
A cross-platform, performant image component for React Native and Expo with Web support
155 lines • 5.73 kB
JavaScript
import React from 'react';
const SUPPORTED_ANIMATIONS = [
'cross-dissolve',
'flip-from-left',
'flip-from-right',
'flip-from-top',
'flip-from-bottom',
];
function useAnimationManagerNode(node, initialStatus) {
const newNode = React.useMemo(() => {
if (!node) {
return null;
}
const [animationKey, renderFunction] = node;
// key, ReactElement, ref, callbacks
return {
animationKey,
persistedElement: renderFunction,
status: (initialStatus || 'mounted'),
};
}, [node?.[0]]);
return newNode;
}
function validateTimingFunctionForAnimation(animationClass, timingFunction) {
if (animationClass?.includes('flip')) {
if (timingFunction?.includes('ease')) {
return 'ease-in-out';
}
return 'linear';
}
return timingFunction || null;
}
function validateAnimationClass(effect) {
if (SUPPORTED_ANIMATIONS.includes(effect)) {
return effect;
}
return 'cross-dissolve';
}
export function getAnimatorFromTransition(transition) {
if (!transition?.duration) {
return null;
}
const animationClass = validateAnimationClass(transition.effect);
if (!animationClass) {
return {
startingClass: '',
animateInClass: '',
animateOutClass: '',
containerClass: '',
timingFunction: 'linear',
animationClass: '',
duration: 0,
};
}
const timingFunction = validateTimingFunctionForAnimation(animationClass, transition.timing);
const timingClass = `image-timing-${timingFunction}`;
return {
startingClass: `${animationClass}-start`,
animateInClass: [animationClass, 'transitioning', `${animationClass}-active`, timingClass].join(' '),
animateOutClass: [animationClass, `${animationClass}-end`, timingClass].join(' '),
containerClass: `${animationClass}-container`,
timingFunction,
animationClass,
duration: transition?.duration || 0,
};
}
export default function AnimationManager({ children: renderFunction, initial, transition, recyclingKey, }) {
const animation = getAnimatorFromTransition(transition);
const initialNode = useAnimationManagerNode(initial, 'active');
const [nodes, setNodes] = React.useState(initialNode ? [initialNode] : []);
const [prevRecyclingKey, setPrevRecyclingKey] = React.useState(recyclingKey ?? '');
if (prevRecyclingKey !== (recyclingKey ?? '')) {
setPrevRecyclingKey(recyclingKey ?? '');
setNodes(initialNode ? [initialNode] : []);
}
const removeAllNodesOfKeyExceptShowing = (key) => {
setNodes((n) => n.filter((node) => (key ? node.animationKey !== key : false) ||
node.status === 'in' ||
node.status === 'active'));
};
const newNode = useAnimationManagerNode(renderFunction);
React.useEffect(() => {
setNodes((n) => {
if (!newNode) {
return n;
}
const existingNodeIndex = n.findIndex((node) => node.animationKey === newNode.animationKey);
if (existingNodeIndex >= 0) {
if (animation) {
return n.map((n2) => n2.animationKey === newNode.animationKey
? { ...newNode, status: 'in' }
: { ...n2, status: 'out' });
}
else {
return [{ ...newNode, status: 'in' }];
}
}
return [...n, newNode];
});
}, [newNode]);
function wrapNodeWithCallbacks(node) {
if (renderFunction[0] === node.animationKey) {
return renderFunction[1]({
onReady: () => {
if (animation) {
setNodes((nodes) => nodes.map((n) => (n === newNode ? { ...n, status: 'in' } : { ...n, status: 'out' })));
}
else {
setNodes([{ ...node, status: 'in' }]);
}
},
onAnimationFinished: () => {
setNodes([{ ...node, status: 'in' }]);
},
onError: () => {
setNodes((nodes) => nodes.map((n) => (n === node ? { ...n, status: 'errored' } : n)));
},
});
}
if (initial?.[0] === node.animationKey) {
return initial[1]({
onAnimationFinished: () => {
if (node.status === 'out') {
removeAllNodesOfKeyExceptShowing(node.animationKey);
}
},
onError: () => {
setNodes((nodes) => nodes.map((n) => (n === node ? { ...n, status: 'errored' } : n)));
},
});
}
return node.persistedElement({
onAnimationFinished: () => {
removeAllNodesOfKeyExceptShowing(node.animationKey);
},
});
}
const styles = {
transitionDuration: `${animation?.duration || 0}ms`,
transitionTimingFunction: animation?.timingFunction || 'linear',
};
const classes = {
in: animation?.animateInClass,
out: animation?.animateOutClass,
mounted: animation?.startingClass,
};
return (<>
{[...nodes]
.filter((n) => n.status !== 'errored')
.map((n) => (<div className={animation?.containerClass} key={n.animationKey}>
{wrapNodeWithCallbacks(n)(classes[n.status], styles)}
</div>))}
</>);
}
//# sourceMappingURL=AnimationManager.js.map