-
Notifications
You must be signed in to change notification settings - Fork 66
/
VisibilityDetectingView.tsx
128 lines (108 loc) · 3.06 KB
/
VisibilityDetectingView.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* Copyright (c) Garuda Labs, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { Dimensions, View } from 'react-native';
import React, { PureComponent } from 'react';
import type { StyleProp, ViewStyle } from 'react-native';
import type { ElementRef } from 'react';
const TICK_INTERVAL = 100;
type Props = {
children?: React.ReactNode | undefined;
id: string;
onInvisible: (() => void | null | undefined) | null;
onVisible: () => void | null | undefined;
style: StyleProp<ViewStyle> | null | undefined;
};
/** A view that lets you know when its contents become visible/invisible in the screen.
* Useful for progressively loading content in a scroll view.
* Uses a timer internally to periodically check whether or not it is visible/invisible
*/
export default class VisibilityDetectingView extends PureComponent<Props> {
previouslyVisible = false;
tickInterval: ReturnType<typeof setTimeout> | null | undefined;
unmounted = false;
view: ElementRef<typeof View> | null | undefined;
onRef = (view?: ElementRef<typeof View> | null) => {
this.view = view;
};
onTick = () => {
if (this.unmounted) {
return;
}
// UIManager.measure may not exist during render-testing, which might break the
// `view.measure` call
if (this.view && this.view.measure) {
this.view.measure(this.onMeasure);
}
};
onMeasure = (
x: number,
y: number,
width: number,
height: number,
pageX: number,
pageY: number,
) => {
// Grab metrics
const windowDimensions = Dimensions.get('window');
const bottom = pageY + height;
const left = pageX;
const right = pageX + width;
const top = pageY;
const windowHeight = windowDimensions.height;
const windowWidth = windowDimensions.width;
// Calculate visibility
const visible =
right > 0 && left < windowWidth && top < windowHeight && bottom > 0;
// Either trigger onVisible or onInvisible
if (!this.previouslyVisible && visible) {
if (this.props.onVisible) {
this.props.onVisible();
}
} else if (this.previouslyVisible && !visible) {
if (this.props.onInvisible) {
this.props.onInvisible();
}
}
// Remember visibility
this.previouslyVisible = visible;
};
start = () => {
this.tickInterval = setInterval(this.onTick, TICK_INTERVAL);
};
stop = () => {
if (this.tickInterval) {
clearInterval(this.tickInterval);
}
};
componentDidMount() {
this.start();
}
componentWillUnmount() {
this.stop();
this.unmounted = true;
}
componentDidUpdate(prevProps: Props) {
if (prevProps.id !== this.props.id) {
this.stop();
this.previouslyVisible = false;
this.start();
}
}
render() {
return (
<View
ref={this.onRef}
// collapsable has to be false for view.measure to work on Android
collapsable={false}
style={this.props.style}
>
{this.props.children}
</View>
);
}
}