react-native-reanimated-dnd
Version:
A powerful drag-and-drop library for React Native using Reanimated 3
1 lines • 6.4 kB
JavaScript
import{useState,useRef,useEffect}from"react";import React from"react";import{runOnJS,runOnUI,useAnimatedGestureHandler,useAnimatedReaction,useAnimatedStyle,useDerivedValue,useSharedValue,withSpring,withTiming}from"react-native-reanimated";export var ScrollDirection;(function(ScrollDirection){ScrollDirection["None"]="none";ScrollDirection["Up"]="up";ScrollDirection["Down"]="down"})(ScrollDirection||(ScrollDirection={}));export function clamp(value,lowerBound,upperBound){"worklet";return Math.max(lowerBound,Math.min(value,upperBound))}export function objectMove(object,from,to){"worklet";const newObject=Object.assign({},object);const movedUp=to<from;for(const id in object){if(object[id]===from){newObject[id]=to;continue}const currentPosition=object[id];if(movedUp&¤tPosition>=to&¤tPosition<from){newObject[id]++}else if(currentPosition<=to&¤tPosition>from){newObject[id]--}}return newObject}export function listToObject(list){const values=Object.values(list);const object={};for(let i=0;i<values.length;i++){object[values[i].id]=i}return object}export function setPosition(positionY,itemsCount,positions,id,itemHeight){"worklet";const newPosition=clamp(Math.floor(positionY/itemHeight),0,itemsCount-1);if(newPosition!==positions.value[id]){positions.value=objectMove(positions.value,positions.value[id],newPosition)}}export function setAutoScroll(positionY,lowerBound,upperBound,scrollThreshold,autoScroll){"worklet";if(positionY<=lowerBound+scrollThreshold){autoScroll.value=ScrollDirection.Up}else if(positionY>=upperBound-scrollThreshold){autoScroll.value=ScrollDirection.Down}else{autoScroll.value=ScrollDirection.None}}export function useSortable(options){const{id,positions,lowerBound,autoScrollDirection,itemsCount,itemHeight,containerHeight=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 positionY=useSharedValue(0);const top=useSharedValue(0);const targetLowerBound=useSharedValue(0);useEffect((()=>{runOnUI((()=>{"worklet";const initialTopVal=positions.value[id]*itemHeight;const initialLowerBoundVal=lowerBound.value;top.value=initialTopVal;positionY.value=initialTopVal;targetLowerBound.value=initialLowerBoundVal}))()}),[]);const calculatedContainerHeight=useRef(containerHeight).current;const upperBound=useDerivedValue((()=>lowerBound.value+calculatedContainerHeight));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((()=>positionY.value),((currentY,previousY)=>{if(currentY===null||!movingSV.value){return}if(previousY!==null&¤tY===previousY){return}const clampedPosition=Math.min(Math.max(0,Math.ceil(currentY/itemHeight)),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(currentY));onDraggingLastCallTimestamp.value=now}}top.value=currentY;setPosition(currentY,itemsCount,positions,id,itemHeight);setAutoScroll(currentY,lowerBound.value,upperBound.value,itemHeight,autoScrollDirection)}),[movingSV,itemHeight,itemsCount,positions,id,onDragging,lowerBound,upperBound,autoScrollDirection,currentOverItemId,top,onDraggingLastCallTimestamp]);useAnimatedReaction((()=>positions.value[id]),((currentPosition,previousPosition)=>{if(currentPosition!==null&&previousPosition!==null&¤tPosition!==previousPosition){if(!movingSV.value){top.value=withSpring(currentPosition*itemHeight);if(onMove){runOnJS(onMove)(id,previousPosition,currentPosition)}}}}),[movingSV]);useAnimatedReaction((()=>autoScrollDirection.value),((scrollDirection,previousValue)=>{if(scrollDirection!==null&&previousValue!==null&&scrollDirection!==previousValue){switch(scrollDirection){case ScrollDirection.Up:{targetLowerBound.value=lowerBound.value;targetLowerBound.value=withTiming(0,{duration:1500});break}case ScrollDirection.Down:{const contentHeight=itemsCount*itemHeight;const maxScroll=contentHeight-calculatedContainerHeight;targetLowerBound.value=lowerBound.value;targetLowerBound.value=withTiming(maxScroll,{duration:1500});break}case ScrollDirection.None:{targetLowerBound.value=lowerBound.value;break}}}}));useAnimatedReaction((()=>targetLowerBound.value),((targetLowerBoundValue,previousValue)=>{if(targetLowerBoundValue!==null&&previousValue!==null&&targetLowerBoundValue!==previousValue){if(movingSV.value){lowerBound.value=targetLowerBoundValue}}}),[movingSV]);const panGestureHandler=useAnimatedGestureHandler({onStart(event,ctx){"worklet";ctx.initialItemContentY=positions.value[id]*itemHeight;ctx.initialFingerAbsoluteY=event.absoluteY;ctx.initialLowerBound=lowerBound.value;positionY.value=ctx.initialItemContentY;movingSV.value=true;runOnJS(setIsMoving)(true);if(onDragStart){runOnJS(onDragStart)(id,positions.value[id])}},onActive(event,ctx){"worklet";const fingerDyScreen=event.absoluteY-ctx.initialFingerAbsoluteY;const scrollDeltaSinceStart=lowerBound.value-ctx.initialLowerBound;positionY.value=ctx.initialItemContentY+fingerDyScreen+scrollDeltaSinceStart},onFinish(){"worklet";const finishPosition=positions.value[id]*itemHeight;top.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",left:0,right:0,top:top.value,zIndex:movingSV.value?1:0,backgroundColor:"#000000",shadowColor:"black",shadowOpacity:withSpring(movingSV.value?.2:0),shadowRadius:10}}),[movingSV]);return{animatedStyle,panGestureHandler,isMoving,hasHandle}}