Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add list itemLayoutAnimation documentation page and example #6279

Merged
merged 14 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
tjzel marked this conversation as resolved.
Show resolved Hide resolved
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