@tamagui/react-native-web-lite
Version:
React Native for Web
396 lines (347 loc) • 10.1 kB
JSX
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { invariant } from '@tamagui/react-native-web-internals'
import ScrollView from '../../../ScrollView'
import RefreshControl from '../../../RefreshControl'
import Batchinator from '../Batchinator'
import ChildListCollection from './ChildListCollection'
import FillRateHelper from '../FillRateHelper'
import StateSafePureComponent from './StateSafePureComponent'
import ViewabilityHelper from '../ViewabilityHelper'
import CellRenderer from './VirtualizedListCellRenderer'
import {
VirtualizedListContext
} from './VirtualizedListContext'
const __DEV__ = process.env.NODE_ENV !== 'production'
const ON_EDGE_REACHED_EPSILON = 0.001
let _usedIndexForKey = false
let _keylessItemComponentName = ''
const defaultViewabilityConfig = {
viewabilityHelper: null,
onViewableItemsChanged: null,
}
const defaultRenderMask = {
renderMask: null,
cellsAroundViewport: { first: 0, last: 0 },
}
// Default Props Helper Functions
function horizontalOrDefault(horizontal) {
return horizontal ?? false
}
function initialNumToRenderOrDefault(initialNumToRender) {
return initialNumToRender ?? 10
}
function maxToRenderPerBatchOrDefault(maxToRenderPerBatch) {
return maxToRenderPerBatch ?? 10
}
function onStartReachedThresholdOrDefault(onStartReachedThreshold) {
return onStartReachedThreshold ?? 2
}
function onEndReachedThresholdOrDefault(onEndReachedThreshold) {
return onEndReachedThreshold ?? 2
}
function getScrollingThreshold(threshold, visibleLength) {
return (threshold * visibleLength) / 2
}
function scrollEventThrottleOrDefault(scrollEventThrottle) {
return scrollEventThrottle ?? 50
}
function windowSizeOrDefault(windowSize) {
return windowSize ?? 21
}
function findLastWhere(arr, predicate) {
for (let i = arr.length - 1; i >= 0; i--) {
if (predicate(arr[i])) {
return arr[i]
}
}
return null
}
class VirtualizedList extends StateSafePureComponent {
static contextType = VirtualizedListContext
constructor(props) {
super(props)
this._nestedChildLists = new ChildListCollection()
this._viewabilityTuples = []
this._scrollMetrics = {
contentLength: 0,
dOffset: 0,
dt: 10,
offset: 0,
timestamp: 0,
velocity: 0,
visibleLength: 0,
}
this._highestMeasuredFrameIndex = 0
this._headerLength = 0
this._footerLength = 0
this._averageCellLength = 0
this._hasWarned = {}
this._fillRateHelper = new FillRateHelper(this._getFrameMetrics)
this._updateCellsToRenderBatcher = new Batchinator(
this._updateCellsToRender,
this.props.updateCellsBatchingPeriod ?? 50
)
if (this.props.viewabilityConfig && this.props.onViewableItemsChanged) {
this._viewabilityTuples.push({
viewabilityHelper: new ViewabilityHelper(this.props.viewabilityConfig),
onViewableItemsChanged: this.props.onViewableItemsChanged,
})
}
}
scrollToEnd(params) {
const animated = params ? params.animated : true
const veryLast = this.props.getItemCount(this.props.data) - 1
if (veryLast < 0) {
return
}
const frame = this.__getFrameMetricsApprox(veryLast, this.props)
const offset = Math.max(
0,
frame.offset +
frame.length +
this._footerLength -
this._scrollMetrics.visibleLength
)
if (this._scrollRef == null) {
return
}
if (this._scrollRef.scrollTo == null) {
console.warn(
'No scrollTo method provided. This may be because you have two nested ' +
'VirtualizedLists with the same orientation, or because you are ' +
'using a custom component that does not implement scrollTo.'
)
return
}
this._scrollRef.scrollTo(
horizontalOrDefault(this.props.horizontal)
? { x: offset, animated }
: { y: offset, animated }
)
}
scrollToIndex(params) {
const {
data,
horizontal,
getItemCount,
getItemLayout,
onScrollToIndexFailed,
} = this.props
const { animated, index, viewOffset, viewPosition } = params
invariant(
index >= 0,
`scrollToIndex out of range: requested index ${index} but minimum is 0`
)
invariant(
getItemCount(data) >= 1,
`scrollToIndex out of range: item length ${getItemCount(
data
)} but minimum is 1`
)
invariant(
index < getItemCount(data),
`scrollToIndex out of range: requested index ${index} is out of 0 to ${
getItemCount(data) - 1
}`
)
if (!getItemLayout && index > this._highestMeasuredFrameIndex) {
invariant(
!!onScrollToIndexFailed,
'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' +
'otherwise there is no way to know the location of offscreen indices or handle failures.'
)
onScrollToIndexFailed({
averageItemLength: this._averageCellLength,
highestMeasuredFrameIndex: this._highestMeasuredFrameIndex,
index,
})
return
}
const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props)
const offset =
Math.max(
0,
this._getOffsetApprox(index, this.props) -
(viewPosition || 0) *
(this._scrollMetrics.visibleLength - frame.length)
) - (viewOffset || 0)
if (this._scrollRef == null) {
return
}
this._scrollRef.scrollTo(
horizontalOrDefault(horizontal)
? { x: offset, animated }
: { y: offset, animated }
)
}
scrollToItem(params) {
const { data, getItem, getItemCount, horizontal, onScrollToIndexFailed } =
this.props
const { animated, item, viewPosition, viewOffset } = params
const index = this.props.data.indexOf(item)
if (index !== -1) {
this.scrollToIndex({
animated,
index,
viewOffset,
viewPosition,
})
} else {
const itemCount = getItemCount(data)
for (let i = 0; i < itemCount; i++) {
if (getItem(data, i) === item) {
this.scrollToIndex({
animated,
index: i,
viewOffset,
viewPosition,
})
break
}
}
}
}
scrollToOffset(params) {
const { animated, offset } = params
if (this._scrollRef == null) {
return
}
this._scrollRef.scrollTo(
horizontalOrDefault(this.props.horizontal)
? { x: offset, animated }
: { y: offset, animated }
)
}
recordInteraction() {
this._nestedChildLists.forEach((childList) => {
childList.recordInteraction()
})
this._viewabilityTuples.forEach((viewabilityTuple) => {
viewabilityTuple.viewabilityHelper.recordInteraction()
})
}
flashScrollIndicators() {
if (this._scrollRef && this._scrollRef.flashScrollIndicators) {
this._scrollRef.flashScrollIndicators()
}
}
getScrollResponder() {
if (this._scrollRef && this._scrollRef.getScrollResponder) {
return this._scrollRef.getScrollResponder()
}
}
getScrollableNode() {
if (this._scrollRef && this._scrollRef.getScrollableNode) {
return this._scrollRef.getScrollableNode()
}
}
getScrollRef() {
return this._scrollRef
}
setNativeProps(props) {
if (this._scrollRef) {
this._scrollRef.setNativeProps(props)
}
}
render() {
const {
ListEmptyComponent,
ListFooterComponent,
ListHeaderComponent,
data,
debug,
disableVirtualization,
getItem,
getItemCount,
getItemLayout,
horizontal,
keyExtractor,
numColumns,
onEndReached,
onEndReachedThreshold,
onLayout,
onRefresh,
onScroll,
onScrollBeginDrag,
onScrollEndDrag,
onMomentumScrollBegin,
onMomentumScrollEnd,
onStartReached,
onStartReachedThreshold,
onViewableItemsChanged,
refreshing,
removeClippedSubviews,
renderItem,
viewabilityConfig,
viewabilityConfigCallbackPairs,
...restProps
} = this.props
const itemCount = getItemCount(data)
if (itemCount === 0) {
return ListEmptyComponent ? <ListEmptyComponent /> : null
}
return (
<ScrollView
{...restProps}
ref={this._captureRef}
onContentSizeChange={this._onContentSizeChange}
onLayout={this._onLayout}
onScroll={this._onScroll}
refreshControl={
onRefresh && (
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
)
}
scrollEventThrottle={scrollEventThrottleOrDefault(this.props.scrollEventThrottle)}
removeClippedSubviews={removeClippedSubviews}
>
{this._renderChildren()}
</ScrollView>
)
}
_renderChildren() {
const { data, getItem, getItemCount, renderItem } = this.props
const items = []
for (let i = 0; i < getItemCount(data); i++) {
const item = getItem(data, i)
items.push(
<CellRenderer
key={this.props.keyExtractor ? this.props.keyExtractor(item, i) : i}
cellKey={String(i)}
index={i}
item={item}
renderItem={renderItem}
/>
)
}
return items
}
_captureRef = (ref) => {
this._scrollRef = ref
}
_onContentSizeChange = (width, height) => {
// Handle content size changes
}
_onLayout = (event) => {
// Handle layout changes
}
_onScroll = (event) => {
// Handle scroll events
}
__getFrameMetricsApprox(index, props) {
const frame = {
length: this._averageCellLength,
offset: this._averageCellLength * index,
}
return frame
}
_getOffsetApprox(index, props) {
return this.__getFrameMetricsApprox(index, props).offset
}
}
export default VirtualizedList