diff --git a/demo/src/screens/componentScreens/SortableListScreen.tsx b/demo/src/screens/componentScreens/SortableListScreen.tsx
index cf5d696318..5657131926 100644
--- a/demo/src/screens/componentScreens/SortableListScreen.tsx
+++ b/demo/src/screens/componentScreens/SortableListScreen.tsx
@@ -28,7 +28,8 @@ const SortableListScreen = () => {
} else {
setSelectedItems(selectedItems.concat(item));
}
- }, [selectedItems, setSelectedItems]);
+ },
+ [selectedItems, setSelectedItems]);
const addItem = useCallback(() => {
if (removedItems.length > 0) {
@@ -74,17 +75,30 @@ const SortableListScreen = () => {
);
- }, [selectedItems, toggleItemSelection]);
+ },
+ [selectedItems, toggleItemSelection]);
return (
{renderHeader('Sortable List', {'margin-10': true})}
-
+
);
diff --git a/src/components/sortableList/SortableList.api.json b/src/components/sortableList/SortableList.api.json
index 45aa82a9ed..f60e69a096 100644
--- a/src/components/sortableList/SortableList.api.json
+++ b/src/components/sortableList/SortableList.api.json
@@ -22,6 +22,12 @@
"name": "enableHaptic",
"type": "boolean",
"description": "Whether to enable the haptic feedback.\n(please note that react-native-haptic-feedback does not support the specific haptic type on Android starting on an unknown version, you can use 1.8.2 for it to work properly)"
+ },
+ {
+ "name": "scale",
+ "type": "number",
+ "default": "1",
+ "description": "Scale the item once dragged."
}
],
"snippet": [
diff --git a/src/components/sortableList/SortableListContext.ts b/src/components/sortableList/SortableListContext.ts
index 6ab8477044..e2fad03eac 100644
--- a/src/components/sortableList/SortableListContext.ts
+++ b/src/components/sortableList/SortableListContext.ts
@@ -2,13 +2,14 @@ import {createContext} from 'react';
import {ViewProps} from 'react-native';
import {SharedValue} from 'react-native-reanimated';
-interface SortableListContextType {
+export interface SortableListContextType {
data: any
itemsOrder: SharedValue;
onChange: () => void;
itemHeight: SharedValue;
onItemLayout: ViewProps['onLayout'];
enableHaptic?: boolean;
+ scale?: number;
}
// @ts-ignore
diff --git a/src/components/sortableList/SortableListItem.tsx b/src/components/sortableList/SortableListItem.tsx
index 306eb647a8..88779a94c7 100644
--- a/src/components/sortableList/SortableListItem.tsx
+++ b/src/components/sortableList/SortableListItem.tsx
@@ -31,55 +31,54 @@ const animationConfig = {
const SortableListItem = (props: Props) => {
const {children, index} = props;
- const {data, itemHeight, onItemLayout, itemsOrder, onChange, enableHaptic} = useContext(SortableListContext);
+ const {
+ data,
+ itemHeight,
+ onItemLayout,
+ itemsOrder,
+ onChange,
+ enableHaptic,
+ scale: propsScale = 1
+ } = useContext(SortableListContext);
const {getTranslationByIndexChange, getItemIndexById, getIndexByPosition, getIdByItemIndex} = usePresenter();
const id: string = data[index].id;
const initialIndex = useSharedValue(map(data, 'id').indexOf(id));
+ const currIndex = useSharedValue(initialIndex.value);
const translateY = useSharedValue(0);
const isDragging = useSharedValue(false);
const tempTranslateY = useSharedValue(0);
const tempItemsOrder = useSharedValue(itemsOrder.value);
- const dataManuallyChanged = useSharedValue(false);
useDidUpdate(() => {
- dataManuallyChanged.value = true;
- initialIndex.value = map(data, 'id').indexOf(id);
+ const newItemIndex = map(data, 'id').indexOf(id);
+
+ initialIndex.value = newItemIndex;
+ currIndex.value = newItemIndex;
+
+ translateY.value = 0;
}, [data]);
- useAnimatedReaction(() => itemsOrder.value,
- (currItemsOrder, prevItemsOrder) => {
- // Note: Unfortunately itemsOrder sharedValue is being initialized on each render
- // Therefore I added this extra check here that compares current and previous values
- // See open issue: https://github.com/software-mansion/react-native-reanimated/issues/3224
- if (prevItemsOrder === null || currItemsOrder.join(',') === prevItemsOrder.join(',')) {
+ useAnimatedReaction(() => getItemIndexById(itemsOrder.value, id),
+ (newIndex, prevIndex) => {
+ if (prevIndex === null || newIndex === prevIndex) {
return;
- } else {
- const newIndex = getItemIndexById(currItemsOrder, id);
- const oldIndex = getItemIndexById(prevItemsOrder, id);
-
- /* In case the order of the item has returned back to its initial index we reset its position */
- if (newIndex === initialIndex.value) {
- /* Reset without an animation when the change is due to manual data change */
- if (dataManuallyChanged.value) {
- translateY.value = 0;
- dataManuallyChanged.value = false;
- /* Reset with an animation when the change id due to user reordering */
- } else {
- translateY.value = withTiming(0, animationConfig);
- }
- /* Handle an order change, animate item to its new position */
- } else if (newIndex !== oldIndex) {
- const translation = getTranslationByIndexChange(newIndex, oldIndex, itemHeight.value);
- translateY.value = withTiming(translateY.value + translation, animationConfig);
- }
}
- });
+
+ currIndex.value = newIndex;
+ if (!isDragging.value) {
+ const translation = getTranslationByIndexChange(currIndex.value, initialIndex.value, itemHeight.value);
+
+ translateY.value = withTiming(translation, animationConfig);
+ }
+ },
+ []);
const dragOnLongPressGesture = Gesture.Pan()
.activateAfterLongPress(250)
.onStart(() => {
isDragging.value = true;
+ translateY.value = getTranslationByIndexChange(currIndex.value, initialIndex.value, itemHeight.value);
tempTranslateY.value = translateY.value;
tempItemsOrder.value = itemsOrder.value;
})
@@ -92,10 +91,15 @@ const SortableListItem = (props: Props) => {
translateY.value = tempTranslateY.value + event.translationY;
// Swapping items
- const newIndex = getIndexByPosition(translateY.value, itemHeight.value) + initialIndex.value;
+ let newIndex = getIndexByPosition(translateY.value, itemHeight.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;
+ }
+
const itemIdToSwap = getIdByItemIndex(itemsOrder.value, newIndex);
if (itemIdToSwap !== undefined) {
@@ -124,7 +128,7 @@ const SortableListItem = (props: Props) => {
});
const draggedAnimatedStyle = useAnimatedStyle(() => {
- const scaleY = withSpring(isDragging.value ? 1.1 : 1);
+ 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
@@ -140,7 +144,7 @@ const SortableListItem = (props: Props) => {
return {
backgroundColor: Colors.$backgroundDefault, // required for elevation to work in Android
zIndex,
- transform: [{translateY: translateY.value}, {scaleY}],
+ transform: [{translateY: translateY.value}, {scale}],
opacity,
...shadow
};
diff --git a/src/components/sortableList/index.tsx b/src/components/sortableList/index.tsx
index a03b3d5eef..f64f9e02ce 100644
--- a/src/components/sortableList/index.tsx
+++ b/src/components/sortableList/index.tsx
@@ -4,15 +4,17 @@ import React, {useMemo, useCallback} from 'react';
import {FlatList, FlatListProps, LayoutChangeEvent} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
-import SortableListContext from './SortableListContext';
+import SortableListContext, {SortableListContextType} from './SortableListContext';
import SortableListItem from './SortableListItem';
-import {useDidUpdate} from 'hooks';
+import {useDidUpdate, useThemeProps} from 'hooks';
interface ItemWithId {
id: string;
}
-export interface SortableListProps extends Omit, 'extraData' | 'data'> {
+export interface SortableListProps
+ extends Omit, 'extraData' | 'data'>,
+ Pick {
/**
* The data of the list, do not update the data.
*/
@@ -33,7 +35,8 @@ function generateItemsOrder(data: SortableListProps(props: SortableListProps) => {
- const {data, onOrderChange, enableHaptic, ...others} = props;
+ const themeProps = useThemeProps(props, 'SortableList');
+ const {data, onOrderChange, enableHaptic, scale, ...others} = themeProps;
const itemsOrder = useSharedValue(generateItemsOrder(data));
const itemHeight = useSharedValue(52);
@@ -67,7 +70,8 @@ const SortableList = (props: SortableListProps)
onChange,
itemHeight,
onItemLayout,
- enableHaptic
+ enableHaptic,
+ scale
};
}, [data]);
diff --git a/src/incubator/TextField/Input.tsx b/src/incubator/TextField/Input.tsx
index 2551b6abb4..49f904b375 100644
--- a/src/incubator/TextField/Input.tsx
+++ b/src/incubator/TextField/Input.tsx
@@ -1,5 +1,5 @@
-import React, {useContext} from 'react';
-import {TextInput, StyleSheet, Platform} from 'react-native';
+import React, {useContext, useMemo} from 'react';
+import {TextInput as RNTextInput, StyleSheet, Platform} from 'react-native';
import {Constants, ForwardRefInjectedProps} from '../../commons/new';
import {InputProps, ColorType} from './types';
import {getColorByState} from './Presenter';
@@ -18,6 +18,7 @@ const Input = ({
color = DEFAULT_INPUT_COLOR,
forwardedRef,
formatter,
+ useGestureHandlerInput,
...props
}: InputProps & ForwardRefInjectedProps) => {
const inputRef = useImperativeInputHandle(forwardedRef, {onChangeText: props.onChangeText});
@@ -27,6 +28,17 @@ const Input = ({
const placeholderTextColor = getColorByState(props.placeholderTextColor, context);
const value = formatter && !context.isFocused ? formatter(props.value) : props.value;
+ const TextInput = useMemo(() => {
+ if (useGestureHandlerInput) {
+ const {
+ TextInput: GestureTextInput
+ }: typeof import('react-native-gesture-handler') = require('react-native-gesture-handler');
+ return GestureTextInput;
+ } else {
+ return RNTextInput;
+ }
+ }, [useGestureHandlerInput]);
+
return (
string | undefined",
"description": "Custom formatter for the input value (used only when input if not focused)"
+ },
+ {
+ "name": "useGestureHandlerInput",
+ "type": "boolean",
+ "description": "Use react-native-gesture-handler instead of react-native for the base TextInput"
}
],
"snippet": [
diff --git a/src/incubator/TextField/types.ts b/src/incubator/TextField/types.ts
index f09572f400..6d18dbb159 100644
--- a/src/incubator/TextField/types.ts
+++ b/src/incubator/TextField/types.ts
@@ -139,6 +139,10 @@ export interface InputProps
* Custom formatter for the input value (used only when input if not focused)
*/
formatter?: (value?: string) => string | undefined;
+ /**
+ * Use react-native-gesture-handler instead of react-native for the base TextInput
+ */
+ useGestureHandlerInput?: boolean;
}
export type TextFieldProps = MarginModifiers &