Skip to content

Commit 5066edd

Browse files
authored
perf: Merge item decoration styles with position styles (#407)
## Description This PR improves `ItemCell` component implementation by merging decoration animated styles and layout styles (which results in twice less animated views with custom props, so less commits and clones of ShadowNodes to apply reanimated changes). It also cleans up code a bit by separating item position calculation from animated styles hooks.
1 parent ace7037 commit 5066edd

16 files changed

Lines changed: 417 additions & 338 deletions

File tree

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

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { PropsWithChildren, ReactNode } from 'react';
22
import { Fragment, memo, useEffect, useState } from 'react';
3-
import { StyleSheet, type ViewStyle } from 'react-native';
43
import { GestureDetector } from 'react-native-gesture-handler';
54
import {
65
LayoutAnimationConfig,
@@ -12,9 +11,8 @@ import {
1211
CommonValuesContext,
1312
ItemContextProvider,
1413
useCommonValuesContext,
15-
useItemDecorationStyles,
16-
useItemLayoutStyles,
1714
useItemPanGesture,
15+
useItemStyles,
1816
useMeasurementsContext,
1917
usePortalContext
2018
} from '../../../providers';
@@ -51,12 +49,7 @@ function DraggableView({
5149
const [isTeleported, setIsTeleported] = useState(false);
5250
const activationAnimationProgress = useSharedValue(0);
5351
const isActive = useDerivedValue(() => activeItemKey.value === key);
54-
const layoutStyles = useItemLayoutStyles(key, activationAnimationProgress);
55-
const decorationStyles = useItemDecorationStyles(
56-
key,
57-
isActive,
58-
activationAnimationProgress
59-
);
52+
const itemStyles = useItemStyles(key, isActive, activationAnimationProgress);
6053
const gesture = useItemPanGesture(key, activationAnimationProgress);
6154

6255
useEffect(() => {
@@ -79,6 +72,9 @@ function DraggableView({
7972
};
8073
}, [portalContext, teleportedItemId]);
8174

75+
const onMeasure = (width: number, height: number) =>
76+
handleItemMeasurement(key, { height, width });
77+
8278
const withItemContext = (component: ReactNode) => (
8379
<ItemContextProvider
8480
activationAnimationProgress={activationAnimationProgress}
@@ -89,16 +85,14 @@ function DraggableView({
8985
</ItemContextProvider>
9086
);
9187

92-
const renderItemCell = (styleOverride?: ViewStyle) => {
88+
const renderItemCell = (hidden = false) => {
9389
const innerComponent = (
9490
<ItemCell
9591
{...layoutAnimations}
96-
cellStyle={[style, layoutStyles, styleOverride]}
97-
decorationStyles={decorationStyles}
92+
cellStyle={[style, itemStyles]}
93+
hidden={hidden}
9894
itemsOverridesStyle={itemsOverridesStyle}
99-
onMeasure={(width, height) =>
100-
handleItemMeasurement(key, { height, width })
101-
}>
95+
onMeasure={onMeasure}>
10296
<LayoutAnimationConfig skipEntering={false} skipExiting={false}>
10397
{children}
10498
</LayoutAnimationConfig>
@@ -134,7 +128,8 @@ function DraggableView({
134128
baseCellStyle={style}
135129
isActive={isActive}
136130
itemKey={key}
137-
itemsOverridesStyle={itemsOverridesStyle}>
131+
itemsOverridesStyle={itemsOverridesStyle}
132+
onMeasure={onMeasure}>
138133
{children}
139134
</TeleportedItemCell>
140135
)}
@@ -145,7 +140,7 @@ function DraggableView({
145140
<Fragment>
146141
{/* We cannot unmount this item as its gesture detector must be still
147142
mounted to continue handling the pan gesture */}
148-
{renderItemCell(isTeleported ? styles.hidden : undefined)}
143+
{renderItemCell(isTeleported)}
149144
<ActiveItemPortal
150145
activationAnimationProgress={activationAnimationProgress}
151146
renderTeleportedItemCell={renderTeleportedItemCell}
@@ -156,10 +151,4 @@ function DraggableView({
156151
);
157152
}
158153

159-
const styles = StyleSheet.create({
160-
hidden: {
161-
opacity: 0
162-
}
163-
});
164-
165154
export default memo(DraggableView);
Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { PropsWithChildren } from 'react';
2-
import type { LayoutChangeEvent, ViewStyle } from 'react-native';
2+
import {
3+
type LayoutChangeEvent,
4+
Platform,
5+
StyleSheet,
6+
type ViewStyle
7+
} from 'react-native';
38
import type { AnimatedStyle } from 'react-native-reanimated';
49
import Animated from 'react-native-reanimated';
510

@@ -11,42 +16,74 @@ import type {
1116
import AnimatedOnLayoutView from '../AnimatedOnLayoutView';
1217

1318
export type ItemCellProps = PropsWithChildren<{
14-
decorationStyles: AnimatedStyleProp;
1519
itemsOverridesStyle: AnimatedStyle<ViewStyle>;
1620
cellStyle: AnimatedStyleProp;
17-
onMeasure?: MeasureCallback;
21+
onMeasure: MeasureCallback;
22+
hidden?: boolean;
1823
entering?: LayoutAnimation;
1924
exiting?: LayoutAnimation;
2025
}>;
2126

2227
export default function ItemCell({
2328
cellStyle,
2429
children,
25-
decorationStyles,
2630
entering,
2731
exiting,
32+
hidden,
2833
itemsOverridesStyle,
2934
onMeasure
3035
}: ItemCellProps) {
31-
const maybeOnLayout = onMeasure
32-
? ({
36+
const style = [
37+
styles.decoration,
38+
cellStyle,
39+
itemsOverridesStyle,
40+
hidden && styles.hidden
41+
];
42+
43+
const onLayout = hidden
44+
? undefined
45+
: ({
3346
nativeEvent: {
3447
layout: { height, width }
3548
}
3649
}: LayoutChangeEvent) => {
3750
onMeasure(width, height);
38-
}
39-
: undefined;
51+
};
4052

4153
return (
42-
<Animated.View style={cellStyle}>
43-
<AnimatedOnLayoutView
44-
entering={entering}
45-
exiting={exiting}
46-
style={[itemsOverridesStyle, decorationStyles]}
47-
onLayout={maybeOnLayout}>
48-
{children}
49-
</AnimatedOnLayoutView>
50-
</Animated.View>
54+
<AnimatedOnLayoutView style={style} onLayout={onLayout}>
55+
{/* TODO - remove itemEntering and itemExiting layout animation in sortables v2 */}
56+
{entering || exiting ? (
57+
<Animated.View entering={entering} exiting={exiting}>
58+
{children}
59+
</Animated.View>
60+
) : (
61+
children
62+
)}
63+
</AnimatedOnLayoutView>
5164
);
5265
}
66+
67+
const styles = StyleSheet.create({
68+
decoration: Platform.select<ViewStyle>({
69+
android: {
70+
elevation: 5
71+
},
72+
default: {},
73+
native: {
74+
shadowOffset: {
75+
height: 0,
76+
width: 0
77+
},
78+
shadowOpacity: 1,
79+
shadowRadius: 5
80+
}
81+
}),
82+
hidden: {
83+
// TODO - find a better way to hide the item
84+
// (can't use opacity and transform because they are used in animated
85+
// styles, which take precedence over the js style; can't change dimensions
86+
// as they trigger layout transition in the child component)
87+
left: -9999
88+
}
89+
});

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

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import type { ViewStyle } from 'react-native';
33
import type { AnimatedStyle, SharedValue } from 'react-native-reanimated';
44
import { LayoutAnimationConfig } from 'react-native-reanimated';
55

6-
import {
7-
useItemDecorationStyles,
8-
useTeleportedItemStyles
9-
} from '../../../providers';
10-
import type { AnimatedStyleProp } from '../../../types';
6+
import { useTeleportedItemStyles } from '../../../providers';
7+
import type { AnimatedStyleProp, MeasureCallback } from '../../../types';
118
import ItemCell from './ItemCell';
129

1310
type TeleportedItemCellProps = PropsWithChildren<{
@@ -16,6 +13,7 @@ type TeleportedItemCellProps = PropsWithChildren<{
1613
baseCellStyle: AnimatedStyleProp;
1714
isActive: SharedValue<boolean>;
1815
itemKey: string;
16+
onMeasure: MeasureCallback;
1917
}>;
2018

2119
export default function TeleportedItemCell({
@@ -24,24 +22,20 @@ export default function TeleportedItemCell({
2422
children,
2523
isActive,
2624
itemKey,
27-
itemsOverridesStyle
25+
itemsOverridesStyle,
26+
onMeasure
2827
}: TeleportedItemCellProps) {
2928
const teleportedItemStyles = useTeleportedItemStyles(
3029
itemKey,
3130
isActive,
3231
activationAnimationProgress
3332
);
34-
const decorationStyles = useItemDecorationStyles(
35-
itemKey,
36-
isActive,
37-
activationAnimationProgress
38-
);
3933

4034
return (
4135
<ItemCell
4236
cellStyle={[baseCellStyle, teleportedItemStyles]}
43-
decorationStyles={decorationStyles}
44-
itemsOverridesStyle={itemsOverridesStyle}>
37+
itemsOverridesStyle={itemsOverridesStyle}
38+
onMeasure={onMeasure}>
4539
<LayoutAnimationConfig skipEntering>{children}</LayoutAnimationConfig>
4640
</ItemCell>
4741
);

packages/react-native-sortables/src/providers/layout/grid/GridLayoutProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ const { GridLayoutProvider, useGridLayoutContext } = createProvider(
174174
const mainDimension = isVertical ? 'width' : 'height';
175175
if (currentStyleOverride?.[mainDimension] !== mainGroupSize.value) {
176176
itemsStyleOverride.value = {
177-
[mainDimension]: mainGroupSize.value
177+
[mainDimension]: mainGroupSize.value + mainGap.value
178178
};
179179
}
180180

packages/react-native-sortables/src/providers/layout/grid/utils/layout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const calculateLayout = ({
5454
// Update offset of the next group
5555
crossAxisOffsets[crossIndex + 1] = Math.max(
5656
crossAxisOffsets[crossIndex + 1] ?? 0,
57-
crossAxisOffset + crossItemSize + gaps.cross
57+
crossAxisOffset + crossItemSize
5858
);
5959

6060
// Update item position
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export { default as useDebugBoundingBox } from './useDebugBoundingBox';
2-
export { default as useItemDecorationStyles } from './useItemDecorationStyles';
3-
export { default as useItemLayoutStyles } from './useItemLayoutStyles';
42
export { default as useItemPanGesture } from './useItemPanGesture';
3+
export { default as useItemStyles } from './useItemStyles';
54
export { default as useOrderUpdater } from './useOrderUpdater';
65
export { default as useTeleportedItemStyles } from './useTeleportedItemStyles';

packages/react-native-sortables/src/providers/shared/hooks/useItemDecorationStyles.ts renamed to packages/react-native-sortables/src/providers/shared/hooks/useItemDecorationValues.ts

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
1-
import { useMemo } from 'react';
2-
import type { ViewStyle } from 'react-native';
3-
import { Platform, StyleSheet } from 'react-native';
41
import type { SharedValue } from 'react-native-reanimated';
52
import {
63
interpolate,
74
interpolateColor,
8-
useAnimatedStyle,
95
useDerivedValue,
106
withTiming
117
} from 'react-native-reanimated';
128

139
import { IS_WEB } from '../../../constants';
14-
import type { AnimatedStyleProp } from '../../../types';
1510
import { useCommonValuesContext } from '../CommonValuesProvider';
1611

17-
export default function useItemDecorationStyles(
18-
itemKey: string,
12+
export default function useItemDecorationValues(
13+
key: string,
1914
isActive: SharedValue<boolean>,
2015
activationAnimationProgress: SharedValue<number>
21-
): AnimatedStyleProp {
16+
) {
2217
const {
2318
activationAnimationDuration,
2419
activeItemOpacity,
@@ -31,7 +26,7 @@ export default function useItemDecorationStyles(
3126
} = useCommonValuesContext();
3227

3328
const adjustedInactiveProgress = useDerivedValue(() => {
34-
if (isActive.value || prevActiveItemKey.value === itemKey) {
29+
if (isActive.value || prevActiveItemKey.value === key) {
3530
return withTiming(0, { duration: activationAnimationDuration.value });
3631
}
3732

@@ -42,7 +37,7 @@ export default function useItemDecorationStyles(
4237
);
4338
});
4439

45-
const animatedStyle = useAnimatedStyle(() => {
40+
return useDerivedValue(() => {
4641
const progress = activationAnimationProgress.value;
4742
const zeroProgressOpacity = interpolate(
4843
adjustedInactiveProgress.value,
@@ -83,23 +78,4 @@ export default function useItemDecorationStyles(
8378
]
8479
};
8580
});
86-
87-
return useMemo(() => [styles.decoration, animatedStyle], [animatedStyle]);
8881
}
89-
90-
const styles = StyleSheet.create({
91-
decoration: Platform.select<ViewStyle>({
92-
android: {
93-
elevation: 5
94-
},
95-
default: {},
96-
native: {
97-
shadowOffset: {
98-
height: 0,
99-
width: 0
100-
},
101-
shadowOpacity: 1,
102-
shadowRadius: 5
103-
}
104-
})
105-
});

0 commit comments

Comments
 (0)