Skip to content

Commit

Permalink
feat: use measure to calculate dynamic layout
Browse files Browse the repository at this point in the history
  • Loading branch information
johnhaup committed May 11, 2024
1 parent fb00066 commit cb734a1
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 36 deletions.
5 changes: 3 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Dimensions, ScrollView, StyleSheet, Text, View } from 'react-native';
import { LazyScrollView } from 'react-native-lazy-scrollview';
import { ColorBlock } from './components/ColorBlock';
import shuffle from 'lodash/shuffle';
import { random } from 'lodash';

const ALBUMS = [
'https://audioxide.com/api/images/album-artwork/in-utero-nirvana-medium-square.jpg',
Expand All @@ -24,7 +25,7 @@ const OFFSETS = [-100, -500];

export default function App() {
const renderBlock = (uri: string | null, i: number) => (
<ColorBlock key={`child_${i}`} uri={uri} />
<ColorBlock key={`child_${i}`} uri={uri} nested={random(1) === 1} />
);

const renderScrollView = (offset: number) => (
Expand Down Expand Up @@ -56,7 +57,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 8,
},
scrollview: {
paddingVertical: 40,
// paddingVertical: 40,
},
offsetBar: {
position: 'absolute',
Expand Down
41 changes: 38 additions & 3 deletions example/src/components/ColorBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ const NO_LAZY_CHILD_BACKGROUNDS = [
'#1e90ff',
];

export function ColorBlock({ uri }: { uri: string | null }) {
export function ColorBlock({
uri,
nested,
}: {
uri: string | null;
nested?: boolean;
}) {
const [triggered, setTriggered] = useState(false);

const onThresholdPass = () => {
// Make api call
setTriggered(true);
};

if (!uri) {
const backgroundColor = sample(NO_LAZY_CHILD_BACKGROUNDS);
const backgroundColor = sample(NO_LAZY_CHILD_BACKGROUNDS);

if (!uri) {
return (
<View style={[styles.container, { backgroundColor }]}>
<Text style={styles.text}>
Expand All @@ -33,6 +39,25 @@ export function ColorBlock({ uri }: { uri: string | null }) {

const aspectRatio = triggered ? 1 / 2 : 1;

if (nested) {
return (
<View style={[styles.nested, { backgroundColor }]}>
<Text style={styles.text}>
I am not wrapped in LazyChild, but my child is!
</Text>
<LazyChild onThresholdPass={onThresholdPass}>
<View style={[styles.container, { aspectRatio }]}>
{triggered ? (
<Image source={{ uri }} style={styles.image} />
) : (
<ActivityIndicator />
)}
</View>
</LazyChild>
</View>
);
}

return (
<LazyChild onThresholdPass={onThresholdPass}>
<View style={[styles.container, { aspectRatio }]}>
Expand Down Expand Up @@ -63,4 +88,14 @@ const styles = StyleSheet.create({
color: 'white',
},
image: { width: '100%', height: '100%' },
nested: {
paddingVertical: 100,
borderWidth: 1,
borderRadius: 8,
marginVertical: 8,
},
nestedText: {
fontSize: 16,
textAlign: 'center',
},
});
28 changes: 17 additions & 11 deletions src/components/LazyChild.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import type { LayoutChangeEvent } from 'react-native';
import Animated, {
measure,
runOnJS,
useAnimatedReaction,
useAnimatedRef,
useSharedValue,
} from 'react-native-reanimated';
import { useAnimatedContext } from '../context/AnimatedContext';
Expand All @@ -19,8 +20,8 @@ export function LazyChild({
children: React.ReactNode;
onThresholdPass: () => void;
}) {
const { triggerValue, hasReachedEnd } = useAnimatedContext();
const topOfView = useSharedValue(0);
const { triggerValue, hasReachedEnd, scrollValue } = useAnimatedContext();
const _viewRef = useAnimatedRef<Animated.View>();
const hasFiredTrigger = useSharedValue(false);

const handleTrigger = () => {
Expand All @@ -32,7 +33,18 @@ export function LazyChild({

useAnimatedReaction(
() => {
return hasReachedEnd.value || triggerValue.value > topOfView.value;
const measurement = measure(_viewRef);

if (hasReachedEnd.value) {
return true;
}

// scrollValue only here to make the reactoin fire
if (measurement !== null && scrollValue.value > -1) {
return measurement.pageY < triggerValue.value && !hasFiredTrigger.value;
}

return false;
},
(hasPassedThreshold) => {
if (hasPassedThreshold) {
Expand All @@ -41,11 +53,5 @@ export function LazyChild({
}
);

const onLayout = (e: LayoutChangeEvent) => {
if (topOfView.value !== e.nativeEvent.layout.y) {
topOfView.value = e.nativeEvent.layout.y;
}
};

return <Animated.View onLayout={onLayout}>{children}</Animated.View>;
return <Animated.View ref={_viewRef}>{children}</Animated.View>;
}
51 changes: 31 additions & 20 deletions src/components/LazyScrollView.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import type { LayoutChangeEvent } from 'react-native';
import React, { useRef } from 'react';
import { View, type LayoutChangeEvent } from 'react-native';
import Animated, {
useAnimatedReaction,
useAnimatedRef,
useAnimatedScrollHandler,
useDerivedValue,
useScrollViewOffset,
useSharedValue,
} from 'react-native-reanimated';
import { AnimatedContext } from '../context/AnimatedContext';
Expand All @@ -29,22 +29,28 @@ export function LazyScrollView({
...rest
}: Props) {
const _scrollRef = useAnimatedRef<Animated.ScrollView>();
const _wrapperRef = useRef<View>(null);

const _offset = useSharedValue(injectedOffset || 0);
const _transY = useSharedValue(0);
const _containerHeight = useSharedValue(0);
const _contentHeight = useSharedValue(0);
const _distanceFromEnd = useDerivedValue(
() => _contentHeight.value - _transY.value - _containerHeight.value
);

const _scrollViewTopY = useSharedValue(0);
/**
* Starts at 0 and increases as the user scrolls down
*/
const scrollValue = useScrollViewOffset(_scrollRef);
const hasReachedEnd = useSharedValue(false);
const triggerValue = useDerivedValue(
() => _transY.value + _containerHeight.value + _offset.value
() => _containerHeight.value + _offset.value
);

useAnimatedReaction(
() => {
return _contentHeight.value > 0 && _distanceFromEnd.value <= 1;
if (!_contentHeight.value || !_containerHeight.value) {
return false;
}

return scrollValue.value >= _contentHeight.value - _containerHeight.value;
},
(reachedEnd) => {
if (reachedEnd && !hasReachedEnd.value) {
Expand All @@ -53,32 +59,37 @@ export function LazyScrollView({
}
);

const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
_transY.value = event.contentOffset.y;
_contentHeight.value = event.contentSize.height;
},
});

const onLayout = (e: LayoutChangeEvent) => {
_containerHeight.value = e.nativeEvent.layout.height;
_wrapperRef.current?.measureInWindow(
(_: number, y: number, _2: number, height: number) => {
_scrollViewTopY.value = y;
_contentHeight.value = height;
}
);
};

const onContentContainerLayout = (e: LayoutChangeEvent) => {
_contentHeight.value = e.nativeEvent.layout.height;
};

return (
<Animated.ScrollView
{...rest}
onLayout={onLayout}
onScroll={scrollHandler}
ref={_scrollRef}
scrollEventThrottle={16}
onLayout={onLayout}
>
<AnimatedContext.Provider
value={{
hasReachedEnd,
triggerValue,
scrollValue,
}}
>
{children}
<View ref={_wrapperRef} onLayout={onContentContainerLayout}>
{children}
</View>
</AnimatedContext.Provider>
</Animated.ScrollView>
);
Expand Down
1 change: 1 addition & 0 deletions src/context/AnimatedContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createContext, useContext } from 'react';
const initialContext = {
hasReachedEnd: { value: false },
triggerValue: { value: 0 },
scrollValue: { value: 0 },
};

export const AnimatedContext = createContext(initialContext);
Expand Down

0 comments on commit cb734a1

Please sign in to comment.