Skip to content
Open
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
4 changes: 4 additions & 0 deletions example/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ const data: ListElement[] = [
title: "Cards FlatList",
url: "/cards-flatlist",
},
{
title: "Animated LegendList",
url: "/animated-legend-list",
},
].map(
(v, i) =>
({
Expand Down
222 changes: 222 additions & 0 deletions example/app/animated-legend-list/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { AnimatedLegendList } from "@legendapp/list";
import { type TCountryCode, countries, getEmojiFlag } from "countries-list";
import { memo, useCallback, useState } from "react";
import { SafeAreaView, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
import Animated, {
CurvedTransition,
EntryExitTransition,
FadeIn,
FadeOut,
FadingTransition,
JumpingTransition,
LayoutAnimationConfig,
LinearTransition,
SequencedTransition,
} from "react-native-reanimated";

type Country = {
id: string;
name: string;
flag: string;
};

const ORIGINAL_DATA: Country[] = Object.entries(countries)
.map(([code, country]) => ({
id: code,
name: country.name,
flag: getEmojiFlag(code as TCountryCode),
}))
.sort((a, b) => a.name.localeCompare(b.name))
.slice(0, 30);

const LAYOUT_TRANSITIONS = [
LinearTransition,
FadingTransition,
SequencedTransition,
JumpingTransition,
CurvedTransition,
EntryExitTransition,
];

const CountryItem = memo(({ item, onPress }: { item: Country; onPress: (id: string) => void }) => (
<TouchableOpacity onPress={() => onPress(item.id)} style={styles.listItem}>
<View style={styles.flagContainer}>
<Text style={styles.flag}>{item.flag}</Text>
</View>
<View style={styles.contentContainer}>
<Text style={styles.title}>{item.name}</Text>
<Text style={styles.subtitle}>({item.id}) - Tap to remove</Text>
</View>
</TouchableOpacity>
));

const App = () => {
const [countriesData, setCountriesData] = useState<Country[]>(ORIGINAL_DATA);

const [search, setSearch] = useState("");

const [currentTransitionIndex, setCurrentTransitionIndex] = useState(0);

const [layoutTransitionEnabled, setLayoutTransitionEnabled] = useState(true);

const layout = layoutTransitionEnabled ? LAYOUT_TRANSITIONS[currentTransitionIndex] : undefined;

const removeItem = (id: string) => {
setCountriesData((prev) => prev.filter((c) => c.id !== id));
};

const addItem = () => {
const unused = ORIGINAL_DATA.find((c) => !countriesData.some((d) => d.id === c.id));
if (unused) {
setCountriesData((prev) => [unused, ...prev]);
}
};

const reorderItems = () => {
setCountriesData((prev) => [...prev].sort(() => Math.random() - 0.5));
};

const resetItems = () => {
setCountriesData(ORIGINAL_DATA);
setSearch("");
};

const renderItem = useCallback(
({ item }: { item: Country }) => <CountryItem item={item} onPress={removeItem} />,
[removeItem],
);

const keyExtractor = useCallback((item: Country) => item.id, []);

const filteredData = countriesData.filter(
(c) => c.name.toLowerCase().includes(search.toLowerCase()) || c.id.toLowerCase().includes(search.toLowerCase()),
);

return (
<LayoutAnimationConfig skipEntering>
<SafeAreaView style={styles.container}>
<View style={styles.controls}>
<TextInput
placeholder="Search country..."
value={search}
onChangeText={setSearch}
style={styles.input}
/>

<TouchableOpacity onPress={() => setLayoutTransitionEnabled((v) => !v)}>
<Text style={styles.buttonText}>Layout: {layoutTransitionEnabled ? "On" : "Off"}</Text>
</TouchableOpacity>

{layout && (
<Animated.View style={styles.row} entering={FadeIn} exiting={FadeOut}>
<Text style={styles.transitionText}>Current: {layout?.presetName ?? "Unknown"}</Text>
<TouchableOpacity
onPress={() =>
setCurrentTransitionIndex((prev) => (prev + 1) % LAYOUT_TRANSITIONS.length)
}
>
<Text style={styles.buttonText}>Change</Text>
</TouchableOpacity>
</Animated.View>
)}
</View>

<AnimatedLegendList
data={filteredData}
renderItem={renderItem}
keyExtractor={keyExtractor}
estimatedItemSize={70}
// @ts-ignore
itemLayoutAnimation={layout}
// @ts-ignore
layout={layout}
entering={FadeIn}
exiting={FadeOut}
contentContainerStyle={styles.listContainer}
/>

<Animated.View style={styles.controls} layout={layout}>
<TouchableOpacity onPress={addItem}>
<Text style={styles.buttonText}>Add Country</Text>
</TouchableOpacity>
<TouchableOpacity onPress={reorderItems}>
<Text style={styles.buttonText}>Reorder</Text>
</TouchableOpacity>
<TouchableOpacity onPress={resetItems}>
<Text style={styles.buttonText}>Reset</Text>
</TouchableOpacity>
</Animated.View>
</SafeAreaView>
</LayoutAnimationConfig>
);
};

export default App;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
controls: {
padding: 12,
alignItems: "center",
gap: 12,
},
row: {
flexDirection: "row",
gap: 16,
alignItems: "center",
},
input: {
width: "90%",
height: 40,
backgroundColor: "#f0f0f0",
borderRadius: 8,
paddingHorizontal: 10,
},
buttonText: {
fontSize: 16,
fontWeight: "600",
color: "#7a42f4",
},
transitionText: {
fontSize: 16,
color: "#333",
},
listContainer: {
padding: 12,
gap: 8,
},
listItem: {
padding: 16,
backgroundColor: "#e3d7fb",
borderRadius: 10,
flexDirection: "row",
alignItems: "center",
},
flagContainer: {
marginRight: 12,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#fff",
justifyContent: "center",
alignItems: "center",
},
flag: {
fontSize: 26,
},
contentContainer: {
flex: 1,
},
title: {
fontSize: 16,
fontWeight: "500",
color: "#222",
},
subtitle: {
fontSize: 13,
color: "#666",
},
});
2 changes: 1 addition & 1 deletion example/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion src/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export const Container = <ItemT,>({
getRenderedItem,
updateItemSize,
ItemSeparatorComponent,
CellRendererComponent,
}: {
id: number;
recycleItems?: boolean;
horizontal: boolean;
getRenderedItem: (key: string) => { index: number; item: ItemT; renderedItem: React.ReactNode } | null;
updateItemSize: (itemKey: string, size: number) => void;
ItemSeparatorComponent?: React.ComponentType<{ leadingItem: ItemT }>;
CellRendererComponent?: React.ComponentType<any> | null;
}) => {
const ctx = useStateContext();
const columnWrapperStyle = ctx.columnWrapperStyle;
Expand Down Expand Up @@ -167,10 +169,18 @@ export const Container = <ItemT,>({
return { containerId: id, itemKey, index: index!, value: data, triggerLayout };
}, [id, itemKey, index, data]);

const content = CellRendererComponent ? (
<CellRendererComponent index={index} item={renderedItemInfo?.item}>
{renderedItem}
</CellRendererComponent>
) : (
renderedItem
);

const contentFragment = (
<React.Fragment key={recycleItems ? undefined : itemKey}>
<ContextContainer.Provider value={contextValue}>
{renderedItem}
{content}
{renderedItemInfo && ItemSeparatorComponent && !lastItemKeys.includes(itemKey) && (
<ItemSeparatorComponent leadingItem={renderedItemInfo.item} />
)}
Expand Down
3 changes: 3 additions & 0 deletions src/Containers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface ContainersProps<ItemT> {
waitForInitialLayout: boolean | undefined;
updateItemSize: (itemKey: string, size: number) => void;
getRenderedItem: (key: string) => { index: number; item: ItemT; renderedItem: React.ReactNode } | null;
CellRendererComponent?: React.ComponentType<any> | null;
}

export const Containers = typedMemo(function Containers<ItemT>({
Expand All @@ -22,6 +23,7 @@ export const Containers = typedMemo(function Containers<ItemT>({
waitForInitialLayout,
updateItemSize,
getRenderedItem,
CellRendererComponent,
}: ContainersProps<ItemT>) {
const ctx = useStateContext();
const columnWrapperStyle = ctx.columnWrapperStyle;
Expand All @@ -42,6 +44,7 @@ export const Containers = typedMemo(function Containers<ItemT>({
// specifying inline separator makes Containers rerender on each data change
// should we do memo of ItemSeparatorComponent?
ItemSeparatorComponent={ItemSeparatorComponent}
CellRendererComponent={CellRendererComponent}
/>,
);
}
Expand Down
7 changes: 6 additions & 1 deletion src/LegendList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const LegendListInner = typedForwardRef(function LegendListInner<T>(
viewabilityConfig,
viewabilityConfigCallbackPairs,
onViewableItemsChanged,
CellRendererComponent,
...rest
} = props;

Expand Down Expand Up @@ -943,7 +944,10 @@ const LegendListInner = typedForwardRef(function LegendListInner<T>(
const setPaddingTop = ({
stylePaddingTop,
alignItemsPaddingTop,
}: { stylePaddingTop?: number; alignItemsPaddingTop?: number }) => {
}: {
stylePaddingTop?: number;
alignItemsPaddingTop?: number;
}) => {
if (stylePaddingTop !== undefined) {
const prevStylePaddingTop = peek$(ctx, "stylePaddingTop") || 0;
if (stylePaddingTop < prevStylePaddingTop) {
Expand Down Expand Up @@ -1867,6 +1871,7 @@ const LegendListInner = typedForwardRef(function LegendListInner<T>(
}
style={style}
contentContainerStyle={contentContainerStyle}
CellRendererComponent={CellRendererComponent}
/>
{__DEV__ && ENABLE_DEBUG_VIEW && <DebugView state={refState.current!} />}
</>
Expand Down
2 changes: 2 additions & 0 deletions src/ListComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export const ListComponent = typedMemo(function ListComponent<ItemT>({
onRefresh,
refreshing,
progressViewOffset,
CellRendererComponent,
...rest
}: ListComponentProps<ItemT>) {
const ctx = useStateContext();
Expand Down Expand Up @@ -198,6 +199,7 @@ export const ListComponent = typedMemo(function ListComponent<ItemT>({
waitForInitialLayout={waitForInitialLayout}
getRenderedItem={getRenderedItem}
ItemSeparatorComponent={ItemSeparatorComponent}
CellRendererComponent={CellRendererComponent}
updateItemSize={updateItemSize}
/>
{ListFooterComponent && (
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { LegendList } from "./LegendList";
export { AnimatedLegendList } from "./reanimated";
export { useRecyclingEffect, useRecyclingState, useViewability, useViewabilityAmount } from "./ContextContainer";
export type * from "./types";
Loading