react-native-draggable-button
Version:
A simple React Native draggable button with functionalities to add to your code.
252 lines (225 loc) • 8.65 kB
JavaScript
import { useRef, useEffect, useCallback } from 'react';
import { Dimensions } from 'react-native';
import { PanResponder } from 'react-native';
import { GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
ReduceMotion,
} from 'react-native-reanimated';
const INITIAL_POSITION = "initial-position";
const NONE = "none";
const CLOSEST_AXIS_X = "closest-axis-x";
const CLOSEST_AXIS_Y = "closest-axis-y";
const CLOSEST_AXIS = "closest-axis";
export const DraggableButton = ({
onArrangeEnd = null,
onArrangeInit = null,
gesture = null,
scaleCustomConfig = null,
dragCustomConfig = null,
returnCustomSpringConfig = null,
returnMode = INITIAL_POSITION,
initialPosition,
children,
canMove = true,
blockDragX = false,
blockDragY = false,
animateButton = false,
maxDistance = 0,
minDistance = 0,
style = {},
}) => {
const { width, height } = Dimensions.get('window');
const initialPositionRef = useRef({ x: initialPosition.x, y: initialPosition.y });
const position = {
x: useSharedValue(initialPosition.x),
y: useSharedValue(initialPosition.y)
};
// Sync the internal position with the initial position prop
useEffect(() => {
initialPositionRef.current = { x: initialPosition.x, y: initialPosition.y };
position.x.value = withSpring(initialPosition.x, dragSpringConfig);
position.y.value = withSpring(initialPosition.y, dragSpringConfig);
}, [initialPosition]);
const scale = useSharedValue(1);
const dimensions = useRef({ width: 0, height: 0 });
const returnSpringConfig = returnCustomSpringConfig || {
duration: 1000,
dampingRatio: 0.7,
stiffness: 100,
overshootClamping: false,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 2,
reduceMotion: ReduceMotion.System,
};
const dragSpringConfig = dragCustomConfig || {
duration: 150,
dampingRatio: 0.7,
stiffness: 100,
};
const scaleSpringConfig = scaleCustomConfig || {
duration: 150,
dampingRatio: 0.7,
stiffness: 100,
};
/* Pan responder and drag handlers */
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderStart: (_, gestureState) => {
if (!!onArrangeInit)
onArrangeInit();
},
onPanResponderMove: (_, gestureState) => {
if (canMove)
onDrag(gestureState);
},
onPanResponderEnd: (_, gestureState) => {
onDragRelease(gestureState);
},
})
).current;
const getNewPosition = useCallback((gestureState) => {
const { moveX, moveY, dx, dy } = gestureState;
const { width, height } = dimensions.current;
let result = { x: moveX - (width * scale.value) / 2, y: moveY - (height * scale.value) / 2 };
if (maxDistance !== 0) {
// Calculate the distance from the initial position
const distanceX = result.x - initialPositionRef.current.x;
const distanceY = result.y - initialPositionRef.current.y;
// Limit the movement in the x direction
if (Math.abs(distanceX) > maxDistance) {
result.x = initialPositionRef.current.x + (distanceX > 0 ? maxDistance : -maxDistance);
}
// Limit the movement in the y direction
if (Math.abs(distanceY) > maxDistance) {
result.y = initialPositionRef.current.y + (distanceY > 0 ? maxDistance : -maxDistance);
}
}
return result;
}, [dimensions, maxDistance, scale]);
// handle drag function
const onDrag = useCallback((gestureState) => {
if (scale.value === 1.0 && animateButton)
scale.value = withSpring(1.5, scaleSpringConfig);
const newPos = getNewPosition(gestureState);
if (!blockDragX)
position.x.value = withSpring(newPos.x, dragSpringConfig);
if (!blockDragY)
position.y.value = withSpring(newPos.y, dragSpringConfig);
}, [position, getNewPosition, scale, animateButton, blockDragX, blockDragY, scaleSpringConfig]);
// end drag function
const onDragRelease = useCallback((gestureState) => {
// send signal to create new object in panel
const newPos = getNewPosition(gestureState);
let movedEnough = minDistance == 0;
// Check if the new position is different enough from the initial position
if (minDistance !== 0) {
// Calculate the distance from the initial position
const distanceX = Math.abs(position.x.value) - Math.abs(initialPositionRef.current.x);
const distanceY = Math.abs(position.y.value) - Math.abs(initialPositionRef.current.y);
// Limit the movement in the direction
// console.log("DistanceX", distanceX, "DistanceY", distanceY, "MinLimitDistance", minLimitDistance);
movedEnough = Math.abs(distanceX) > minDistance || Math.abs(distanceY) > minDistance;
}
// if moved enough, call onArrangeEnd function
if (!!onArrangeEnd && movedEnough)
onArrangeEnd(newPos.x, newPos.y);
// Reset position
if (returnMode === INITIAL_POSITION) {
position.x.value = withSpring(initialPositionRef.current.x, returnSpringConfig);
position.y.value = withSpring(initialPositionRef.current.y, returnSpringConfig);
}
else if (returnMode === NONE) {
// do nothing
}
if (returnMode === CLOSEST_AXIS_X) {
// return to the closes border window in x axis and preserve y axis
// calc the distance to check if the button is closer to 0 or width
if (position.x.value > width / 2) {
position.x.value = withSpring(width - dimensions.current.width, returnSpringConfig);
}
else {
position.x.value = withSpring(0, returnSpringConfig);
}
}
if (returnMode === CLOSEST_AXIS_Y) {
// return to the closes border window in y axis and preserve x axis
// calc the distance to check if the button is closer to 0 or height
if (position.y.value > height / 2) {
position.y.value = withSpring(height - dimensions.current.height, returnSpringConfig);
}
else {
position.y.value = withSpring(0, returnSpringConfig);
}
}
if (returnMode === CLOSEST_AXIS) {
// return the closest border window in x or y axis, will go only to the closest axis
// calc the distance to check if the button is closer to 0 or width
// calc diff among 0 and position x
let x0 = Math.abs(position.x.value);
let x1 = Math.abs(width - dimensions.current.width - position.x.value);
let y0 = Math.abs(position.y.value);
let y1 = Math.abs(height - dimensions.current.height - position.y.value);
// the lowest distance will be the closest axis
let xDistance = Math.min(x0, x1);
let yDistance = Math.min(y0, y1);
if (xDistance < yDistance) {
// return to the closes border window in x axis and preserve y axis
if (position.x.value > width / 2) {
position.x.value = withSpring(width - dimensions.current.width, returnSpringConfig);
}
else {
position.x.value = withSpring(0, returnSpringConfig);
}
}
else {
// return to the closes border window in y axis and preserve x axis
if (position.y.value > height / 2) {
position.y.value = withSpring(height - dimensions.current.height, returnSpringConfig);
}
else {
position.y.value = withSpring(0, returnSpringConfig);
}
}
}
// Reset scale
if (animateButton)
scale.value = withSpring(1, scaleSpringConfig);
}, [position, getNewPosition, initialPositionRef, maxDistance, onArrangeEnd, animateButton, returnSpringConfig, scaleSpringConfig]);
// animated style
const dragAnimatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: position?.x.value },
{ translateY: position?.y.value },
{ scaleX: scale.value },
{ scaleY: scale.value },
],
position: 'absolute',
}));
return (
<Animated.View
style={[dragAnimatedStyle, style]}
{...panResponder.panHandlers}
onLayout={(event) => {
const { width, height } = event.nativeEvent.layout;
dimensions.current = { width, height };
}}>
{/* tap gesture */}
{!!gesture && (
<GestureHandlerRootView>
<GestureDetector
gesture={gesture}
style={[{ position: "absolute" }]}>
{children}
</GestureDetector>
</GestureHandlerRootView>
)}
{/* non tap gesture */}
{!gesture && children}
</Animated.View>
);
};