UNPKG

react-native-reanimated-dnd

Version:

A powerful drag-and-drop library for React Native using Reanimated 3

1 lines 5.65 kB
import{useState,useRef,useEffect}from"react";import React from"react";import{runOnJS,runOnUI,useAnimatedGestureHandler,useAnimatedReaction,useAnimatedStyle,useDerivedValue,useSharedValue,withSpring,withTiming}from"react-native-reanimated";import{setHorizontalPosition,setHorizontalAutoScroll,getItemXPosition,getContentWidth}from"../components/sortableUtils";import{HorizontalScrollDirection}from"../types/sortable";export function useHorizontalSortable(options){const{id,positions,leftBound,autoScrollDirection,itemsCount,itemWidth,gap=0,paddingHorizontal=0,containerWidth=500,onMove,onDragStart,onDrop,onDragging,children,handleComponent}=options;const[isMoving,setIsMoving]=useState(false);const[hasHandle,setHasHandle]=useState(false);const movingSV=useSharedValue(false);const currentOverItemId=useSharedValue(null);const onDraggingLastCallTimestamp=useSharedValue(0);const THROTTLE_INTERVAL=50;const positionX=useSharedValue(0);const left=useSharedValue(0);const targetLeftBound=useSharedValue(0);useEffect((()=>{runOnUI((()=>{"worklet";const initialLeftVal=getItemXPosition(positions.value[id],itemWidth,gap,paddingHorizontal);const initialLeftBoundVal=leftBound.value;left.value=initialLeftVal;positionX.value=initialLeftVal;targetLeftBound.value=initialLeftBoundVal}))()}),[]);const calculatedContainerWidth=useRef(containerWidth).current;const rightBound=useDerivedValue((()=>leftBound.value+calculatedContainerWidth));useEffect((()=>{if(!children||!handleComponent){setHasHandle(false);return}const checkForHandle=child=>{if(React.isValidElement(child)){if(child.type===handleComponent){return true}if(child.props&&child.props.children){if(React.Children.toArray(child.props.children).some(checkForHandle)){return true}}}return false};setHasHandle(React.Children.toArray(children).some(checkForHandle))}),[children,handleComponent]);useAnimatedReaction((()=>positionX.value),((currentX,previousX)=>{if(currentX===null||!movingSV.value){return}if(previousX!==null&&currentX===previousX){return}const adjustedX=currentX-paddingHorizontal;const itemWithGapWidth=itemWidth+gap;const clampedPosition=Math.min(Math.max(0,Math.ceil(adjustedX/itemWithGapWidth)),itemsCount-1);let newOverItemId=null;for(const[itemIdIter,itemPosIter]of Object.entries(positions.value)){if(itemPosIter===clampedPosition&&itemIdIter!==id){newOverItemId=itemIdIter;break}}if(currentOverItemId.value!==newOverItemId){currentOverItemId.value=newOverItemId}if(onDragging){const now=Date.now();if(now-onDraggingLastCallTimestamp.value>THROTTLE_INTERVAL){runOnJS(onDragging)(id,newOverItemId,Math.round(currentX));onDraggingLastCallTimestamp.value=now}}left.value=currentX;setHorizontalPosition(currentX,itemsCount,positions,id,itemWidth,gap,paddingHorizontal);setHorizontalAutoScroll(currentX,leftBound.value,rightBound.value,itemWidth,autoScrollDirection)}),[movingSV,itemWidth,gap,paddingHorizontal,itemsCount,positions,id,onDragging,leftBound,rightBound,autoScrollDirection,currentOverItemId,left,onDraggingLastCallTimestamp]);useAnimatedReaction((()=>positions.value[id]),((currentPosition,previousPosition)=>{if(currentPosition!==null&&previousPosition!==null&&currentPosition!==previousPosition){if(!movingSV.value){const newLeft=getItemXPosition(currentPosition,itemWidth,gap,paddingHorizontal);left.value=withSpring(newLeft);if(onMove){runOnJS(onMove)(id,previousPosition,currentPosition)}}}}),[movingSV,itemWidth,gap,paddingHorizontal]);useAnimatedReaction((()=>autoScrollDirection.value),((scrollDirection,previousValue)=>{if(scrollDirection!==null&&previousValue!==null&&scrollDirection!==previousValue){switch(scrollDirection){case HorizontalScrollDirection.Left:{targetLeftBound.value=leftBound.value;targetLeftBound.value=withTiming(0,{duration:1500});break}case HorizontalScrollDirection.Right:{const contentWidth=getContentWidth(itemsCount,itemWidth,gap,paddingHorizontal);const maxScroll=Math.max(0,contentWidth-calculatedContainerWidth);targetLeftBound.value=leftBound.value;targetLeftBound.value=withTiming(maxScroll,{duration:1500});break}case HorizontalScrollDirection.None:{targetLeftBound.value=leftBound.value;break}}}}));useAnimatedReaction((()=>targetLeftBound.value),((targetLeftBoundValue,previousValue)=>{if(targetLeftBoundValue!==null&&previousValue!==null&&targetLeftBoundValue!==previousValue){if(movingSV.value){leftBound.value=targetLeftBoundValue}}}),[movingSV]);const panGestureHandler=useAnimatedGestureHandler({onStart(event,ctx){"worklet";ctx.initialItemContentX=getItemXPosition(positions.value[id],itemWidth,gap,paddingHorizontal);ctx.initialFingerAbsoluteX=event.absoluteX;ctx.initialLeftBound=leftBound.value;positionX.value=ctx.initialItemContentX;movingSV.value=true;runOnJS(setIsMoving)(true);if(onDragStart){runOnJS(onDragStart)(id,positions.value[id])}},onActive(event,ctx){"worklet";const fingerDxScreen=event.absoluteX-ctx.initialFingerAbsoluteX;const scrollDeltaSinceStart=leftBound.value-ctx.initialLeftBound;positionX.value=ctx.initialItemContentX+fingerDxScreen+scrollDeltaSinceStart},onFinish(){"worklet";const finishPosition=getItemXPosition(positions.value[id],itemWidth,gap,paddingHorizontal);left.value=withTiming(finishPosition);movingSV.value=false;runOnJS(setIsMoving)(false);if(onDrop){runOnJS(onDrop)(id,positions.value[id])}currentOverItemId.value=null}});const animatedStyle=useAnimatedStyle((()=>{"worklet";return{position:"absolute",top:0,bottom:0,left:left.value,width:itemWidth,zIndex:movingSV.value?1:0,backgroundColor:"transparent",shadowColor:"black",shadowOpacity:withSpring(movingSV.value?.2:0),shadowRadius:10}}),[movingSV,itemWidth]);return{animatedStyle,panGestureHandler,isMoving,hasHandle}}