Skip to content

Commit 0f8310c

Browse files
MatiPl01tjzel
andauthored
Add list itemLayoutAnimation documentation page and example (#6279)
## Summary This PR adds example usage of `itemLayoutAnimation` prop to the example app and docs page with example recording and details. ## Example image of the added docs page ![Screenshot 2024-07-22 at 15 46 23](https://github.com/user-attachments/assets/bcb5667c-afb5-45b9-9c25-114f7b06e0c0) ## Related context Related issue: #6278 Support for `multi-column` lists seems to be impossible to implement. react-native adds additional wrapper component for each row and re-renders list items in different rows when new items are added or items are removed from the list. Because the parent of list items changes and the layout animation cannot be applied to the wrapper that is added to list rows, layout animations won't work for lists with multiple columns. At least, I didn't come up with any valid solution. PR that adds support for `FlatList` items animations: #2674 What react-native does for mutli-column lists: https://github.com/facebook/react-native/blob/2098806c2207f376027184329a7285913ef8d090/packages/react-native/Libraries/Lists/FlatList.js#L643 --------- Co-authored-by: Tomasz Żelawski <[email protected]>
1 parent 274bc79 commit 0f8310c

File tree

6 files changed

+294
-0
lines changed

6 files changed

+294
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import React, { memo, useCallback, useState } from 'react';
2+
import type { ListRenderItem } from 'react-native';
3+
import {
4+
Dimensions,
5+
Pressable,
6+
SafeAreaView,
7+
StyleSheet,
8+
Text,
9+
TouchableOpacity,
10+
View,
11+
} from 'react-native';
12+
import Animated, {
13+
CurvedTransition,
14+
EntryExitTransition,
15+
FadeOut,
16+
FadeIn,
17+
FadingTransition,
18+
JumpingTransition,
19+
LayoutAnimationConfig,
20+
LinearTransition,
21+
SequencedTransition,
22+
} from 'react-native-reanimated';
23+
24+
const ITEMS = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
25+
const LAYOUT_TRANSITIONS = [
26+
LinearTransition,
27+
FadingTransition,
28+
SequencedTransition,
29+
JumpingTransition,
30+
CurvedTransition,
31+
EntryExitTransition,
32+
] as const;
33+
34+
type ListItemProps = {
35+
id: string;
36+
text: string;
37+
onPress: (id: string) => void;
38+
};
39+
40+
const ListItem = memo(function ({ id, text, onPress }: ListItemProps) {
41+
return (
42+
<Pressable onPress={() => onPress(id)} style={styles.listItem}>
43+
<Text style={styles.itemText}>{text}</Text>
44+
</Pressable>
45+
);
46+
});
47+
48+
export default function ListItemLayoutAnimation() {
49+
const [layoutTransitionEnabled, setLayoutTransitionEnabled] = useState(true);
50+
const [currentTransitionIndex, setCurrentTransitionIndex] = useState(0);
51+
const [items, setItems] = useState(ITEMS);
52+
53+
const removeItem = useCallback((id: string) => {
54+
setItems((prevItems) => prevItems.filter((item) => item !== id));
55+
}, []);
56+
57+
const renderItem = useCallback<ListRenderItem<string>>(
58+
({ item }) => <ListItem id={item} text={item} onPress={removeItem} />,
59+
[removeItem]
60+
);
61+
62+
const getNewItemName = useCallback(() => {
63+
let i = 1;
64+
while (items.includes(`Item ${i}`)) {
65+
i++;
66+
}
67+
return `Item ${i}`;
68+
}, [items]);
69+
70+
const reorderItems = useCallback(() => {
71+
setItems((prevItems) => {
72+
const newItems = [...prevItems];
73+
newItems.sort(() => Math.random() - 0.5);
74+
return newItems;
75+
});
76+
}, []);
77+
78+
const resetOrder = useCallback(() => {
79+
setItems((prevItems) => {
80+
const newItems = [...prevItems];
81+
newItems.sort((left, right) => {
82+
const aNum = parseInt(left.match(/\d+$/)![0], 10);
83+
const bNum = parseInt(right.match(/\d+$/)![0], 10);
84+
return aNum - bNum;
85+
});
86+
return newItems;
87+
});
88+
}, []);
89+
90+
const transition = layoutTransitionEnabled
91+
? LAYOUT_TRANSITIONS[currentTransitionIndex]
92+
: undefined;
93+
94+
return (
95+
<LayoutAnimationConfig skipEntering>
96+
<SafeAreaView style={styles.container}>
97+
<View style={styles.menu}>
98+
<View style={styles.row}>
99+
<Text style={styles.infoText}>Layout animation: </Text>
100+
<TouchableOpacity
101+
onPress={() => {
102+
setLayoutTransitionEnabled((prev) => !prev);
103+
}}>
104+
<Text style={styles.buttonText}>
105+
{layoutTransitionEnabled ? 'Enabled' : 'Disabled'}
106+
</Text>
107+
</TouchableOpacity>
108+
</View>
109+
{transition && (
110+
<Animated.View
111+
style={styles.row}
112+
entering={FadeIn}
113+
exiting={FadeOut}>
114+
<Text style={styles.infoText}>
115+
Current: {transition?.presetName}
116+
</Text>
117+
<TouchableOpacity
118+
onPress={() => {
119+
setCurrentTransitionIndex(
120+
(prev) => (prev + 1) % LAYOUT_TRANSITIONS.length
121+
);
122+
}}>
123+
<Text style={styles.buttonText}>Change</Text>
124+
</TouchableOpacity>
125+
</Animated.View>
126+
)}
127+
</View>
128+
129+
<Animated.FlatList
130+
style={styles.list}
131+
data={items}
132+
renderItem={renderItem}
133+
keyExtractor={(item) => item}
134+
contentContainerStyle={styles.contentContainer}
135+
itemLayoutAnimation={layoutTransitionEnabled ? transition : undefined}
136+
layout={transition}
137+
/>
138+
139+
<Animated.View style={styles.menu} layout={transition}>
140+
<Text style={styles.infoText}>Press an item to remove it</Text>
141+
<TouchableOpacity
142+
onPress={() => setItems([...items, getNewItemName()])}>
143+
<Text style={styles.buttonText}>Add item</Text>
144+
</TouchableOpacity>
145+
<Animated.View style={styles.row} layout={transition}>
146+
<TouchableOpacity onPress={reorderItems}>
147+
<Text style={styles.buttonText}>Reorder</Text>
148+
</TouchableOpacity>
149+
<TouchableOpacity onPress={resetOrder}>
150+
<Text style={styles.buttonText}>Reset order</Text>
151+
</TouchableOpacity>
152+
</Animated.View>
153+
</Animated.View>
154+
</SafeAreaView>
155+
</LayoutAnimationConfig>
156+
);
157+
}
158+
159+
const styles = StyleSheet.create({
160+
container: {
161+
flex: 1,
162+
},
163+
contentContainer: {
164+
padding: 16,
165+
gap: 16,
166+
},
167+
row: {
168+
flexDirection: 'row',
169+
gap: 16,
170+
alignItems: 'center',
171+
},
172+
list: {
173+
flexGrow: 0,
174+
maxHeight: Dimensions.get('window').height - 300,
175+
},
176+
listItem: {
177+
padding: 20,
178+
backgroundColor: '#ad8ee9',
179+
shadowColor: '#000',
180+
shadowOpacity: 0.05,
181+
},
182+
itemText: {
183+
color: 'white',
184+
fontSize: 22,
185+
},
186+
menu: {
187+
padding: 16,
188+
alignItems: 'center',
189+
justifyContent: 'center',
190+
paddingTop: 16,
191+
gap: 8,
192+
},
193+
infoText: {
194+
color: '#222534',
195+
fontSize: 18,
196+
},
197+
buttonText: {
198+
fontSize: 18,
199+
fontWeight: 'bold',
200+
color: '#b59aeb',
201+
},
202+
});

apps/common-app/src/examples/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import BorderRadiiExample from './SharedElementTransitions/BorderRadii';
131131
import FreezingShareablesExample from './ShareableFreezingExample';
132132
import TabNavigatorExample from './SharedElementTransitions/TabNavigatorExample';
133133
import StrictDOMExample from './StrictDOMExample';
134+
import ListItemLayoutAnimation from './LayoutAnimations/ListItemLayoutAnimation';
134135

135136
interface Example {
136137
icon?: string;
@@ -669,6 +670,10 @@ export const EXAMPLES: Record<string, Example> = {
669670
title: '[LA] Reactions counter',
670671
screen: ReactionsCounterExample,
671672
},
673+
ListItemLayoutAnimation: {
674+
title: '[LA] List item layout animation',
675+
screen: ListItemLayoutAnimation,
676+
},
672677
SwipeableList: {
673678
title: '[LA] Swipeable list',
674679
screen: SwipeableList,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
sidebar_position: 6
3+
title: List Layout Animations
4+
sidebar_label: List Layout Animations
5+
---
6+
7+
`itemLayoutAnimation` lets you define a [layout transition](/docs/layout-animations/layout-transitions) that's applied when list items layout changes. You can use one of the [predefined transitions](/docs/layout-animations/layout-transitions#predefined-transitions) like `LinearTransition` or create [your own transition](/docs/layout-animations/custom-animations#custom-layout-transition).
8+
9+
## Example
10+
11+
<Row>
12+
13+
<ThemedVideo
14+
sources={{
15+
light: '/recordings/layout-animations/listitem_light.mov',
16+
dark: '/recordings/layout-animations/listitem_dark.mov',
17+
}}
18+
/>
19+
20+
<div style={{flexGrow: 1}}>
21+
22+
```jsx
23+
import Animated, { LinearTransition } from 'react-native-reanimated';
24+
25+
function App() {
26+
return (
27+
<Animated.FlatList
28+
data={data}
29+
renderItem={renderItem}
30+
// highlight-next-line
31+
itemLayoutAnimation={LinearTransition}
32+
/>
33+
);
34+
}
35+
```
36+
37+
</div>
38+
39+
</Row>
40+
41+
## Remarks
42+
43+
- `itemLayoutAnimation` works only with a single-column `Animated.FlatList`, `numColumns` property cannot be grater than 1.
44+
- You can change the `itemLayoutAnimation` on the fly or disable it by setting it to `undefined`.
45+
46+
<Indent>
47+
48+
```javascript
49+
function App() {
50+
const [transition, setTransition] = useState(LinearTransition);
51+
52+
const changeTransition = () => {
53+
// highlight-start
54+
setTransition(
55+
transition === LinearTransition ? JumpingTransition : LinearTransition
56+
);
57+
// highlight-end
58+
};
59+
60+
const toggleTransition = () => {
61+
// highlight-next-line
62+
setTransition(transition ? undefined : LinearTransition);
63+
};
64+
65+
return (
66+
<Animated.FlatList
67+
data={data}
68+
renderItem={renderItem}
69+
// highlight-next-line
70+
itemLayoutAnimation={transition}
71+
/>
72+
);
73+
}
74+
```
75+
76+
</Indent>
77+
78+
## Platform compatibility
79+
80+
<div className="platform-compatibility">
81+
82+
| Android | iOS | Web |
83+
| ------- | --- | --- |
84+
||||
85+
86+
</div>
Binary file not shown.
Binary file not shown.

packages/react-native-reanimated/src/component/FlatList.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ interface ReanimatedFlatListPropsWithLayout<T>
4545
extends AnimatedProps<FlatListProps<T>> {
4646
/**
4747
* Lets you pass layout animation directly to the FlatList item.
48+
* Works only with a single-column `Animated.FlatList`, `numColumns` property cannot be greater than 1.
4849
*/
4950
itemLayoutAnimation?: ILayoutAnimationBuilder;
5051
/**

0 commit comments

Comments
 (0)