Skip to content

Commit

Permalink
feat: expose scroll methods in LazyScrollview ref
Browse files Browse the repository at this point in the history
  • Loading branch information
johnhaup committed Oct 4, 2024
1 parent 5956845 commit 1390966
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 75 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,27 @@ This library requires reanimated. Follow their [installation instructions](https
```js
// MyCoolHomeScreen.tsx
import { LazyScrollView } from 'react-native-lazy-scrollview';
import { CoolComponentA, CoolComponentB, CoolComponentC } from './components';
import {
CoolComponentA,
CoolComponentB,
CoolComponentC,
PriceMasterVideo,
ScrollToTopButton,
} from './components';

export function MyCoolHomeScreen() {
const ref = useRef<LazyScrollViewMethods>(null);

return (
// Trigger onThresholdReached when child is 300 pixels below the bottom
<LazyScrollView offset={300} showsVerticalScrollIndicator={false}>
<CoolComponentA />
<VideoPlayer />
<PriceMasterVideo />
<CoolComponentB />
<CoolComponentC />
<ScrollToTopButton
onPress={() => ref.current?.scrollToStart({ animated: true })}
/>
</LazyScrollView>
);
}
Expand Down
34 changes: 32 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';

import { random } from 'lodash';
import shuffle from 'lodash/shuffle';
Expand All @@ -10,7 +10,10 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import { LazyScrollView } from 'react-native-lazy-scrollview';
import {
LazyScrollView,
LazyScrollViewMethods,
} from 'react-native-lazy-scrollview';
import { ColorBlock } from './components/ColorBlock';

const ALBUMS: ImageSourcePropType = [
Expand All @@ -35,6 +38,8 @@ const OFFSET = -100;
const SHUFFLED_ALBUMS = shuffle(ALBUMS);

function VerticalScrollView() {
const ref = useRef<LazyScrollViewMethods>(null);

const renderBlock = useCallback(
(source: ImageSourcePropType | null, i: number) => (
<ColorBlock key={`child_${i}`} source={source} nested={random(1) === 1} />
Expand All @@ -45,12 +50,28 @@ function VerticalScrollView() {
return (
<View style={styles.scrollviewContainer}>
<LazyScrollView
ref={ref}
contentContainerStyle={styles.scrollview}
offset={OFFSET}
showsVerticalScrollIndicator={false}
>
{SHUFFLED_ALBUMS.map(renderBlock)}
</LazyScrollView>
<View style={styles.arrowsContainer}>
<TouchableOpacity
style={styles.arrowButton}
activeOpacity={0.7}
onPress={() => ref.current?.scrollToStart({ animated: true })}
>
<Text style={styles.arrow}>⬆️</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => ref.current?.scrollToEnd({ animated: true })}
>
<Text style={styles.arrow}>⬇️</Text>
</TouchableOpacity>
</View>
<View style={styles.offsetBar}>
<Text style={styles.offsetText}>{`Offset: ${OFFSET}`}</Text>
</View>
Expand Down Expand Up @@ -182,4 +203,13 @@ const styles = StyleSheet.create({
marginBottom: 16,
marginTop: PADDING_VERTICAL * 2,
},
arrowsContainer: {
top: 0,
bottom: 0,
right: 0,
position: 'absolute',
justifyContent: 'center',
},
arrowButton: { marginBottom: 8 },
arrow: { fontSize: 32 },
});
179 changes: 108 additions & 71 deletions src/components/LazyScrollView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import React, { useCallback, useRef } from 'react';
import { StatusBar, View, type LayoutChangeEvent } from 'react-native';
import React, {
MutableRefObject,
forwardRef,
useCallback,
useImperativeHandle,
useRef,
} from 'react';
import {
ScrollView,
StatusBar,
View,
type LayoutChangeEvent,
} from 'react-native';
import Animated, {
useAnimatedRef,
useDerivedValue,
Expand All @@ -8,12 +19,22 @@ import Animated, {
} from 'react-native-reanimated';
import { AnimatedContext } from '../context/AnimatedContext';

export interface LazyScrollViewMethods {
scrollTo: typeof ScrollView.prototype.scrollTo;
scrollToStart: typeof ScrollView.prototype.scrollToEnd;
scrollToEnd: typeof ScrollView.prototype.scrollToEnd;
}

export interface LazyScrollViewProps {
/**
* How far above or below the bottom of the ScrollView the threshold trigger is. Negative is above, postive it below. Accepts [ScrollView props](https://reactnative.dev/docs/scrollview).
* @defaultValue 0 (bottom of ScrollView)
*/
offset?: number;
/**
* Ref to the LazyScrollView. Exposes scrollTo, scrollToStart, and scrollToEnd methods.
*/
ref?: MutableRefObject<LazyScrollViewMethods>;
}

type Props = LazyScrollViewProps &
Expand All @@ -25,79 +46,95 @@ type Props = LazyScrollViewProps &
/**
* ScrollView to wrap Lazy Children in.
*/
export function LazyScrollView({
children,
offset: injectedOffset,
...rest
}: Props) {
const _scrollRef = useAnimatedRef<Animated.ScrollView>();
const _wrapperRef = useRef<View>(null);
const _offset = useSharedValue(injectedOffset || 0);
const _containerHeight = useSharedValue(0);
const _contentHeight = useSharedValue(0);
const _hasProvider = useSharedValue(true);
const LazyScrollView = forwardRef<LazyScrollViewMethods, Props>(
({ children, offset: injectedOffset, ...rest }, ref) => {
const _scrollRef = useAnimatedRef<Animated.ScrollView>();
const _wrapperRef = useRef<View>(null);
const _offset = useSharedValue(injectedOffset || 0);
const _containerHeight = useSharedValue(0);
const _contentHeight = useSharedValue(0);
const _hasProvider = useSharedValue(true);

/**
* Starts at 0 and increases as the user scrolls down
*/
const scrollValue = useScrollViewOffset(_scrollRef);
useImperativeHandle(ref, () => ({
scrollTo: (options) => {
_scrollRef.current?.scrollTo(options);
},
scrollToStart: (options) => {
_scrollRef.current?.scrollTo({
x: 0,
y: 0,
animated: options?.animated,
});
},
scrollToEnd: (options) => {
_scrollRef.current?.scrollToEnd(options);
},
}));

/**
* Starts at 0 and increases as the user scrolls down
*/
const scrollValue = useScrollViewOffset(_scrollRef);

const topYValue = useSharedValue(0);
const bottomYValue = useDerivedValue(
() => _containerHeight.value + topYValue.value
);
const topYValue = useSharedValue(0);
const bottomYValue = useDerivedValue(
() => _containerHeight.value + topYValue.value
);

const topTriggerValue = useDerivedValue(
() => topYValue.value - _offset.value
);
const bottomTriggerValue = useDerivedValue(
() => bottomYValue.value + _offset.value
);
const topTriggerValue = useDerivedValue(
() => topYValue.value - _offset.value
);
const bottomTriggerValue = useDerivedValue(
() => bottomYValue.value + _offset.value
);

const onLayout = useCallback(
(e: LayoutChangeEvent) => {
_containerHeight.value = e.nativeEvent.layout.height;
_wrapperRef.current?.measureInWindow(
(_: number, y: number, _2: number, height: number) => {
topYValue.value = y + (StatusBar.currentHeight || 0);
_contentHeight.value = height;
}
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- shared values do not trigger re-renders
[]
);
const onLayout = useCallback(
(e: LayoutChangeEvent) => {
_containerHeight.value = e.nativeEvent.layout.height;
_wrapperRef.current?.measureInWindow(
(_: number, y: number, _2: number, height: number) => {
topYValue.value = y + (StatusBar.currentHeight || 0);
_contentHeight.value = height;
}
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- shared values do not trigger re-renders
[]
);

const onContentContainerLayout = useCallback(
(e: LayoutChangeEvent) => {
_contentHeight.value = e.nativeEvent.layout.height;
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- shared values do not trigger re-renders
[]
);
const onContentContainerLayout = useCallback(
(e: LayoutChangeEvent) => {
_contentHeight.value = e.nativeEvent.layout.height;
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- shared values do not trigger re-renders
[]
);

return (
<Animated.ScrollView
horizontal={false}
scrollEventThrottle={16}
{...rest}
ref={_scrollRef}
onLayout={onLayout}
>
<AnimatedContext.Provider
value={{
_hasProvider,
scrollValue,
topYValue,
bottomYValue,
topTriggerValue,
bottomTriggerValue,
}}
return (
<Animated.ScrollView
horizontal={false}
scrollEventThrottle={16}
{...rest}
ref={_scrollRef}
onLayout={onLayout}
>
<View ref={_wrapperRef} onLayout={onContentContainerLayout}>
{children}
</View>
</AnimatedContext.Provider>
</Animated.ScrollView>
);
}
<AnimatedContext.Provider
value={{
_hasProvider,
scrollValue,
topYValue,
bottomYValue,
topTriggerValue,
bottomTriggerValue,
}}
>
<View ref={_wrapperRef} onLayout={onContentContainerLayout}>
{children}
</View>
</AnimatedContext.Provider>
</Animated.ScrollView>
);
}
);

export { LazyScrollView };

0 comments on commit 1390966

Please sign in to comment.