import Animated, { clockRunning, not, startClock, stopClock } from 'react-native-reanimated'; import { isWeb } from './constants'; const { set, cond, add, sub, block, eq, neq, and, divide, greaterThan, greaterOrEq, Value, spring, lessThan, lessOrEq, multiply } = Animated; if (!Animated.proc) { throw new Error('Incompatible Reanimated version (proc not found)'); } // clock procs don't seem to work in web, not sure if there's a perf benefit to web procs anyway? const proc = isWeb ? (cb: T) => cb : Animated.proc; export const getIsAfterActive = proc((currentIndex: Animated.Node, activeIndex: Animated.Node) => greaterThan(currentIndex, activeIndex), ); export const hardReset = proc( (position: Animated.Value, finished: Animated.Value, time: Animated.Value, toValue: Animated.Value) => block([set(position, 0), set(finished, 0), set(time, 0), set(toValue, 0)]), ); /** * The in react-native-reanimated.d.ts definition of `proc` only has generics * for up to 10 arguments. We cast it to accept any params to avoid errors when * type-checking. */ type RetypedProc = (cb: (...params: any) => Animated.Node) => typeof cb; export const setupCell = proc( ( currentIndex: Animated.Value, size: Animated.Node, offset: Animated.Node, isAfterActive: Animated.Value, prevToValue: Animated.Value, prevSpacerIndex: Animated.Value, activeIndex: Animated.Node, activeCellSize: Animated.Node, hoverOffset: Animated.Node, spacerIndex: Animated.Value, toValue: Animated.Value, position: Animated.Value, time: Animated.Value, finished: Animated.Value, runSpring: Animated.Node, onFinished: Animated.Node, isDraggingCell: Animated.Node, placeholderOffset: Animated.Value, prevIsDraggingCell: Animated.Value, clock: Animated.Clock, disabled: Animated.Node, ) => block([ cond( greaterThan(activeIndex, -1), [ // Only update spacer if touch is not disabled. // Fixes android bugs where state would update with invalid touch values on touch end. cond(not(disabled), [ // Determine whether this cell is after the active cell in the list set(isAfterActive, getIsAfterActive(currentIndex, activeIndex)), // Determining spacer index is hard to visualize, see diagram: https://i.imgur.com/jRPf5t3.jpg cond( isAfterActive, [ cond( and( greaterOrEq(add(hoverOffset, activeCellSize), offset), lessThan(add(hoverOffset, activeCellSize), add(offset, divide(size, 2))), ), set(spacerIndex, sub(currentIndex, 1)), ), cond( and( greaterOrEq(add(hoverOffset, activeCellSize), add(offset, divide(size, 2))), lessThan(add(hoverOffset, activeCellSize), add(offset, size)), ), set(spacerIndex, currentIndex), ), ], cond(lessThan(currentIndex, activeIndex), [ cond( and(lessThan(hoverOffset, add(offset, size)), greaterOrEq(hoverOffset, add(offset, divide(size, 2)))), set(spacerIndex, add(currentIndex, 1)), ), cond( and(greaterOrEq(hoverOffset, offset), lessThan(hoverOffset, add(offset, divide(size, 2)))), set(spacerIndex, currentIndex), ), ]), ), // Set placeholder offset cond(eq(spacerIndex, currentIndex), [ set(placeholderOffset, cond(isAfterActive, add(sub(offset, activeCellSize), size), offset)), ]), ]), cond( eq(currentIndex, activeIndex), [ // If this cell is the active cell cond( isDraggingCell, [ // Set its position to the drag position set(position, sub(hoverOffset, offset)), ], [ // Active item, not pressed in // Set value hovering element will snap to once released cond(prevIsDraggingCell, [ set(toValue, sub(placeholderOffset, offset)), // The clock starts automatically when toValue changes, however, we need to handle the // case where the item should snap back to its original location and toValue doesn't change cond(eq(prevToValue, toValue), [ cond(clockRunning(clock), stopClock(clock)), set(time, 0), set(finished, 0), startClock(clock), ]), ]), ], ), ], [ // Not the active item // Translate cell down if it is before active index and active cell has passed it. // Translate cell up if it is after the active index and active cell has passed it. set( toValue, cond( cond(isAfterActive, lessOrEq(currentIndex, spacerIndex), greaterOrEq(currentIndex, spacerIndex)), cond(isAfterActive, multiply(activeCellSize, -1), activeCellSize), 0, ), ), ], ), // If this cell should animate somewhere new, reset its state and start its clock cond(neq(toValue, prevToValue), [cond(clockRunning(clock), stopClock(clock)), set(time, 0), set(finished, 0), startClock(clock)]), cond(neq(prevSpacerIndex, spacerIndex), [ cond(eq(spacerIndex, -1), [ // Hard reset to prevent stale state bugs cond(clockRunning(clock), stopClock(clock)), hardReset(position, finished, time, toValue), ]), ]), cond(finished, [onFinished, set(time, 0), set(finished, 0)]), set(prevSpacerIndex, spacerIndex), set(prevToValue, toValue), set(prevIsDraggingCell, isDraggingCell), cond(clockRunning(clock), runSpring), ], [ // // Reset the spacer index when drag ends cond(neq(spacerIndex, -1), set(spacerIndex, -1)), cond(neq(position, 0), set(position, 0)), ], ), position, ]), ); const betterSpring = (proc as RetypedProc)( ( finished: Animated.Value, velocity: Animated.Value, position: Animated.Value, time: Animated.Value, prevPosition: Animated.Value, toValue: Animated.Value, damping: Animated.Value, mass: Animated.Value, stiffness: Animated.Value, overshootClamping: Animated.SpringConfig['overshootClamping'], restSpeedThreshold: Animated.Value, restDisplacementThreshold: Animated.Value, clock: Animated.Clock, ) => spring( clock, { finished, velocity, position, time, // @ts-ignore -- https://github.com/software-mansion/react-native-reanimated/blob/master/src/animations/spring.js#L177 prevPosition, }, { toValue, damping, mass, stiffness, overshootClamping, restDisplacementThreshold, restSpeedThreshold, }, ), ); export function springFill(clock: Animated.Clock, state: Animated.SpringState, config: Animated.SpringConfig) { return betterSpring( state.finished, state.velocity, state.position, state.time, new Value(0), config.toValue, config.damping, config.mass, config.stiffness, config.overshootClamping, config.restSpeedThreshold, config.restDisplacementThreshold, clock, ); }