Skip to content

Commit db490d9

Browse files
committed
chore: use animation frame to time layout measure
1 parent 2915dbb commit db490d9

File tree

7 files changed

+50
-58
lines changed

7 files changed

+50
-58
lines changed

src/components/LazyChild/components/FullLazyChild.tsx

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { useAnimatedContext } from '../../../context/AnimatedContext';
1212
import { useEnteringCallbacks } from '../hooks/useEnteringCallbacks';
1313
import { useVisibilityCallbacks } from '../hooks/useVisibilityCallbacks';
1414
import { LazyChildProps } from '../types';
15-
import { FRAME_MS } from '../../../constants';
1615

1716
const SCREEN_HEIGHT = Dimensions.get('window').height;
1817

@@ -34,15 +33,15 @@ export function FullLazyChild({
3433
} = useAnimatedContext();
3534

3635
/**
37-
* If onLayout returns a height value greater than 0.
36+
* If onLayout returns a height or width value greater than 0.
3837
*/
39-
const _canMeasure = useSharedValue(false);
38+
const _isJsLayoutComplete = useSharedValue(false);
4039
/**
4140
* LazyChild view ref.
4241
*/
4342
const _viewRef = useAnimatedRef<Animated.View>();
4443
/**
45-
* Latest measure return.
44+
* Latest valid measure return or null.
4645
*/
4746
const _measurement = useSharedValue<ReturnType<typeof measure>>(null);
4847

@@ -59,32 +58,35 @@ export function FullLazyChild({
5958
typeof onVisibilityExit === 'function'
6059
);
6160

62-
const _shouldMeasure = useDerivedValue(
61+
const _hasValidCallback = useDerivedValue(
6362
() =>
6463
_shouldFireThresholdEnter.value ||
6564
_shouldFireThresholdExit.value ||
6665
_shouldMeasurePercentVisible.value ||
6766
_shouldFireVisibilityExit.value
6867
);
6968

69+
function measureView() {
70+
'worklet';
71+
const measurement = measure(_viewRef);
72+
73+
if (measurement && (measurement?.height || measurement?.width)) {
74+
_measurement.value = measurement;
75+
}
76+
}
77+
7078
useAnimatedReaction(
7179
() => {
7280
// Track scollValue to make reaction fire. SCREEN_HEIGHT negative is to generously allow for overscroll.
73-
if (
74-
_canMeasure.value &&
75-
_shouldMeasure.value &&
81+
return (
82+
_isJsLayoutComplete.value &&
83+
_hasValidCallback.value &&
7684
scrollValue.value > -SCREEN_HEIGHT
77-
) {
78-
const measurement = measure(_viewRef);
79-
80-
return measurement;
81-
}
82-
83-
return null;
85+
);
8486
},
85-
(measured) => {
86-
if (measured?.height || measured?.width) {
87-
_measurement.value = measured;
87+
(shouldMeasure) => {
88+
if (shouldMeasure) {
89+
measureView();
8890
}
8991
}
9092
);
@@ -113,19 +115,15 @@ export function FullLazyChild({
113115
});
114116

115117
const onLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
116-
// Don't measure until we know we have something. This prevents those pesky Android measurement warnings.
117-
// https://github.com/software-mansion/react-native-reanimated/blob/d8ef9c27c31dd2c32d4c3a2111326a448bf19ec9/packages/react-native-reanimated/src/platformFunctions/measure.ts#L95
118-
if (nativeEvent.layout.height > 0) {
119-
_canMeasure.value = true;
120-
// Sometimes native measure runs too quick and return 0 on first paint.
121-
setTimeout(() => {
118+
// Don't measure until we know we have something.
119+
if (nativeEvent.layout.height > 0 || nativeEvent.layout.width > 0) {
120+
// onLayout runs when RN finishes render, but native layout may not be fully settled until the next frame.
121+
requestAnimationFrame(() => {
122122
runOnUI(() => {
123-
const measurement = measure(_viewRef);
124-
if (measurement) {
125-
_measurement.value = measurement;
126-
}
123+
'worklet';
124+
_isJsLayoutComplete.value = true;
127125
})();
128-
}, FRAME_MS);
126+
});
129127
}
130128
// eslint-disable-next-line react-hooks/exhaustive-deps -- shared values do not trigger re-renders
131129
}, []);

src/components/LazyChild/hooks/useEnteringCallbacks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const useEnteringCallbacks = ({
3636
_hasFiredThresholdExited.value = false;
3737

3838
if (!_shouldFireThresholdExit.value) {
39+
// Enter callback has fired and there is no exit callback, so it cannot refire. Set shouldFire to false to prevent unnecessary measures.
3940
_shouldFireThresholdEnter.value = false;
4041
}
4142

@@ -58,7 +59,7 @@ export const useEnteringCallbacks = ({
5859
// eslint-disable-next-line react-hooks/exhaustive-deps -- shared values do not trigger re-renders
5960
}, [onExitThresholdPass]);
6061

61-
const isEntering = useDerivedValue(() => {
62+
const _hasEntered = useDerivedValue(() => {
6263
if (_measurement.value !== null) {
6364
const { pageX, pageY, width, height } = _measurement.value;
6465
const startOfView = horizontal.value ? pageX : pageY;
@@ -74,7 +75,7 @@ export const useEnteringCallbacks = ({
7475
});
7576

7677
useAnimatedReaction(
77-
() => isEntering.value,
78+
() => _hasEntered.value,
7879
(hasLazyChildEntered) => {
7980
if (hasLazyChildEntered) {
8081
if (_shouldFireThresholdEnter.value) {

src/components/LazyChild/hooks/useVisibilityCallbacks.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const useVisibilityCallbacks = ({
4242
_hasFiredOnVisibilityExited.value = false;
4343

4444
if (!_shouldFireVisibilityExit.value) {
45+
// Enter callback has fired and there is no exit callback, so it cannot refire. Set shouldFire to false to prevent unnecessary measures.
4546
_shouldMeasurePercentVisible.value = false;
4647
}
4748

@@ -69,7 +70,7 @@ export const useVisibilityCallbacks = ({
6970
[onVisibilityExit]
7071
);
7172

72-
const isVisible = useDerivedValue(() => {
73+
const _isVisible = useDerivedValue(() => {
7374
if (_measurement.value !== null) {
7475
const { pageX, pageY, width, height } = _measurement.value;
7576
const startOfView = horizontal.value ? pageX : pageY;
@@ -92,7 +93,7 @@ export const useVisibilityCallbacks = ({
9293
});
9394

9495
useAnimatedReaction(
95-
() => isVisible.value,
96+
() => _isVisible.value,
9697
(isLazyChildVisible) => {
9798
if (isLazyChildVisible) {
9899
if (_shouldMeasurePercentVisible.value) {

src/components/LazyChild/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './LazyChild';

src/components/LazyScrollView.tsx

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import Animated, {
2121
useSharedValue,
2222
} from 'react-native-reanimated';
2323
import { AnimatedContext } from '../context/AnimatedContext';
24-
import { FRAME_MS } from '../constants';
2524

2625
export interface LazyScrollViewMethods {
2726
scrollTo: typeof ScrollView.prototype.scrollTo;
@@ -107,25 +106,21 @@ const LazyScrollView = forwardRef<LazyScrollViewMethods, Props>(
107106
height: e.nativeEvent.layout.height,
108107
width: e.nativeEvent.layout.width,
109108
};
110-
try {
111-
// @ts-ignore measureInWindow is available on the direct ref
112-
// Add failure fallback because incorrect typings scare me
113-
_scrollRef.current?.measureInWindow((x: number, y: number) => {
114-
_containerCoordinates.value = { x, y: y + _statusBarHeight.value };
115-
});
116-
} catch (err) {
117-
setTimeout(() => {
118-
runOnUI(() => {
119-
const measurement = measure(_scrollRef);
120-
if (measurement) {
121-
_containerCoordinates.value = {
122-
x: measurement.pageX,
123-
y: measurement.pageY + _statusBarHeight.value,
124-
};
125-
}
126-
})();
127-
}, FRAME_MS);
128-
}
109+
110+
// onLayout runs when RN finishes render, but native layout may not be fully settled until the next frame.
111+
requestAnimationFrame(() => {
112+
runOnUI(() => {
113+
'worklet';
114+
const measurement = measure(_scrollRef);
115+
116+
if (measurement) {
117+
_containerCoordinates.value = {
118+
x: measurement.pageX,
119+
y: measurement.pageY + _statusBarHeight.value,
120+
};
121+
}
122+
})();
123+
});
129124
},
130125
// eslint-disable-next-line react-hooks/exhaustive-deps -- shared values do not trigger re-renders
131126
[]
@@ -158,7 +153,7 @@ const LazyScrollView = forwardRef<LazyScrollViewMethods, Props>(
158153

159154
return (
160155
<Animated.ScrollView
161-
scrollEventThrottle={FRAME_MS}
156+
scrollEventThrottle={16}
162157
{...rest}
163158
ref={_scrollRef}
164159
onLayout={measureScrollView}

src/constants.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)