Skip to content

Commit 9373580

Browse files
committed
chore: add paged examples for FlashList and LegendList
1 parent c7e4e45 commit 9373580

4 files changed

Lines changed: 746 additions & 5 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import {
2+
ContentCard,
3+
DynamicBox,
4+
TabButton,
5+
TitleWithSubtitle,
6+
} from '@/components';
7+
import HeaderMotion, {
8+
createHeaderMotionScrollable,
9+
useActiveScrollId,
10+
useMotionProgress,
11+
} from 'react-native-header-motion';
12+
import { Stack } from 'expo-router';
13+
import { useRef } from 'react';
14+
import { StyleSheet, View } from 'react-native';
15+
import PagerView, {
16+
type PagerViewOnPageSelectedEvent,
17+
} from 'react-native-pager-view';
18+
import Animated, {
19+
Extrapolation,
20+
interpolate,
21+
useAnimatedStyle,
22+
} from 'react-native-reanimated';
23+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
24+
import { FlashList } from '@shopify/flash-list';
25+
26+
const HeaderMotionFlashList = createHeaderMotionScrollable(FlashList, {
27+
displayName: 'HeaderMotionFlashList',
28+
});
29+
30+
const indexToKey = new Map([
31+
[0, 'A'],
32+
[1, 'B'],
33+
]);
34+
const keyToIndex = new Map([
35+
['A', 0],
36+
['B', 1],
37+
]);
38+
39+
export default function Screen() {
40+
const [activeScrollId, setActiveScrollId] = useActiveScrollId<string>('A');
41+
const pagerRef = useRef<PagerView>(null);
42+
43+
const handleTabPress = (key: string) => {
44+
pagerRef.current?.setPage(keyToIndex.get(key)!);
45+
};
46+
47+
const onPageSelected = (e: PagerViewOnPageSelectedEvent) => {
48+
setActiveScrollId(indexToKey.get(e.nativeEvent.position)!);
49+
};
50+
51+
return (
52+
<HeaderMotion activeScrollId={activeScrollId.sv}>
53+
<HeaderMotion.Bridge>
54+
{(value) => (
55+
<Stack.Screen
56+
options={{
57+
header: () => (
58+
<HeaderMotion.NavigationBridge value={value}>
59+
<CollapsibleHeader
60+
activeTab={activeScrollId.state}
61+
onTabChange={handleTabPress}
62+
/>
63+
</HeaderMotion.NavigationBridge>
64+
),
65+
}}
66+
/>
67+
)}
68+
</HeaderMotion.Bridge>
69+
<PagerView
70+
ref={pagerRef}
71+
style={styles.pagerView}
72+
initialPage={0}
73+
onPageSelected={onPageSelected}
74+
>
75+
<View key="A">
76+
<HeaderMotionFlashList
77+
scrollId="A"
78+
data={content}
79+
keyExtractor={(item) => `${item.index}`}
80+
renderItem={({ item }) => (
81+
<ContentCard
82+
index={item.index}
83+
label={item.label}
84+
backgroundColor="#dcfce7"
85+
textColor="#14532d"
86+
/>
87+
)}
88+
/>
89+
</View>
90+
<View key="B">
91+
<HeaderMotionFlashList
92+
scrollId="B"
93+
data={content}
94+
keyExtractor={(item) => `${item.index}`}
95+
renderItem={({ item }) => (
96+
<ContentCard
97+
index={item.index}
98+
label={item.label}
99+
backgroundColor="#dcfce7"
100+
textColor="#14532d"
101+
/>
102+
)}
103+
/>
104+
</View>
105+
</PagerView>
106+
</HeaderMotion>
107+
);
108+
}
109+
110+
function CollapsibleHeader({
111+
activeTab,
112+
onTabChange,
113+
}: {
114+
activeTab: string;
115+
onTabChange: (newTab: string) => void;
116+
}) {
117+
const { progress, progressThreshold } = useMotionProgress();
118+
const insets = useSafeAreaInsets();
119+
120+
// 1. Container Animation (Moves UP)
121+
const containerStyle = useAnimatedStyle(() => {
122+
const threshold = progressThreshold.get();
123+
const translateY = interpolate(
124+
progress.get(),
125+
[0, 1],
126+
[0, -threshold],
127+
Extrapolation.CLAMP
128+
);
129+
return { transform: [{ translateY }] };
130+
});
131+
132+
// 2. Title Animation (Counter-Moves DOWN to stay sticky)
133+
const titleStyle = useAnimatedStyle(() => {
134+
const threshold = progressThreshold.get();
135+
const translateY = interpolate(
136+
progress.get(),
137+
[0, 1],
138+
[0, threshold],
139+
Extrapolation.CLAMP
140+
);
141+
return { transform: [{ translateY }] };
142+
});
143+
144+
// 3. Content Animation (Parallax + Opacity + Scale)
145+
const boxSectionStyle = useAnimatedStyle(() => {
146+
const threshold = progressThreshold.get();
147+
const parallaxTranslateY = interpolate(
148+
progress.get(),
149+
[0, 1],
150+
[0, threshold * 0.5],
151+
Extrapolation.CLAMP
152+
);
153+
const opacity = interpolate(
154+
progress.get(),
155+
[0, 1 * 0.6],
156+
[1, 0],
157+
Extrapolation.CLAMP
158+
);
159+
const scale = interpolate(
160+
progress.get(),
161+
[0, 1],
162+
[1, 0.8],
163+
Extrapolation.CLAMP
164+
);
165+
return {
166+
opacity,
167+
transform: [{ translateY: parallaxTranslateY }, { scale }],
168+
};
169+
});
170+
171+
return (
172+
<HeaderMotion.Header
173+
style={[styles.headerWrapper, { paddingTop: insets.top }, containerStyle]}
174+
>
175+
<Animated.View style={[titleStyle]}>
176+
<TitleWithSubtitle title="Title" subtitle="Subtitle" />
177+
</Animated.View>
178+
179+
<View style={styles.dynamicContent}>
180+
<HeaderMotion.Header.Dynamic
181+
style={[styles.boxContainer, boxSectionStyle]}
182+
>
183+
<DynamicBox />
184+
<DynamicBox />
185+
</HeaderMotion.Header.Dynamic>
186+
</View>
187+
188+
<View style={styles.tabBar}>
189+
<TabButton
190+
label="Page A"
191+
isActive={activeTab === 'A'}
192+
onPress={() => onTabChange('A')}
193+
/>
194+
<TabButton
195+
label="Page B"
196+
isActive={activeTab === 'B'}
197+
onPress={() => onTabChange('B')}
198+
/>
199+
</View>
200+
</HeaderMotion.Header>
201+
);
202+
}
203+
204+
const styles = StyleSheet.create({
205+
pagerView: {
206+
flex: 1,
207+
},
208+
dynamicContent: {
209+
overflow: 'hidden',
210+
},
211+
headerWrapper: {
212+
backgroundColor: '#304077',
213+
borderBottomWidth: 1,
214+
borderBottomColor: 'rgba(0,0,0,0.1)',
215+
},
216+
boxContainer: {
217+
flexDirection: 'row',
218+
gap: 6,
219+
padding: 12,
220+
alignItems: 'stretch',
221+
overflow: 'hidden',
222+
},
223+
tabBar: {
224+
flexDirection: 'row',
225+
backgroundColor: '#FFF',
226+
borderTopWidth: 1,
227+
borderTopColor: '#EEE',
228+
paddingBottom: 4,
229+
},
230+
pageLabel: {
231+
fontSize: 24,
232+
fontWeight: 'bold',
233+
padding: 20,
234+
textAlign: 'center',
235+
},
236+
});
237+
238+
const content = Array.from({ length: 500 }, (_, k) => ({
239+
index: k + 1,
240+
label: 'FlashList Item',
241+
}));

0 commit comments

Comments
 (0)