From cb734a1f50c5473363d2d7040ba2af66a29c9257 Mon Sep 17 00:00:00 2001 From: John Haupenthal Date: Fri, 10 May 2024 21:23:43 -0700 Subject: [PATCH] feat: use measure to calculate dynamic layout --- example/src/App.tsx | 5 +-- example/src/components/ColorBlock.tsx | 41 +++++++++++++++++++-- src/components/LazyChild.tsx | 28 +++++++++------ src/components/LazyScrollView.tsx | 51 ++++++++++++++++----------- src/context/AnimatedContext.ts | 1 + 5 files changed, 90 insertions(+), 36 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 6807629..80b2b6d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -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', @@ -24,7 +25,7 @@ const OFFSETS = [-100, -500]; export default function App() { const renderBlock = (uri: string | null, i: number) => ( - + ); const renderScrollView = (offset: number) => ( @@ -56,7 +57,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 8, }, scrollview: { - paddingVertical: 40, + // paddingVertical: 40, }, offsetBar: { position: 'absolute', diff --git a/example/src/components/ColorBlock.tsx b/example/src/components/ColorBlock.tsx index 70a0adf..c81840e 100644 --- a/example/src/components/ColorBlock.tsx +++ b/example/src/components/ColorBlock.tsx @@ -11,7 +11,13 @@ 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 = () => { @@ -19,9 +25,9 @@ export function ColorBlock({ uri }: { uri: string | null }) { setTriggered(true); }; - if (!uri) { - const backgroundColor = sample(NO_LAZY_CHILD_BACKGROUNDS); + const backgroundColor = sample(NO_LAZY_CHILD_BACKGROUNDS); + if (!uri) { return ( @@ -33,6 +39,25 @@ export function ColorBlock({ uri }: { uri: string | null }) { const aspectRatio = triggered ? 1 / 2 : 1; + if (nested) { + return ( + + + I am not wrapped in LazyChild, but my child is! + + + + {triggered ? ( + + ) : ( + + )} + + + + ); + } + return ( @@ -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', + }, }); diff --git a/src/components/LazyChild.tsx b/src/components/LazyChild.tsx index 58229cf..820b542 100644 --- a/src/components/LazyChild.tsx +++ b/src/components/LazyChild.tsx @@ -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'; @@ -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(); const hasFiredTrigger = useSharedValue(false); const handleTrigger = () => { @@ -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) { @@ -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 {children}; + return {children}; } diff --git a/src/components/LazyScrollView.tsx b/src/components/LazyScrollView.tsx index 625fcfa..a292075 100644 --- a/src/components/LazyScrollView.tsx +++ b/src/components/LazyScrollView.tsx @@ -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'; @@ -29,22 +29,28 @@ export function LazyScrollView({ ...rest }: Props) { const _scrollRef = useAnimatedRef(); + const _wrapperRef = useRef(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) { @@ -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 ( - {children} + + {children} + ); diff --git a/src/context/AnimatedContext.ts b/src/context/AnimatedContext.ts index db5d8cb..c2f6bbc 100644 --- a/src/context/AnimatedContext.ts +++ b/src/context/AnimatedContext.ts @@ -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);