Skip to content

Commit dac417e

Browse files
authored
feat: Make drag activation timings and drop animation duration customizable (#226)
## Description This PR exposes props changing the delay and duration of the item drag activation. It also adds a separate prop for dragged item drop animation duration customization (when the active item is released and animated to its target position). This PR also fixes bug that something occurred when an item was added to the sortable component after initial render and the user started dragging this item before any other layout change. In this case, the item drop animation was immediate, which was an invalid behavior.
1 parent fe329bd commit dac417e

File tree

15 files changed

+164
-77
lines changed

15 files changed

+164
-77
lines changed

packages/react-native-sortables/src/components/shared/DraggableView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export default function DraggableView({
101101
<GestureDetector gesture={gesture}>
102102
<ItemDecoration
103103
isBeingActivated={isBeingActivated}
104+
itemKey={key}
104105
pressProgress={pressProgress}
105106
// Keep onLayout the closest to the children to measure the real item size
106107
// (without paddings or other style changes made to the wrapper component)

packages/react-native-sortables/src/components/shared/DropIndicator.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function DropIndicator({ DropIndicatorComponent, style }: DropIndicatorProps) {
4141

4242
const dropIndex = useSharedValue(0);
4343
const dropPosition = useSharedValue<Vector>({ x: 0, y: 0 });
44-
const prevTouchedItemKey = useSharedValue<null | string>(null);
44+
const prevUpdateItemKey = useSharedValue<null | string>(null);
4545

4646
const x = useSharedValue<null | number>(null);
4747
const y = useSharedValue<null | number>(null);
@@ -59,7 +59,7 @@ function DropIndicator({ DropIndicatorComponent, style }: DropIndicatorProps) {
5959
dropPosition.value = positions[key] ?? { x: 0, y: 0 };
6060

6161
const update = (target: SharedValue<null | number>, value: number) => {
62-
if (target.value === null || prevTouchedItemKey.value === null) {
62+
if (target.value === null || prevUpdateItemKey.value === null) {
6363
target.value = value;
6464
} else {
6565
target.value = withTiming(value, {
@@ -74,7 +74,7 @@ function DropIndicator({ DropIndicatorComponent, style }: DropIndicatorProps) {
7474
x.value = null;
7575
y.value = null;
7676
}
77-
prevTouchedItemKey.value = key;
77+
prevUpdateItemKey.value = key;
7878
}
7979
);
8080

packages/react-native-sortables/src/components/shared/ItemDecoration.tsx

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import Animated, {
55
interpolate,
66
interpolateColor,
77
useAnimatedStyle,
8-
useDerivedValue
8+
useDerivedValue,
9+
withTiming
910
} from 'react-native-reanimated';
1011

1112
import { useCommonValuesContext } from '../../providers';
@@ -14,54 +15,69 @@ type ItemDecorationProps = {
1415
isBeingActivated: SharedValue<boolean>;
1516
pressProgress: SharedValue<number>;
1617
onLayout?: ViewProps['onLayout'];
18+
itemKey: string;
1719
} & ViewProps;
1820

1921
export default function ItemDecoration({
2022
isBeingActivated,
23+
itemKey: key,
2124
pressProgress,
2225
...rest
2326
}: ItemDecorationProps) {
2427
const {
2528
activeItemOpacity,
2629
activeItemScale,
2730
activeItemShadowOpacity,
31+
dragActivationDuration,
2832
inactiveAnimationProgress,
2933
inactiveItemOpacity,
3034
inactiveItemScale,
31-
itemsStyleOverride
35+
itemsStyleOverride,
36+
prevTouchedItemKey
3237
} = useCommonValuesContext();
3338

34-
const resultingProgress = useDerivedValue(() =>
35-
isBeingActivated.value || pressProgress.value > 0
36-
? pressProgress.value
37-
: -inactiveAnimationProgress.value
38-
);
39+
const adjustedInactiveProgress = useDerivedValue(() => {
40+
if (isBeingActivated.value || prevTouchedItemKey.value === key) {
41+
return withTiming(0, { duration: dragActivationDuration.value });
42+
}
43+
44+
return interpolate(
45+
pressProgress.value,
46+
[0, 1],
47+
[inactiveAnimationProgress.value, 0]
48+
);
49+
});
3950

4051
const animatedStyle = useAnimatedStyle(() => {
41-
const progress = resultingProgress.value;
52+
const progress = pressProgress.value;
53+
const zeroProgressOpacity = interpolate(
54+
adjustedInactiveProgress.value,
55+
[0, 1],
56+
[1, inactiveItemOpacity.value]
57+
);
58+
const zeroProgressScale = interpolate(
59+
adjustedInactiveProgress.value,
60+
[0, 1],
61+
[1, inactiveItemScale.value]
62+
);
4263

4364
return {
4465
opacity: interpolate(
4566
progress,
46-
[-1, 0, 1],
47-
[inactiveItemOpacity.value, 1, activeItemOpacity.value]
67+
[0, 1],
68+
[zeroProgressOpacity, activeItemOpacity.value]
4869
),
4970
shadowColor: interpolateColor(
5071
progress,
51-
[-1, 0, 1],
52-
[
53-
'transparent',
54-
'transparent',
55-
`rgba(0, 0, 0, ${activeItemShadowOpacity.value})`
56-
]
72+
[0, 1],
73+
['transparent', `rgba(0, 0, 0, ${activeItemShadowOpacity.value})`]
5774
),
58-
shadowOpacity: interpolate(progress, [-1, 0, 1], [0, 0, 1]),
5975
transform: [
6076
{
6177
scale: interpolate(
6278
progress,
63-
[-1, 0, 1],
64-
[inactiveItemScale.value, 1, activeItemScale.value]
79+
[0, 1],
80+
[zeroProgressScale, activeItemScale.value]
6581
)
6682
}
6783
],
@@ -79,6 +95,7 @@ const styles = StyleSheet.create({
7995
height: 0,
8096
width: 0
8197
},
98+
shadowOpacity: 1,
8299
shadowRadius: 5
83100
}
84101
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export const OFFSET_EPS = 1;
2-
export const ACTIVATION_FAIL_OFFSET = 20;
2+
export const DRAG_ACTIVATION_FAIL_OFFSET = 20;
33
export const EXTRA_SWAP_OFFSET = 5;

packages/react-native-sortables/src/constants/layoutAnimations.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
/* eslint-disable import/no-unused-modules */
21
import { type LayoutAnimation, withTiming } from 'react-native-reanimated';
32

4-
import { ITEM_ANIMATION_DURATION } from './timings';
3+
import { ITEM_LAYOUT_ANIMATION_DURATION } from './timings';
54

65
export const SortableItemExiting = (): LayoutAnimation => {
76
'worklet';
87
const animations = {
98
opacity: withTiming(0, {
10-
duration: ITEM_ANIMATION_DURATION
9+
duration: ITEM_LAYOUT_ANIMATION_DURATION
1110
}),
1211
transform: [
1312
{
1413
scale: withTiming(0.5, {
15-
duration: ITEM_ANIMATION_DURATION
14+
duration: ITEM_LAYOUT_ANIMATION_DURATION
1615
})
1716
}
1817
]
@@ -31,12 +30,12 @@ export const SortableItemEntering = (): LayoutAnimation => {
3130
'worklet';
3231
const animations = {
3332
opacity: withTiming(1, {
34-
duration: ITEM_ANIMATION_DURATION
33+
duration: ITEM_LAYOUT_ANIMATION_DURATION
3534
}),
3635
transform: [
3736
{
3837
scale: withTiming(1, {
39-
duration: ITEM_ANIMATION_DURATION
38+
duration: ITEM_LAYOUT_ANIMATION_DURATION
4039
})
4140
}
4241
]

packages/react-native-sortables/src/constants/props.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import type {
99
SortableGridProps
1010
} from '../types';
1111
import { defaultKeyExtractor } from '../utils/keys';
12+
import { DRAG_ACTIVATION_FAIL_OFFSET } from './layout';
1213
import { SortableItemEntering, SortableItemExiting } from './layoutAnimations';
14+
import { DRAG_ACTIVATION_DELAY, DRAG_ANIMATION_DURATION } from './timings';
1315

1416
export const STYLE_PROPS = ['dropIndicatorStyle'] as const;
1517

@@ -32,6 +34,10 @@ export const DEFAULT_SHARED_PROPS = {
3234
autoScrollEnabled: true,
3335
autoScrollSpeed: 1,
3436
debug: false,
37+
dragActivationDelay: DRAG_ACTIVATION_DELAY,
38+
dragActivationDuration: DRAG_ANIMATION_DURATION,
39+
dragActivationFailOffset: DRAG_ACTIVATION_FAIL_OFFSET,
40+
dropAnimationDuration: DRAG_ANIMATION_DURATION,
3541
dropIndicatorStyle: {
3642
backgroundColor: 'rgba(0, 0, 0, 0.1)',
3743
borderColor: 'black',
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const ITEM_ANIMATION_DURATION = 300;
2-
export const TIME_TO_ACTIVATE_PAN = 500;
3-
export const ACTIVATE_PAN_ANIMATION_DELAY = 250;
1+
export const DRAG_ANIMATION_DURATION = 300;
2+
export const ITEM_LAYOUT_ANIMATION_DURATION = 300;
3+
export const DRAG_ACTIVATION_DELAY = 200;

packages/react-native-sortables/src/providers/SharedProvider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
ActiveItemSnapSettings,
1111
Animatable,
1212
AutoScrollSettings,
13+
ItemActivationSettings,
1314
PartialBy,
1415
SortableCallbacks
1516
} from '../types';
@@ -33,6 +34,7 @@ type SharedProviderProps = PropsWithChildren<
3334
} & ActiveItemDecorationSettings &
3435
ActiveItemSnapSettings &
3536
PartialBy<AutoScrollSettings, 'scrollableRef'> &
37+
Required<ItemActivationSettings> &
3638
Required<SortableCallbacks>
3739
>;
3840

packages/react-native-sortables/src/providers/shared/CommonValuesProvider.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
Animatable,
1515
CommonValuesContextType,
1616
Dimensions,
17+
ItemActivationSettings,
1718
Maybe,
1819
Vector
1920
} from '../../types';
@@ -27,7 +28,8 @@ type CommonValuesProviderProps = PropsWithChildren<
2728
itemKeys: Array<string>;
2829
initialItemsStyleOverride?: ViewStyle;
2930
} & ActiveItemDecorationSettings &
30-
ActiveItemSnapSettings
31+
ActiveItemSnapSettings &
32+
ItemActivationSettings
3133
>;
3234

3335
const { CommonValuesProvider, useCommonValuesContext } = createProvider(
@@ -36,6 +38,10 @@ const { CommonValuesProvider, useCommonValuesContext } = createProvider(
3638
activeItemOpacity: _activeItemOpacity,
3739
activeItemScale: _activeItemScale,
3840
activeItemShadowOpacity: _activeItemShadowOpacity,
41+
dragActivationDelay: _dragActivationDelay,
42+
dragActivationDuration: _dragActivationDuration,
43+
dragActivationFailOffset: _dragActivationFailOffset,
44+
dropAnimationDuration: _dropAnimationDuration,
3945
enableActiveItemSnap: _enableActiveItemSnap,
4046
inactiveItemOpacity: _inactiveItemOpacity,
4147
inactiveItemScale: _inactiveItemScale,
@@ -53,7 +59,7 @@ const { CommonValuesProvider, useCommonValuesContext } = createProvider(
5359
Object.fromEntries(indexToKey.value.map((key, index) => [key, index]))
5460
);
5561

56-
// POSITIONs
62+
// POSITIONS
5763
const itemPositions = useSharedValue<Record<string, Vector>>({});
5864
const touchPosition = useSharedValue<Vector | null>(null);
5965
const touchedItemPosition = useSharedValue<Vector | null>(null);
@@ -70,12 +76,21 @@ const { CommonValuesProvider, useCommonValuesContext } = createProvider(
7076

7177
// DRAG STATE
7278
const touchedItemKey = useSharedValue<null | string>(null);
79+
const prevTouchedItemKey = useSharedValue<null | string>(null);
7380
const activeItemKey = useSharedValue<null | string>(null);
7481
const activationState = useSharedValue(DragActivationState.INACTIVE);
7582
const activationProgress = useSharedValue(0);
7683
const inactiveAnimationProgress = useSharedValue(0);
7784
const activeItemDropped = useSharedValue(true);
7885

86+
// ITEM ACTIVATION SETTINGS
87+
const dragActivationDelay = useAnimatableValue(_dragActivationDelay);
88+
const dragActivationDuration = useAnimatableValue(_dragActivationDuration);
89+
const dragActivationFailOffset = useAnimatableValue(
90+
_dragActivationFailOffset
91+
);
92+
const dropAnimationDuration = useAnimatableValue(_dropAnimationDuration);
93+
7994
// ACTIVE ITEM DECORATION
8095
const activeItemOpacity = useAnimatableValue(_activeItemOpacity);
8196
const activeItemScale = useAnimatableValue(_activeItemScale);
@@ -113,6 +128,10 @@ const { CommonValuesProvider, useCommonValuesContext } = createProvider(
113128
containerHeight,
114129
containerRef,
115130
containerWidth,
131+
dragActivationDelay,
132+
dragActivationDuration,
133+
dragActivationFailOffset,
134+
dropAnimationDuration,
116135
enableActiveItemSnap,
117136
inactiveAnimationProgress,
118137
inactiveItemOpacity,
@@ -122,6 +141,7 @@ const { CommonValuesProvider, useCommonValuesContext } = createProvider(
122141
itemPositions,
123142
itemsStyleOverride,
124143
keyToIndex,
144+
prevTouchedItemKey,
125145
snapOffsetX,
126146
snapOffsetY,
127147
sortEnabled,

0 commit comments

Comments
 (0)