diff --git a/example/assets/albums/american-psycho.jpg b/example/assets/albums/american-psycho.jpg
new file mode 100644
index 0000000..fbaf766
Binary files /dev/null and b/example/assets/albums/american-psycho.jpg differ
diff --git a/example/assets/albums/doggstyle.jpg b/example/assets/albums/doggstyle.jpg
new file mode 100644
index 0000000..f464379
Binary files /dev/null and b/example/assets/albums/doggstyle.jpg differ
diff --git a/example/assets/albums/dude-ranch.jpg b/example/assets/albums/dude-ranch.jpg
new file mode 100644
index 0000000..be1d69d
Binary files /dev/null and b/example/assets/albums/dude-ranch.jpg differ
diff --git a/example/assets/albums/in_utero.jpg b/example/assets/albums/in_utero.jpg
new file mode 100644
index 0000000..50ade17
Binary files /dev/null and b/example/assets/albums/in_utero.jpg differ
diff --git a/example/assets/albums/is-this-it.jpg b/example/assets/albums/is-this-it.jpg
new file mode 100644
index 0000000..d491c10
Binary files /dev/null and b/example/assets/albums/is-this-it.jpg differ
diff --git a/example/assets/albums/let-it-be.jpg b/example/assets/albums/let-it-be.jpg
new file mode 100644
index 0000000..2df226f
Binary files /dev/null and b/example/assets/albums/let-it-be.jpg differ
diff --git a/example/assets/albums/rip-this.jpeg b/example/assets/albums/rip-this.jpeg
new file mode 100644
index 0000000..b2a3bbf
Binary files /dev/null and b/example/assets/albums/rip-this.jpeg differ
diff --git a/example/assets/albums/robbin-the-hood.jpg b/example/assets/albums/robbin-the-hood.jpg
new file mode 100644
index 0000000..ba77c1a
Binary files /dev/null and b/example/assets/albums/robbin-the-hood.jpg differ
diff --git a/example/assets/albums/spilt-milk.jpg b/example/assets/albums/spilt-milk.jpg
new file mode 100644
index 0000000..20d642e
Binary files /dev/null and b/example/assets/albums/spilt-milk.jpg differ
diff --git a/example/assets/albums/suffer.jpg b/example/assets/albums/suffer.jpg
new file mode 100644
index 0000000..f64f6d9
Binary files /dev/null and b/example/assets/albums/suffer.jpg differ
diff --git a/example/assets/albums/t-hives.jpg b/example/assets/albums/t-hives.jpg
new file mode 100644
index 0000000..f3f0364
Binary files /dev/null and b/example/assets/albums/t-hives.jpg differ
diff --git a/example/assets/albums/trendkill.jpg b/example/assets/albums/trendkill.jpg
new file mode 100644
index 0000000..f03e7a8
Binary files /dev/null and b/example/assets/albums/trendkill.jpg differ
diff --git a/example/assets/albums/wysiatwin.jpg b/example/assets/albums/wysiatwin.jpg
new file mode 100644
index 0000000..707694b
Binary files /dev/null and b/example/assets/albums/wysiatwin.jpg differ
diff --git a/example/assets/albums/youre-welcome.jpeg b/example/assets/albums/youre-welcome.jpeg
new file mode 100644
index 0000000..d91daa5
Binary files /dev/null and b/example/assets/albums/youre-welcome.jpeg differ
diff --git a/example/src/App.tsx b/example/src/App.tsx
index cd3185e..64e7c52 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,23 +1,26 @@
-import React from 'react';
+import React, { useCallback } from 'react';
import { random } from 'lodash';
import shuffle from 'lodash/shuffle';
-import { StyleSheet, Text, View } from 'react-native';
+import { ImageSourcePropType, StyleSheet, Text, View } from 'react-native';
import { LazyScrollView } from 'react-native-lazy-scrollview';
import { ColorBlock } from './components/ColorBlock';
-const ALBUMS = [
- 'https://audioxide.com/api/images/album-artwork/in-utero-nirvana-medium-square.jpg',
- 'https://audioxide.com/api/images/album-artwork/pinkerton-weezer-medium-square.jpg',
- 'https://static1.squarespace.com/static/55b7b1bde4b0828f7d1438de/t/6091766fa1c5b50917511c4b/1620145812302/WAVVES+-+Hideaway+-+Album+Cover+3000x3000.jpg?format=1500w',
- 'https://i.scdn.co/image/ab67616d0000b27313f2466b83507515291acce4',
- 'https://m.media-amazon.com/images/I/515NY3NS1EL._UF1000,1000_QL80_.jpg',
- 'https://images.squarespace-cdn.com/content/v1/5e270e203c421657bd3e3208/1584536470858-1OSWN7WUQ3DY749LGHOU/R-1843384-1282324049.jpeg.jpg',
+const ALBUMS: ImageSourcePropType = [
+ require('../assets/albums/american-psycho.jpg'),
+ require('../assets/albums/doggstyle.jpg'),
+ require('../assets/albums/dude-ranch.jpg'),
+ require('../assets/albums/in_utero.jpg'),
+ require('../assets/albums/is-this-it.jpg'),
+ require('../assets/albums/let-it-be.jpg'),
+ require('../assets/albums/rip-this.jpeg'),
+ require('../assets/albums/spilt-milk.jpg'),
+ require('../assets/albums/suffer.jpg'),
+ require('../assets/albums/t-hives.jpg'),
+ require('../assets/albums/trendkill.jpg'),
+ require('../assets/albums/wysiatwin.jpg'),
+ require('../assets/albums/youre-welcome.jpeg'),
null,
- 'https://m.media-amazon.com/images/I/61vMlYT58HL._UF1000,1000_QL80_.jpg',
- 'https://e.snmc.io/i/1200/s/9a0f51b8b776171aaee65c1a352f6d11/2396429',
- 'https://global-uploads.webflow.com/5fda4244c42e015b06d2fd8d/5fdbca8f15a8ee0f0f369767_cover.jpg',
- 'https://i.discogs.com/GXyIsX5gBpgw3XMtEZ-FvyVJBDZyk7Aoq-bPzV_yIXk/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTIyNDY1/MTUtMTI5OTE5MjI1/NS5qcGVn.jpeg',
null,
];
@@ -25,8 +28,11 @@ const OFFSET = -100;
const SHUFFLED_ALBUMS = shuffle(ALBUMS);
export default function App() {
- const renderBlock = (uri: string | null, i: number) => (
-
+ const renderBlock = useCallback(
+ (source: ImageSourcePropType | null, i: number) => (
+
+ ),
+ []
);
return (
@@ -66,7 +72,6 @@ const styles = StyleSheet.create({
opacity: 0.7,
height: 50,
justifyContent: 'flex-end',
- alignItems: 'center',
},
offsetText: {
color: 'white',
@@ -74,5 +79,6 @@ const styles = StyleSheet.create({
fontWeight: '600',
backgroundColor: '#000',
padding: 8,
+ alignSelf: 'flex-start',
},
});
diff --git a/example/src/components/ColorBlock.tsx b/example/src/components/ColorBlock.tsx
index af9a9a6..fd0b21e 100644
--- a/example/src/components/ColorBlock.tsx
+++ b/example/src/components/ColorBlock.tsx
@@ -1,6 +1,13 @@
import sample from 'lodash/sample';
import React, { useMemo, useState } from 'react';
-import { ActivityIndicator, Image, StyleSheet, Text, View } from 'react-native';
+import {
+ ActivityIndicator,
+ Image,
+ ImageSourcePropType,
+ StyleSheet,
+ Text,
+ View,
+} from 'react-native';
import { LazyChild } from 'react-native-lazy-scrollview';
const NO_LAZY_CHILD_BACKGROUNDS = [
@@ -11,33 +18,38 @@ const NO_LAZY_CHILD_BACKGROUNDS = [
'#1e90ff',
];
-const PERCENT = 0.75;
+const PERCENT = 0.9;
const PERCENT_STRING = `${PERCENT * 100}%`;
-const PERCENT_TEXT = `${PERCENT_STRING} threshold passed`;
+const PERCENT_TEXT = `${PERCENT_STRING} not visible`;
export function ColorBlock({
- uri,
+ source,
nested,
}: {
- uri: string | null;
+ source: ImageSourcePropType | null;
nested?: boolean;
}) {
const [triggered, setTriggered] = useState(false);
- const [percentTriggered, setPercentTriggered] = useState(false);
+ const [isVisible, setIsVisible] = useState(false);
const onThresholdPass = () => {
// Make api call
setTriggered(true);
};
- const onPercentVisibleThresholdPass = () => {
- // Make analytic call
- setPercentTriggered(true);
+ const onVisibilityEnter = () => {
+ console.log('ENTER', source?.toString());
+ setIsVisible(true);
+ };
+
+ const onVisibilityExit = () => {
+ console.log('EXIT', source?.toString());
+ setIsVisible(false);
};
const backgroundColor = useMemo(() => sample(NO_LAZY_CHILD_BACKGROUNDS), []);
- if (!uri) {
+ if (!source) {
return (
@@ -55,19 +67,21 @@ export function ColorBlock({
{triggered ? (
-
+
) : (
)}
- {percentTriggered && (
- {PERCENT_TEXT}
+ {!isVisible && (
+
+ {PERCENT_TEXT}
+
)}
-
@@ -77,19 +91,21 @@ export function ColorBlock({
return (
{triggered ? (
-
+
) : (
)}
- {percentTriggered && (
- {PERCENT_TEXT}
+ {!isVisible && (
+
+ {PERCENT_TEXT}
+
)}
-
);
@@ -125,22 +141,20 @@ const styles = StyleSheet.create({
backgroundColor: 'white',
margin: 16,
},
- percentText: {
+ percentTextWrapper: {
position: 'absolute',
top: 0,
right: 0,
- backgroundColor: 'rgba(255, 255, 255, 0.8)',
+ bottom: 0,
+ left: 0,
+ backgroundColor: 'rgba(255, 255, 255, 0.5)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ percentText: {
color: 'red',
fontSize: 16,
padding: 8,
- width: '100%',
- },
- percentLine: {
- position: 'absolute',
- top: PERCENT_STRING,
- left: 0,
- right: 0,
- height: 2,
- backgroundColor: 'red',
+ textAlign: 'center',
},
});
diff --git a/package.json b/package.json
index 6179e94..57f9495 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-lazy-scrollview",
- "version": "0.9.0",
+ "version": "0.9.1",
"description": "Lazy ScrollView for React Native.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
diff --git a/src/components/LazyChild.tsx b/src/components/LazyChild.tsx
index 0ef6551..94badf2 100644
--- a/src/components/LazyChild.tsx
+++ b/src/components/LazyChild.tsx
@@ -8,7 +8,6 @@ import Animated, {
useSharedValue,
} from 'react-native-reanimated';
import { useAnimatedContext } from '../context/AnimatedContext';
-import { Platform } from 'react-native';
interface Props {
children: React.ReactNode;
@@ -23,10 +22,15 @@ interface Props {
/**
* Callback to fire when the LazyChild's viewable area exceeds the percentVisibleThreshold.
*/
- onPercentVisibleThresholdPass?: () => void;
+ onVisibilityEnter?: () => void;
+ /**
+ * Callback to fire when the LazyChild's viewable area goes under the percentVisibleThreshold after being above it.
+ */
+ onVisibilityExit?: () => void;
/**
* Protects against firing callback on measurement with zero value. Default is true. Good to set to false if you know the LazyChild is the first item in the LazyScrollview.
*/
+ // TODO Is there a way to use height here? I know this is re: 0 as a Y measurement and the issue is with the views all starting at 0. Need a more reliable way to check if the view is at the top of the scrollview or hasn't rendered properly yet.
ignoreZeroMeasurement?: boolean;
}
@@ -37,21 +41,16 @@ export function LazyChild({
children,
onThresholdPass,
percentVisibleThreshold = 1,
- onPercentVisibleThresholdPass,
ignoreZeroMeasurement = true,
+ onVisibilityEnter,
+ onVisibilityExit,
}: Props) {
- const { triggerValue, hasReachedEnd, scrollValue, bottomYValue } =
+ const { triggerValue, hasReachedEnd, scrollValue, topYValue, bottomYValue } =
useAnimatedContext();
const _viewRef = useAnimatedRef();
const _hasFiredScrollViewThresholdTrigger = useSharedValue(false);
const _ignoreZeroMeasurement = useSharedValue(ignoreZeroMeasurement);
- const _isAndroid = useSharedValue(Platform.OS === 'android');
- const _canMeasure = useDerivedValue(
- // https://github.com/software-mansion/react-native-reanimated/issues/5006#issuecomment-1826495797
- // Running same check on iOS sometimes causes the view to not be measured
- () => !_isAndroid.value || (_viewRef.current && _isAndroid.value)
- );
const handleScrollViewThresholdPass = useCallback(() => {
if (!_hasFiredScrollViewThresholdTrigger.value) {
@@ -70,10 +69,6 @@ export function LazyChild({
return true;
}
- if (!_canMeasure) {
- return false;
- }
-
const measurement = measure(_viewRef);
// Track scollValue to make reaction fire
@@ -98,68 +93,93 @@ export function LazyChild({
);
const _shouldMeasurePercentVisible = useSharedValue(
- typeof onPercentVisibleThresholdPass === 'function'
+ typeof onVisibilityEnter === 'function'
+ );
+ const _shouldFireVisibilityExit = useSharedValue(
+ typeof onVisibilityExit === 'function'
);
const _percentVisibleTrigger = useSharedValue(percentVisibleThreshold);
- const _hasFiredPercentVisibleTrigger = useSharedValue(false);
+ const _hasFiredOnVisibilityEntered = useSharedValue(false);
+ const _hasFiredOnVisibilityExited = useSharedValue(false);
+
+ const handleOnVisibilityEntered = useCallback(() => {
+ if (onVisibilityEnter && !_hasFiredOnVisibilityEntered.value) {
+ _hasFiredOnVisibilityEntered.value = true;
+ _hasFiredOnVisibilityExited.value = false;
+ onVisibilityEnter();
+ }
+ }, [
+ _hasFiredOnVisibilityEntered,
+ _hasFiredOnVisibilityExited,
+ onVisibilityEnter,
+ ]);
- const handlePercentTrigger = useCallback(() => {
+ const handleOnVisibilityExited = useCallback(() => {
if (
- !_hasFiredPercentVisibleTrigger.value &&
- onPercentVisibleThresholdPass
+ onVisibilityExit &&
+ _hasFiredOnVisibilityEntered.value &&
+ !_hasFiredOnVisibilityExited.value
) {
- _hasFiredPercentVisibleTrigger.value = true;
- onPercentVisibleThresholdPass();
+ _hasFiredOnVisibilityEntered.value = false;
+ _hasFiredOnVisibilityExited.value = true;
+ onVisibilityExit();
}
- }, [_hasFiredPercentVisibleTrigger, onPercentVisibleThresholdPass]);
-
- useAnimatedReaction(
- () => {
- if (!_shouldMeasurePercentVisible) {
- return false;
- }
-
- if (_hasFiredPercentVisibleTrigger.value) {
- return false;
- }
-
- if (hasReachedEnd.value) {
- return true;
- }
-
- if (!_canMeasure) {
- return false;
- }
-
+ }, [
+ _hasFiredOnVisibilityEntered,
+ _hasFiredOnVisibilityExited,
+ onVisibilityExit,
+ ]);
+
+ const isVisible = useDerivedValue(() => {
+ if (_WORKLET) {
const measurement = measure(_viewRef);
// Track scollValue to make reaction fire
if (measurement !== null && scrollValue.value > -1) {
- if (_ignoreZeroMeasurement.value && measurement.pageY === 0) {
+ const topOfView = measurement.pageY;
+ const bottomOfView = measurement.pageY + measurement.height;
+
+ if (_ignoreZeroMeasurement.value && topOfView === 0) {
return false;
}
- const percentOffset = measurement.height * _percentVisibleTrigger.value;
- const percentTrigger = bottomYValue.value - percentOffset;
+ const visibilityHeight =
+ measurement.height * _percentVisibleTrigger.value;
+ const visibleEnterTrigger = bottomYValue.value - visibilityHeight;
+ const visibleExitTrigger = topYValue.value + visibilityHeight;
- if (percentTrigger <= 0) {
+ if (visibleEnterTrigger <= 0) {
return false;
}
return (
- measurement.pageY < percentTrigger &&
- !_hasFiredPercentVisibleTrigger.value
+ topOfView < visibleEnterTrigger && bottomOfView > visibleExitTrigger
);
}
+ }
- return false;
- },
- (shouldFirePercentTrigger) => {
- if (shouldFirePercentTrigger) {
- runOnJS(handlePercentTrigger)();
+ return false;
+ });
+
+ useAnimatedReaction(
+ () => isVisible.value,
+ (isLazyChildVisible) => {
+ if (isLazyChildVisible) {
+ if (_shouldMeasurePercentVisible.value) {
+ runOnJS(handleOnVisibilityEntered)();
+ }
+ } else {
+ if (_shouldFireVisibilityExit.value) {
+ runOnJS(handleOnVisibilityExited)();
+ }
}
}
);
- return {children};
+ return (
+ // https://github.com/software-mansion/react-native-reanimated/blob/d8ef9c27c31dd2c32d4c3a2111326a448bf19ec9/packages/react-native-reanimated/src/platformFunctions/measure.ts#L56
+
+ {children}
+
+ );
}
diff --git a/src/components/LazyScrollView.tsx b/src/components/LazyScrollView.tsx
index c97f146..ddffe28 100644
--- a/src/components/LazyScrollView.tsx
+++ b/src/components/LazyScrollView.tsx
@@ -33,20 +33,21 @@ export function LazyScrollView({
const _offset = useSharedValue(injectedOffset || 0);
const _containerHeight = useSharedValue(0);
const _contentHeight = useSharedValue(0);
- const _scrollViewTopY = useSharedValue(0);
/**
* Starts at 0 and increases as the user scrolls down
*/
const scrollValue = useScrollViewOffset(_scrollRef);
const hasReachedEnd = useDerivedValue(() => {
if (!_contentHeight.value || !_containerHeight.value) {
+ // Container and contend measurements have not completed
return false;
}
return scrollValue.value >= _contentHeight.value - _containerHeight.value;
});
+ const topYValue = useSharedValue(0);
const bottomYValue = useDerivedValue(
- () => _containerHeight.value + _scrollViewTopY.value
+ () => _containerHeight.value + topYValue.value
);
const triggerValue = useDerivedValue(
() => bottomYValue.value + _offset.value
@@ -57,12 +58,12 @@ export function LazyScrollView({
_containerHeight.value = e.nativeEvent.layout.height;
_wrapperRef.current?.measureInWindow(
(_: number, y: number, _2: number, height: number) => {
- _scrollViewTopY.value = y;
+ topYValue.value = y;
_contentHeight.value = height;
}
);
},
- [_containerHeight, _contentHeight, _scrollViewTopY]
+ [_containerHeight, _contentHeight, topYValue]
);
const onContentContainerLayout = useCallback(
@@ -78,7 +79,6 @@ export function LazyScrollView({
ref={_scrollRef}
scrollEventThrottle={16}
onLayout={onLayout}
- // TODO handle x & width values if horizontal is true
horizontal={false}
>
diff --git a/src/context/AnimatedContext.ts b/src/context/AnimatedContext.ts
index 963c6a9..b2c5142 100644
--- a/src/context/AnimatedContext.ts
+++ b/src/context/AnimatedContext.ts
@@ -5,6 +5,7 @@ const initialContext = {
triggerValue: { value: 0 },
scrollValue: { value: 0 },
bottomYValue: { value: 0 },
+ topYValue: { value: 0 },
};
export const AnimatedContext = createContext(initialContext);