-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <40713406+tjzel@users.noreply.github.com>
- Loading branch information
Showing
6 changed files
with
294 additions
and
0 deletions.
There are no files selected for viewing
202 changes: 202 additions & 0 deletions
202
apps/common-app/src/examples/LayoutAnimations/ListItemLayoutAnimation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import React, { memo, useCallback, useState } from 'react'; | ||
import type { ListRenderItem } from 'react-native'; | ||
import { | ||
Dimensions, | ||
Pressable, | ||
SafeAreaView, | ||
StyleSheet, | ||
Text, | ||
TouchableOpacity, | ||
View, | ||
} from 'react-native'; | ||
import Animated, { | ||
CurvedTransition, | ||
EntryExitTransition, | ||
FadeOut, | ||
FadeIn, | ||
FadingTransition, | ||
JumpingTransition, | ||
LayoutAnimationConfig, | ||
LinearTransition, | ||
SequencedTransition, | ||
} from 'react-native-reanimated'; | ||
|
||
const ITEMS = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']; | ||
const LAYOUT_TRANSITIONS = [ | ||
LinearTransition, | ||
FadingTransition, | ||
SequencedTransition, | ||
JumpingTransition, | ||
CurvedTransition, | ||
EntryExitTransition, | ||
] as const; | ||
|
||
type ListItemProps = { | ||
id: string; | ||
text: string; | ||
onPress: (id: string) => void; | ||
}; | ||
|
||
const ListItem = memo(function ({ id, text, onPress }: ListItemProps) { | ||
return ( | ||
<Pressable onPress={() => onPress(id)} style={styles.listItem}> | ||
<Text style={styles.itemText}>{text}</Text> | ||
</Pressable> | ||
); | ||
}); | ||
|
||
export default function ListItemLayoutAnimation() { | ||
const [layoutTransitionEnabled, setLayoutTransitionEnabled] = useState(true); | ||
const [currentTransitionIndex, setCurrentTransitionIndex] = useState(0); | ||
const [items, setItems] = useState(ITEMS); | ||
|
||
const removeItem = useCallback((id: string) => { | ||
setItems((prevItems) => prevItems.filter((item) => item !== id)); | ||
}, []); | ||
|
||
const renderItem = useCallback<ListRenderItem<string>>( | ||
({ item }) => <ListItem id={item} text={item} onPress={removeItem} />, | ||
[removeItem] | ||
); | ||
|
||
const getNewItemName = useCallback(() => { | ||
let i = 1; | ||
while (items.includes(`Item ${i}`)) { | ||
i++; | ||
} | ||
return `Item ${i}`; | ||
}, [items]); | ||
|
||
const reorderItems = useCallback(() => { | ||
setItems((prevItems) => { | ||
const newItems = [...prevItems]; | ||
newItems.sort(() => Math.random() - 0.5); | ||
return newItems; | ||
}); | ||
}, []); | ||
|
||
const resetOrder = useCallback(() => { | ||
setItems((prevItems) => { | ||
const newItems = [...prevItems]; | ||
newItems.sort((left, right) => { | ||
const aNum = parseInt(left.match(/\d+$/)![0], 10); | ||
const bNum = parseInt(right.match(/\d+$/)![0], 10); | ||
return aNum - bNum; | ||
}); | ||
return newItems; | ||
}); | ||
}, []); | ||
|
||
const transition = layoutTransitionEnabled | ||
? LAYOUT_TRANSITIONS[currentTransitionIndex] | ||
: undefined; | ||
|
||
return ( | ||
<LayoutAnimationConfig skipEntering> | ||
<SafeAreaView style={styles.container}> | ||
<View style={styles.menu}> | ||
<View style={styles.row}> | ||
<Text style={styles.infoText}>Layout animation: </Text> | ||
<TouchableOpacity | ||
onPress={() => { | ||
setLayoutTransitionEnabled((prev) => !prev); | ||
}}> | ||
<Text style={styles.buttonText}> | ||
{layoutTransitionEnabled ? 'Enabled' : 'Disabled'} | ||
</Text> | ||
</TouchableOpacity> | ||
</View> | ||
{transition && ( | ||
<Animated.View | ||
style={styles.row} | ||
entering={FadeIn} | ||
exiting={FadeOut}> | ||
<Text style={styles.infoText}> | ||
Current: {transition?.presetName} | ||
</Text> | ||
<TouchableOpacity | ||
onPress={() => { | ||
setCurrentTransitionIndex( | ||
(prev) => (prev + 1) % LAYOUT_TRANSITIONS.length | ||
); | ||
}}> | ||
<Text style={styles.buttonText}>Change</Text> | ||
</TouchableOpacity> | ||
</Animated.View> | ||
)} | ||
</View> | ||
|
||
<Animated.FlatList | ||
style={styles.list} | ||
data={items} | ||
renderItem={renderItem} | ||
keyExtractor={(item) => item} | ||
contentContainerStyle={styles.contentContainer} | ||
itemLayoutAnimation={layoutTransitionEnabled ? transition : undefined} | ||
layout={transition} | ||
/> | ||
|
||
<Animated.View style={styles.menu} layout={transition}> | ||
<Text style={styles.infoText}>Press an item to remove it</Text> | ||
<TouchableOpacity | ||
onPress={() => setItems([...items, getNewItemName()])}> | ||
<Text style={styles.buttonText}>Add item</Text> | ||
</TouchableOpacity> | ||
<Animated.View style={styles.row} layout={transition}> | ||
<TouchableOpacity onPress={reorderItems}> | ||
<Text style={styles.buttonText}>Reorder</Text> | ||
</TouchableOpacity> | ||
<TouchableOpacity onPress={resetOrder}> | ||
<Text style={styles.buttonText}>Reset order</Text> | ||
</TouchableOpacity> | ||
</Animated.View> | ||
</Animated.View> | ||
</SafeAreaView> | ||
</LayoutAnimationConfig> | ||
); | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
flex: 1, | ||
}, | ||
contentContainer: { | ||
padding: 16, | ||
gap: 16, | ||
}, | ||
row: { | ||
flexDirection: 'row', | ||
gap: 16, | ||
alignItems: 'center', | ||
}, | ||
list: { | ||
flexGrow: 0, | ||
maxHeight: Dimensions.get('window').height - 300, | ||
}, | ||
listItem: { | ||
padding: 20, | ||
backgroundColor: '#ad8ee9', | ||
shadowColor: '#000', | ||
shadowOpacity: 0.05, | ||
}, | ||
itemText: { | ||
color: 'white', | ||
fontSize: 22, | ||
}, | ||
menu: { | ||
padding: 16, | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
paddingTop: 16, | ||
gap: 8, | ||
}, | ||
infoText: { | ||
color: '#222534', | ||
fontSize: 18, | ||
}, | ||
buttonText: { | ||
fontSize: 18, | ||
fontWeight: 'bold', | ||
color: '#b59aeb', | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
packages/docs-reanimated/docs/layout-animations/list-layout-animations.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
--- | ||
sidebar_position: 6 | ||
title: List Layout Animations | ||
sidebar_label: List Layout Animations | ||
--- | ||
|
||
`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). | ||
|
||
## Example | ||
|
||
<Row> | ||
|
||
<ThemedVideo | ||
sources={{ | ||
light: '/recordings/layout-animations/listitem_light.mov', | ||
dark: '/recordings/layout-animations/listitem_dark.mov', | ||
}} | ||
/> | ||
|
||
<div style={{flexGrow: 1}}> | ||
|
||
```jsx | ||
import Animated, { LinearTransition } from 'react-native-reanimated'; | ||
|
||
function App() { | ||
return ( | ||
<Animated.FlatList | ||
data={data} | ||
renderItem={renderItem} | ||
// highlight-next-line | ||
itemLayoutAnimation={LinearTransition} | ||
/> | ||
); | ||
} | ||
``` | ||
|
||
</div> | ||
|
||
</Row> | ||
|
||
## Remarks | ||
|
||
- `itemLayoutAnimation` works only with a single-column `Animated.FlatList`, `numColumns` property cannot be grater than 1. | ||
- You can change the `itemLayoutAnimation` on the fly or disable it by setting it to `undefined`. | ||
|
||
<Indent> | ||
|
||
```javascript | ||
function App() { | ||
const [transition, setTransition] = useState(LinearTransition); | ||
|
||
const changeTransition = () => { | ||
// highlight-start | ||
setTransition( | ||
transition === LinearTransition ? JumpingTransition : LinearTransition | ||
); | ||
// highlight-end | ||
}; | ||
|
||
const toggleTransition = () => { | ||
// highlight-next-line | ||
setTransition(transition ? undefined : LinearTransition); | ||
}; | ||
|
||
return ( | ||
<Animated.FlatList | ||
data={data} | ||
renderItem={renderItem} | ||
// highlight-next-line | ||
itemLayoutAnimation={transition} | ||
/> | ||
); | ||
} | ||
``` | ||
|
||
</Indent> | ||
|
||
## Platform compatibility | ||
|
||
<div className="platform-compatibility"> | ||
|
||
| Android | iOS | Web | | ||
| ------- | --- | --- | | ||
| ✅ | ✅ | ✅ | | ||
|
||
</div> |
Binary file added
BIN
+93.9 KB
packages/docs-reanimated/static/recordings/layout-animations/listitem_dark.mov
Binary file not shown.
Binary file added
BIN
+90.7 KB
packages/docs-reanimated/static/recordings/layout-animations/listitem_light.mov
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters