Skip to content

Commit

Permalink
Add list itemLayoutAnimation documentation page and example (#6279)
Browse files Browse the repository at this point in the history
## 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
MatiPl01 and tjzel committed Jul 23, 2024
1 parent 274bc79 commit 0f8310c
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 0 deletions.
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',
},
});
5 changes: 5 additions & 0 deletions apps/common-app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ import BorderRadiiExample from './SharedElementTransitions/BorderRadii';
import FreezingShareablesExample from './ShareableFreezingExample';
import TabNavigatorExample from './SharedElementTransitions/TabNavigatorExample';
import StrictDOMExample from './StrictDOMExample';
import ListItemLayoutAnimation from './LayoutAnimations/ListItemLayoutAnimation';

interface Example {
icon?: string;
Expand Down Expand Up @@ -669,6 +670,10 @@ export const EXAMPLES: Record<string, Example> = {
title: '[LA] Reactions counter',
screen: ReactionsCounterExample,
},
ListItemLayoutAnimation: {
title: '[LA] List item layout animation',
screen: ListItemLayoutAnimation,
},
SwipeableList: {
title: '[LA] Swipeable list',
screen: SwipeableList,
Expand Down
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 not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ interface ReanimatedFlatListPropsWithLayout<T>
extends AnimatedProps<FlatListProps<T>> {
/**
* Lets you pass layout animation directly to the FlatList item.
* Works only with a single-column `Animated.FlatList`, `numColumns` property cannot be greater than 1.
*/
itemLayoutAnimation?: ILayoutAnimationBuilder;
/**
Expand Down

0 comments on commit 0f8310c

Please sign in to comment.