react-native-ui-lib
Version:
[](https://stand-with-ukraine.pp.ua)
172 lines (168 loc) • 6.23 kB
JavaScript
import _map from "lodash/map";
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useContext } from 'react';
import { useSharedValue, useAnimatedReaction, withTiming, Easing, runOnJS, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import View from "../view";
import { Shadows, Colors } from "../../style";
import { useDidUpdate } from "../../hooks";
import SortableListContext from "./SortableListContext";
import usePresenter from "./usePresenter";
import { HapticService, HapticType } from "../../services";
import { StyleUtils } from "../../utils";
export const DEFAULT_LIST_ITEM_SIZE = 52;
const animationConfig = {
easing: Easing.inOut(Easing.ease),
duration: 350
};
// Reanimated 3 - Solving the following error:
// ReanimatedError: Trying to access property `$backgroundDefault` of an object which cannot be sent to the UI runtime., js engine: reanimated
const LIST_ITEM_BACKGROUND = Colors.$backgroundDefault;
const SortableListItem = props => {
const {
children,
index
} = props;
const {
data,
itemSize,
horizontal,
itemProps,
onItemLayout,
itemsOrder,
lockedIds,
onChange,
enableHaptic,
scale: propsScale = 1
} = useContext(SortableListContext);
const {
getTranslationByIndexChange,
getItemIndexById,
getIndexByPosition,
getIdByItemIndex
} = usePresenter();
const id = data[index].id;
const locked = data[index].locked ?? false;
const initialIndex = useSharedValue(_map(data, 'id').indexOf(id));
const lastSwap = useSharedValue({
from: -1,
to: -1
});
const currIndex = useSharedValue(initialIndex.value);
const translation = useSharedValue(0);
const isDragging = useSharedValue(false);
const draggedItemShadow = useSharedValue(StyleUtils.unpackStyle({
...Shadows.sh30.bottom,
...Shadows.sh30.top
}));
const defaultItemShadow = useSharedValue(StyleUtils.unpackStyle({
shadowColor: Colors.transparent,
elevation: 0
}));
const tempTranslation = useSharedValue(0);
const tempItemsOrder = useSharedValue(itemsOrder.value);
useDidUpdate(() => {
const newItemIndex = _map(data, 'id').indexOf(id);
initialIndex.value = newItemIndex;
currIndex.value = newItemIndex;
translation.value = 0;
}, [data]);
useAnimatedReaction(() => getItemIndexById(itemsOrder.value, id), (newIndex, prevIndex) => {
if (prevIndex === null || newIndex === prevIndex) {
return;
}
currIndex.value = newIndex;
if (!isDragging.value) {
const _translation = getTranslationByIndexChange(currIndex.value, initialIndex.value, itemSize.value);
translation.value = withTiming(_translation, animationConfig);
}
}, []);
const dragOnLongPressGesture = Gesture.Pan().activateAfterLongPress(250).enabled(!locked).onStart(() => {
isDragging.value = true;
translation.value = getTranslationByIndexChange(currIndex.value, initialIndex.value, itemSize.value);
lastSwap.value = {
...lastSwap.value,
from: currIndex.value
};
tempTranslation.value = translation.value;
tempItemsOrder.value = itemsOrder.value;
}).onTouchesMove(() => {
if (enableHaptic && !isDragging.value) {
runOnJS(HapticService.triggerHaptic)(HapticType.selection, 'SortableList');
}
}).onUpdate(event => {
const {
translationX,
translationY
} = event;
const _translation = horizontal ? translationX : translationY;
translation.value = tempTranslation.value + _translation;
// Swapping items
let newIndex = getIndexByPosition(translation.value, itemSize.value) + initialIndex.value;
const oldIndex = getItemIndexById(itemsOrder.value, id);
if (newIndex !== oldIndex) {
// Sometimes getIndexByPosition will give an index that is off by one because of rounding error (floor\ceil does not help)
if (Math.abs(newIndex - oldIndex) > 1) {
newIndex = Math.sign(newIndex - oldIndex) + oldIndex;
}
let itemIdToSwap = getIdByItemIndex(itemsOrder.value, newIndex);
// Skip locked item(s)
while (lockedIds.value[itemIdToSwap]) {
const skipDirection = Math.sign(newIndex - oldIndex);
newIndex = skipDirection + newIndex;
itemIdToSwap = getIdByItemIndex(itemsOrder.value, newIndex);
}
// Swap items
if (itemIdToSwap !== undefined) {
const newItemsOrder = [...itemsOrder.value];
newItemsOrder[newIndex] = id;
newItemsOrder[oldIndex] = itemIdToSwap;
itemsOrder.value = newItemsOrder;
lastSwap.value = {
...lastSwap.value,
to: newIndex
};
}
}
}).onEnd(() => {
const _translation = getTranslationByIndexChange(getItemIndexById(itemsOrder.value, id), getItemIndexById(tempItemsOrder.value, id), itemSize.value);
translation.value = withTiming(tempTranslation.value + _translation, animationConfig, () => {
if (tempItemsOrder.value.toString() !== itemsOrder.value.toString()) {
runOnJS(onChange)({
...lastSwap.value
});
}
});
}).onFinalize(() => {
if (isDragging.value) {
isDragging.value = false;
}
});
const draggedAnimatedStyle = useAnimatedStyle(() => {
const scale = withSpring(isDragging.value ? propsScale : 1);
const zIndex = isDragging.value ? 100 : withTiming(0, animationConfig);
const opacity = isDragging.value ? 0.95 : 1;
const shadow = isDragging.value ? draggedItemShadow.value : defaultItemShadow.value;
return {
backgroundColor: itemProps?.backgroundColor ?? LIST_ITEM_BACKGROUND,
// required for elevation to work in Android
zIndex,
transform: [horizontal ? {
translateX: translation.value
} : {
translateY: translation.value
}, {
scale
}],
opacity,
...itemProps?.margins,
...shadow
};
});
return <GestureDetector gesture={dragOnLongPressGesture}>
<View reanimated style={draggedAnimatedStyle} onLayout={index === 0 ? onItemLayout : undefined}>
{children}
</View>
</GestureDetector>;
};
export default React.memo(SortableListItem);