Skip to content

Commit

Permalink
Add scrollTo examples for FlatList and FlashList (#6207)
Browse files Browse the repository at this point in the history
## Summary

This PR adds `FlatList` and `FlashList` examples in the example app
showing the usage of the `scrollTo` function. It also mentions the
`FlatList` (yet only this) support in docs.

## Example recordings

| Paper | Fabric |
|-|-|
| <video
src="https://github.com/software-mansion/react-native-reanimated/assets/52978053/2e38a4b0-56fa-4d3e-8ff2-4a1de5d75d62"
/> | <video
src="https://github.com/software-mansion/react-native-reanimated/assets/52978053/813d8b96-304b-4460-8a22-6fcbf722b2ca"
/> |

## Remarks

- `FlashList` supports `scrollTo` only on paper,
- fabric doesn't support it yet.
[This](Shopify/flash-list#1041) PR will fix the
issue.

---------

Co-authored-by: Tomek Zawadzki <tomasz.zawadzki@swmansion.com>
  • Loading branch information
MatiPl01 and tomekzaw authored Jul 9, 2024
1 parent 8ff5b88 commit 4bff676
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 41 deletions.
2 changes: 2 additions & 0 deletions apps/common-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@react-navigation/native": "*",
"@react-navigation/native-stack": "*",
"@react-navigation/stack": "*",
"@shopify/flash-list": "*",
"@stylexjs/babel-plugin": "*",
"d3-shape": "*",
"react": "*",
Expand All @@ -45,6 +46,7 @@
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"@react-navigation/stack": "^6.3.18",
"@shopify/flash-list": "^1.7.0",
"@stylexjs/babel-plugin": "^0.7.0",
"@tsconfig/react-native": "^3.0.0",
"@types/d3-shape": "^3.1.1",
Expand Down
24 changes: 16 additions & 8 deletions apps/common-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,28 @@ import type { HeaderBackButtonProps } from '@react-navigation/elements';
import { HeaderBackButton } from '@react-navigation/elements';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import type { NavigationProp, PathConfigMap } from '@react-navigation/native';
import type {
NavigationProp,
NavigationState,
PathConfigMap,
} from '@react-navigation/native';
import { NavigationContainer, useNavigation } from '@react-navigation/native';
import type { StackNavigationProp } from '@react-navigation/stack';
import { createStackNavigator } from '@react-navigation/stack';

import AsyncStorage from '@react-native-async-storage/async-storage';
import { EXAMPLES } from './examples';
import React from 'react';
import React, { useCallback } from 'react';
import { useReducedMotion } from 'react-native-reanimated';

function isFabric(): boolean {
return !!(global as Record<string, unknown>)._IS_FABRIC;
}

function noop() {
// do nothing
}

type RootStackParamList = { [P in keyof typeof EXAMPLES]: undefined } & {
Home: undefined;
};
Expand Down Expand Up @@ -200,11 +208,14 @@ export default function App() {
};

if (!isReady) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
restoreState();
restoreState().catch(noop);
}
}, [isReady]);

const persistNavigationState = useCallback((state?: NavigationState) => {
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state)).catch(noop);
}, []);

const shouldReduceMotion = useReducedMotion();

if (!isReady) {
Expand All @@ -220,10 +231,7 @@ export default function App() {
<NavigationContainer
linking={linking}
initialState={initialState}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onStateChange={(state) =>
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state))
}>
onStateChange={persistNavigationState}>
<Stack.Navigator>
<Stack.Screen
name="Home"
Expand Down
203 changes: 176 additions & 27 deletions apps/common-app/src/examples/ScrollToExample.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,210 @@
import type { AnimatedProps } from 'react-native-reanimated';
import Animated, {
runOnUI,
scrollTo,
useAnimatedRef,
} from 'react-native-reanimated';
import type { ListRenderItem as FlatListRenderItem } from 'react-native';
import { Button, StyleSheet, Switch, Text, View } from 'react-native';

import React from 'react';
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
} from 'react';
import type {
FlashListProps,
ListRenderItem as FlashListRenderItem,
} from '@shopify/flash-list';
import { FlashList } from '@shopify/flash-list';

const DATA = [...Array(100).keys()];

function getRandomOffset() {
'worklet';
return Math.random() * 2000;
}

const AnimatedFlashList = Animated.createAnimatedComponent(
FlashList
) as React.ComponentClass<AnimatedProps<FlashListProps<number>>>;

type Scrollable = {
scrollFromJS: () => void;
scrollFromUI: () => void;
};

export default function ScrollToExample() {
const [currentExample, setCurrentExample] = React.useState(0);
const [animated, setAnimated] = React.useState(true);

const aref = useAnimatedRef<Animated.ScrollView>();
const ref = useRef<Scrollable>(null);

const scrollFromJS = () => {
console.log(_WORKLET);
aref.current?.scrollTo({ y: Math.random() * 2000, animated });
};
const examples = [
{
title: 'ScrollView',
component: ScrollViewExample,
},
{
title: 'FlatList',
component: FlatListExample,
},
{
title: 'FlashList',
component: FlashListExample,
},
];

const scrollFromUI = () => {
runOnUI(() => {
console.log(_WORKLET);
scrollTo(aref, 0, Math.random() * 2000, animated);
})();
};
const Example = examples[currentExample].component;

return (
<>
<View style={styles.optionsRow}>
{examples.map(({ title }, index) => (
<Button
disabled={index === currentExample}
key={title}
title={title}
onPress={() => setCurrentExample(index)}
/>
))}
</View>
<View style={styles.buttons}>
<Switch
value={animated}
onValueChange={setAnimated}
style={styles.switch}
<View style={styles.optionsRow}>
<Text style={styles.switchLabel}>Animated</Text>
<Switch value={animated} onValueChange={setAnimated} />
</View>
<Button
onPress={() => ref.current?.scrollFromJS()}
title="Scroll from JS"
/>
<Button
onPress={() => ref.current?.scrollFromUI()}
title="Scroll from UI"
/>
<Button onPress={scrollFromJS} title="Scroll from JS" />
<Button onPress={scrollFromUI} title="Scroll from UI" />
</View>
<Animated.ScrollView ref={aref} style={styles.scrollView}>
{[...Array(100)].map((_, i) => (
<Example ref={ref} animated={animated} />
</>
);
}

type ExampleProps = {
animated: boolean;
};

const ScrollViewExample = forwardRef<Scrollable, ExampleProps>(
({ animated }, ref) => {
const aref = useAnimatedRef<Animated.ScrollView>();

useImperativeHandle(ref, () => ({
scrollFromJS() {
console.log(_WORKLET);
aref.current?.scrollTo({ y: getRandomOffset(), animated });
},
scrollFromUI() {
runOnUI(() => {
console.log(_WORKLET);
scrollTo(aref, 0, getRandomOffset(), animated);
})();
},
}));

return (
<Animated.ScrollView ref={aref} style={styles.fill}>
{DATA.map((_, i) => (
<Text key={i} style={styles.text}>
{i}
</Text>
))}
</Animated.ScrollView>
</>
);
}
);
}
);

const FlatListExample = forwardRef<Scrollable, ExampleProps>(
({ animated }, ref) => {
const aref = useAnimatedRef<Animated.FlatList<number>>();

useImperativeHandle(ref, () => ({
scrollFromJS() {
console.log(_WORKLET);
aref.current?.scrollToOffset({ offset: getRandomOffset(), animated });
},
scrollFromUI() {
runOnUI(() => {
console.log(_WORKLET);
scrollTo(aref, 0, getRandomOffset(), animated);
})();
},
}));

const renderItem = useCallback<FlatListRenderItem<number>>(
({ item }) => <Text style={styles.text}>{item}</Text>,
[]
);

return (
<Animated.FlatList
ref={aref}
renderItem={renderItem}
data={DATA}
style={styles.fill}
/>
);
}
);

const FlashListExample = forwardRef<Scrollable, ExampleProps>(
({ animated }, ref) => {
const aref = useAnimatedRef<FlashList<number>>();

useImperativeHandle(ref, () => ({
scrollFromJS() {
console.log(_WORKLET);
aref.current?.scrollToOffset({ offset: getRandomOffset(), animated });
},
scrollFromUI() {
runOnUI(() => {
console.log(_WORKLET);
scrollTo(aref, 0, getRandomOffset(), animated);
})();
},
}));

const renderItem = useCallback<FlashListRenderItem<number>>(
({ item }) => <Text style={styles.text}>{item}</Text>,
[]
);

return (
<AnimatedFlashList
ref={aref}
estimatedItemSize={60}
renderItem={renderItem}
data={DATA}
/>
);
}
);

const styles = StyleSheet.create({
switch: {
marginBottom: 10,
optionsRow: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 10,
marginVertical: 10,
},
switchLabel: {
fontSize: 20,
},
buttons: {
marginTop: 80,
marginTop: 20,
marginBottom: 40,
alignItems: 'center',
},
scrollView: {
fill: {
flex: 1,
width: '100%',
},
Expand Down
27 changes: 26 additions & 1 deletion apps/fabric-example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,27 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNFlashList (1.7.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
- React-Codegen
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNGestureHandler (2.16.2):
- DoubleConversion
- glog
Expand Down Expand Up @@ -1530,6 +1551,7 @@ DEPENDENCIES:
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
- "RNCMaskedView (from `../../../node_modules/@react-native-masked-view/masked-view`)"
- "RNCPicker (from `../../../node_modules/@react-native-picker/picker`)"
- "RNFlashList (from `../../../node_modules/@shopify/flash-list`)"
- RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`)
- RNReanimated (from `../../../node_modules/react-native-reanimated`)
- RNScreens (from `../../../node_modules/react-native-screens`)
Expand Down Expand Up @@ -1660,6 +1682,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/@react-native-masked-view/masked-view"
RNCPicker:
:path: "../../../node_modules/@react-native-picker/picker"
RNFlashList:
:path: "../../../node_modules/@shopify/flash-list"
RNGestureHandler:
:path: "../../../node_modules/react-native-gesture-handler"
RNReanimated:
Expand Down Expand Up @@ -1731,12 +1755,13 @@ SPEC CHECKSUMS:
RNCAsyncStorage: f2add1326156dc313df59d855c11f459059e4ffd
RNCMaskedView: 090213d32d8b3bb83a4dcb7d12c18f0152591906
RNCPicker: e84f13a98cbc8977870692948ccae15a389461bb
RNFlashList: c52e511c81ede0b8e44d4eb884b8a861976b111d
RNGestureHandler: 156548e18203327173a764c6932a3f52e90cb9cd
RNReanimated: 3a1b8b69bd5afffb33f65ed0d9f49f3c83bdaeb7
RNScreens: a68878603ae871d339f683bc683803545422986f
RNSVG: 02051bffb0b2fb2166e85009a58211643434ff63
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: ff1d575b119f510a5de23c22a794872562078ccf
Yoga: 56f906bf6c11c931588191dde1229fd3e4e3d557

PODFILE CHECKSUM: baaf1c2684753f6bff7da2866e138e1af1fe3499

Expand Down
1 change: 1 addition & 0 deletions apps/fabric-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@react-native-picker/picker": "^2.7.0",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"@shopify/flash-list": "^1.7.0",
"common-app": "workspace:*",
"react": "18.2.0",
"react-native": "0.74.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/macos-example/macos/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1255,7 +1255,7 @@ SPEC CHECKSUMS:
RNReanimated: cdf826862d35e6eed16a7a37478c71decca84760
RNSVG: 963a95f1f5d512a13d11ffd50d351c87fb5c6890
SocketRocket: f6c6249082c011e6de2de60ed641ef8bbe0cfac9
Yoga: 9beedd0c2748cf1d84279f1048486732ee1cb225
Yoga: dd0f2dde9c2bf2398b406740154bd99d5293aed8

PODFILE CHECKSUM: bc6f61f87c0dc3e6c1b90a39188b38c3dcd174b0

Expand Down
1 change: 1 addition & 0 deletions apps/macos-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@react-navigation/native": "^6.1.10",
"@react-navigation/native-stack": "^6.9.18",
"@react-navigation/stack": "^6.3.21",
"@shopify/flash-list": "^1.7.0",
"common-app": "workspace:*",
"react": "18.2.0",
"react-native": "0.73.4",
Expand Down
Loading

0 comments on commit 4bff676

Please sign in to comment.