UNPKG

react-native-animated-content-scroll

Version:

Animated content scroll component for React Native with directional slide-in animations

250 lines (237 loc) 6.63 kB
"use strict"; import { useEffect, useRef, useState, useCallback } from 'react'; import { Animated, FlatList } from 'react-native'; import { jsx as _jsx } from "react/jsx-runtime"; export function AnimatedListContainer({ items, renderItem, direction = 'right', distance = 50, duration = 500, margin = 5, keyExtractor = item => item.id }) { const [internalItems, setInternalItems] = useState(items.map(item => ({ id: keyExtractor(item), data: item, isExiting: false, hasEntryAnimated: false }))); // Detectar cambios en los items useEffect(() => { const currentIds = items.map(keyExtractor); setInternalItems(prevInternal => { let hasChanges = false; const newInternal = prevInternal.map(internalItem => { // Marcar items que ya no están en la lista como "saliendo" if (!currentIds.includes(internalItem.id) && !internalItem.isExiting) { hasChanges = true; return { ...internalItem, isExiting: true }; } return internalItem; }); // Agregar nuevos items items.forEach(item => { const itemId = keyExtractor(item); const existingIndex = newInternal.findIndex(internal => internal.id === itemId); if (existingIndex === -1) { hasChanges = true; newInternal.push({ id: itemId, data: item, isExiting: false, hasEntryAnimated: false }); } else if (existingIndex >= 0) { // Actualizar datos del item existente (por si cambió algo) const existing = newInternal[existingIndex]; if (existing && JSON.stringify(existing.data) !== JSON.stringify(item)) { hasChanges = true; newInternal[existingIndex] = { id: existing.id, data: item, isExiting: existing.isExiting, hasEntryAnimated: existing.hasEntryAnimated }; } } }); // Solo retornar nuevo array si hubo cambios return hasChanges ? newInternal : prevInternal; }); }, [items, keyExtractor]); const handleExitComplete = useCallback(itemId => { setInternalItems(prev => prev.filter(item => item.id !== itemId)); }, []); const handleEntryAnimated = useCallback(itemId => { setInternalItems(prev => prev.map(item => item.id === itemId ? { ...item, hasEntryAnimated: true } : item)); }, []); return /*#__PURE__*/_jsx(FlatList, { data: internalItems, keyExtractor: item => item.id.toString(), renderItem: ({ item, index }) => /*#__PURE__*/_jsx(AnimatedItem, { item: item, index: index, direction: direction, distance: distance, duration: duration, margin: margin, onExitComplete: handleExitComplete, onEntryAnimated: handleEntryAnimated, renderItem: renderItem }) }); } // Componente interno para cada item animado function AnimatedItem({ item, index, direction, distance, duration, margin, onExitComplete, onEntryAnimated, renderItem }) { const opacity = useRef(new Animated.Value(0)).current; const translateX = useRef(new Animated.Value(0)).current; const translateY = useRef(new Animated.Value(0)).current; // Animación de entrada useEffect(() => { if (!item.hasEntryAnimated && !item.isExiting) { // Configurar valores iniciales const initialValues = getInitialValues(direction, distance); translateX.setValue(initialValues.x); translateY.setValue(initialValues.y); opacity.setValue(0); // Ejecutar animación de entrada const animation = Animated.parallel([Animated.timing(opacity, { toValue: 1, duration, useNativeDriver: true, delay: index * 20 }), Animated.timing(translateX, { toValue: 0, duration, useNativeDriver: true, delay: index * 20 }), Animated.timing(translateY, { toValue: 0, duration, useNativeDriver: true, delay: index * 20 })]); animation.start(() => { onEntryAnimated(item.id); }); return () => { animation.stop(); }; } // Retornar función vacía si no se ejecuta la animación return () => {}; }, [item.hasEntryAnimated, item.isExiting, opacity, translateX, translateY, direction, distance, duration, index, item.id, onEntryAnimated]); // Animación de salida useEffect(() => { if (item.isExiting) { const exitAnimation = Animated.parallel([Animated.timing(opacity, { toValue: 0, duration: duration * 0.8, useNativeDriver: true }), Animated.timing(translateX, { toValue: 100, // Siempre sale hacia la derecha duration: duration * 0.8, useNativeDriver: true })]); exitAnimation.start(finished => { if (finished) { onExitComplete(item.id); } }); return () => { exitAnimation.stop(); }; } // Retornar función vacía si no se ejecuta la animación return () => {}; }, [item.isExiting, opacity, translateX, duration, item.id, onExitComplete]); // Calcular márgenes const getContainerMargins = (direction, margin) => { switch (direction) { case 'left': return { marginLeft: margin }; case 'right': return { marginRight: margin }; case 'top': case 'bottom': return {}; default: return {}; } }; return /*#__PURE__*/_jsx(Animated.View, { style: { overflow: 'visible', position: 'relative', ...getContainerMargins(direction, margin) }, children: /*#__PURE__*/_jsx(Animated.View, { style: { opacity, position: 'relative', transform: [{ translateX }, { translateY }] }, children: renderItem(item.data, index) }) }); } // Función auxiliar para obtener valores iniciales según la dirección function getInitialValues(direction, distance) { switch (direction) { case 'left': return { x: -distance, y: 0 }; case 'right': return { x: distance, y: 0 }; case 'top': return { x: 0, y: -distance }; case 'bottom': return { x: 0, y: distance }; default: return { x: 0, y: 0 }; } } //# sourceMappingURL=AnimatedListContainer.js.map