155 lines
3.5 KiB
TypeScript
155 lines
3.5 KiB
TypeScript
import React, { useCallback } from 'react';
|
|
import { FlatList, FlatListProps, ViewStyle, RefreshControl } from 'react-native';
|
|
import Animated, {
|
|
useSharedValue,
|
|
useAnimatedStyle,
|
|
withTiming,
|
|
withDelay,
|
|
withSpring,
|
|
FadeIn,
|
|
SlideInRight,
|
|
Layout,
|
|
} from 'react-native-reanimated';
|
|
import { useTheme } from '../../theme/ThemeContext';
|
|
|
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
|
|
|
interface AnimatedListProps<T> extends Omit<FlatListProps<T>, 'renderItem'> {
|
|
renderItem: (info: { item: T; index: number }) => React.ReactElement;
|
|
/** Delay base entre items en ms */
|
|
staggerDelay?: number;
|
|
/** Tipo de animación de entrada */
|
|
animationType?: 'fade' | 'slide' | 'spring';
|
|
/** Mostrar animación al refrescar */
|
|
animateOnRefresh?: boolean;
|
|
/** Callback de refresh */
|
|
onRefresh?: () => Promise<void> | void;
|
|
/** Está refrescando */
|
|
isRefreshing?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Item animado wrapper
|
|
*/
|
|
function AnimatedItem({
|
|
children,
|
|
index,
|
|
staggerDelay,
|
|
animationType,
|
|
}: {
|
|
children: React.ReactNode;
|
|
index: number;
|
|
staggerDelay: number;
|
|
animationType: 'fade' | 'slide' | 'spring';
|
|
}) {
|
|
const delay = Math.min(index * staggerDelay, 500); // Cap máximo de delay
|
|
|
|
const entering = (() => {
|
|
switch (animationType) {
|
|
case 'slide':
|
|
return SlideInRight.delay(delay).duration(300);
|
|
case 'spring':
|
|
return FadeIn.delay(delay).springify();
|
|
case 'fade':
|
|
default:
|
|
return FadeIn.delay(delay).duration(300);
|
|
}
|
|
})();
|
|
|
|
return (
|
|
<Animated.View entering={entering} layout={Layout.springify()}>
|
|
{children}
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* FlatList con animaciones de entrada staggered
|
|
*/
|
|
export function AnimatedList<T>({
|
|
data,
|
|
renderItem,
|
|
staggerDelay = 50,
|
|
animationType = 'fade',
|
|
animateOnRefresh = true,
|
|
onRefresh,
|
|
isRefreshing = false,
|
|
...props
|
|
}: AnimatedListProps<T>) {
|
|
const { colors } = useTheme();
|
|
const [key, setKey] = React.useState(0);
|
|
|
|
const handleRefresh = useCallback(async () => {
|
|
if (onRefresh) {
|
|
await onRefresh();
|
|
if (animateOnRefresh) {
|
|
setKey((prev) => prev + 1);
|
|
}
|
|
}
|
|
}, [onRefresh, animateOnRefresh]);
|
|
|
|
const animatedRenderItem = useCallback(
|
|
({ item, index }: { item: T; index: number }) => (
|
|
<AnimatedItem
|
|
index={index}
|
|
staggerDelay={staggerDelay}
|
|
animationType={animationType}
|
|
>
|
|
{renderItem({ item, index })}
|
|
</AnimatedItem>
|
|
),
|
|
[renderItem, staggerDelay, animationType]
|
|
);
|
|
|
|
return (
|
|
<FlatList
|
|
key={key}
|
|
data={data}
|
|
renderItem={animatedRenderItem}
|
|
refreshControl={
|
|
onRefresh ? (
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={handleRefresh}
|
|
tintColor={colors.primary}
|
|
colors={[colors.primary]}
|
|
/>
|
|
) : undefined
|
|
}
|
|
{...(props as any)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook para crear estilos animados de item de lista
|
|
*/
|
|
export function useListItemEntering(index: number, baseDelay = 50) {
|
|
const delay = Math.min(index * baseDelay, 500);
|
|
|
|
return FadeIn.delay(delay).duration(300);
|
|
}
|
|
|
|
/**
|
|
* Componente para animar un item individual
|
|
*/
|
|
export function AnimatedListItem({
|
|
children,
|
|
index,
|
|
baseDelay = 50,
|
|
style,
|
|
}: {
|
|
children: React.ReactNode;
|
|
index: number;
|
|
baseDelay?: number;
|
|
style?: ViewStyle;
|
|
}) {
|
|
const entering = useListItemEntering(index, baseDelay);
|
|
|
|
return (
|
|
<Animated.View entering={entering} style={style}>
|
|
{children}
|
|
</Animated.View>
|
|
);
|
|
}
|