amis-rpc-design/node_modules/@react-native/virtualized-lists/Lists/VirtualizeUtils.js
2023-10-07 19:42:30 +08:00

259 lines
7.5 KiB
JavaScript

/**
* 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.
*
* @flow
* @format
*/
'use strict';
import type {FrameMetricProps} from './VirtualizedListProps';
/**
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
* items that bound different windows of content, such as the visible area or the buffered overscan
* area.
*/
export function elementsThatOverlapOffsets(
offsets: Array<number>,
props: FrameMetricProps,
getFrameMetrics: (
index: number,
props: FrameMetricProps,
) => {
length: number,
offset: number,
...
},
zoomScale: number = 1,
): Array<number> {
const itemCount = props.getItemCount(props.data);
const result = [];
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
const currentOffset = offsets[offsetIndex];
let left = 0;
let right = itemCount - 1;
while (left <= right) {
// eslint-disable-next-line no-bitwise
const mid = left + ((right - left) >>> 1);
const frame = getFrameMetrics(mid, props);
const scaledOffsetStart = frame.offset * zoomScale;
const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale;
// We want the first frame that contains the offset, with inclusive bounds. Thus, for the
// first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive.
if (
(mid === 0 && currentOffset < scaledOffsetStart) ||
(mid !== 0 && currentOffset <= scaledOffsetStart)
) {
right = mid - 1;
} else if (currentOffset > scaledOffsetEnd) {
left = mid + 1;
} else {
result[offsetIndex] = mid;
break;
}
}
}
return result;
}
/**
* Computes the number of elements in the `next` range that are new compared to the `prev` range.
* Handy for calculating how many new items will be rendered when the render window changes so we
* can restrict the number of new items render at once so that content can appear on the screen
* faster.
*/
export function newRangeCount(
prev: {
first: number,
last: number,
...
},
next: {
first: number,
last: number,
...
},
): number {
return (
next.last -
next.first +
1 -
Math.max(
0,
1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first),
)
);
}
/**
* Custom logic for determining which items should be rendered given the current frame and scroll
* metrics, as well as the previous render state. The algorithm may evolve over time, but generally
* prioritizes the visible area first, then expands that with overscan regions ahead and behind,
* biased in the direction of scroll.
*/
export function computeWindowedRenderLimits(
props: FrameMetricProps,
maxToRenderPerBatch: number,
windowSize: number,
prev: {
first: number,
last: number,
},
getFrameMetricsApprox: (
index: number,
props: FrameMetricProps,
) => {
length: number,
offset: number,
...
},
scrollMetrics: {
dt: number,
offset: number,
velocity: number,
visibleLength: number,
zoomScale: number,
...
},
): {
first: number,
last: number,
} {
const itemCount = props.getItemCount(props.data);
if (itemCount === 0) {
return {first: 0, last: -1};
}
const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics;
// Start with visible area, then compute maximum overscan region by expanding from there, biased
// in the direction of scroll. Total overscan area is capped, which should cap memory consumption
// too.
const visibleBegin = Math.max(0, offset);
const visibleEnd = visibleBegin + visibleLength;
const overscanLength = (windowSize - 1) * visibleLength;
// Considering velocity seems to introduce more churn than it's worth.
const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5));
const fillPreference =
velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none';
const overscanBegin = Math.max(
0,
visibleBegin - (1 - leadFactor) * overscanLength,
);
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);
const lastItemOffset =
getFrameMetricsApprox(itemCount - 1, props).offset * zoomScale;
if (lastItemOffset < overscanBegin) {
// Entire list is before our overscan window
return {
first: Math.max(0, itemCount - 1 - maxToRenderPerBatch),
last: itemCount - 1,
};
}
// Find the indices that correspond to the items at the render boundaries we're targeting.
let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
[overscanBegin, visibleBegin, visibleEnd, overscanEnd],
props,
getFrameMetricsApprox,
zoomScale,
);
overscanFirst = overscanFirst == null ? 0 : overscanFirst;
first = first == null ? Math.max(0, overscanFirst) : first;
overscanLast = overscanLast == null ? itemCount - 1 : overscanLast;
last =
last == null
? Math.min(overscanLast, first + maxToRenderPerBatch - 1)
: last;
const visible = {first, last};
// We want to limit the number of new cells we're rendering per batch so that we can fill the
// content on the screen quickly. If we rendered the entire overscan window at once, the user
// could be staring at white space for a long time waiting for a bunch of offscreen content to
// render.
let newCellCount = newRangeCount(prev, visible);
while (true) {
if (first <= overscanFirst && last >= overscanLast) {
// If we fill the entire overscan range, we're done.
break;
}
const maxNewCells = newCellCount >= maxToRenderPerBatch;
const firstWillAddMore = first <= prev.first || first > prev.last;
const firstShouldIncrement =
first > overscanFirst && (!maxNewCells || !firstWillAddMore);
const lastWillAddMore = last >= prev.last || last < prev.first;
const lastShouldIncrement =
last < overscanLast && (!maxNewCells || !lastWillAddMore);
if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) {
// We only want to stop if we've hit maxNewCells AND we cannot increment first or last
// without rendering new items. This let's us preserve as many already rendered items as
// possible, reducing render churn and keeping the rendered overscan range as large as
// possible.
break;
}
if (
firstShouldIncrement &&
!(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore)
) {
if (firstWillAddMore) {
newCellCount++;
}
first--;
}
if (
lastShouldIncrement &&
!(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore)
) {
if (lastWillAddMore) {
newCellCount++;
}
last++;
}
}
if (
!(
last >= first &&
first >= 0 &&
last < itemCount &&
first >= overscanFirst &&
last <= overscanLast &&
first <= visible.first &&
last >= visible.last
)
) {
throw new Error(
'Bad window calculation ' +
JSON.stringify({
first,
last,
itemCount,
overscanFirst,
overscanLast,
visible,
}),
);
}
return {first, last};
}
export function keyExtractor(item: any, index: number): string {
if (typeof item === 'object' && item?.key != null) {
return item.key;
}
if (typeof item === 'object' && item?.id != null) {
return item.id;
}
return String(index);
}