The problem I want to solve is to be able to programmatically scroll to an item using it's index. A sample API would be:
async scrollToIndex(viewPort: CdkVirtualScrollViewport, index: number): Promise<boolean>
Here is the high level overview of the approach I'm using:
offset + (N * itemSize)
. This guess is expected to be inaccurate as there are different itemSizes.const range = viewport.getRenderedRange()
N < range.start
- the guess was too high, correct offset and go to 2N >= range.end
- the guess was too low, correct offset and go to 2range.start <= N < range.end
- the guess is good enough to bring N into the viewport, go to 5This approach is working however there are some issues with it:
guess = this.getGuess(index, offset, itemHeight);
viewPort.scrollToOffset(guess);
await delayFor(400); // ARBITRARY DELAY
const renderedRange = viewPort.getRenderedRange();
I need this to work on different mobile devices, which have varying performance characteristics, so I can't use an arbitrary delay, it could work on my PC/device but for some other device the delay could be not enough. I'm looking for a better solution without a hardcoded delay. For example wait for some event signifying that the range has changed after the scrollToOffset()
call.
Regarding issue 2 I'm not sure why it's happening and therefore don't know how to fix it. I'm more interested in solving the first issue but any help is appreciated. On the example scrolling to index 2 gives me incorrect results.
I created a minimal example here - in this example every 3rd list element is taller than the other elements.
To solve your problems, try this approach without unnecessary delays and handle scrolling more efficiently. Also, for the issue of the element not scrolling to the top, use the scrolledIndexChange event to track when the viewport has finished scrolling and then scroll the element into view.
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
async function scrollToIndex(viewport: CdkVirtualScrollViewport, index: number): Promise<boolean> {
// Step 1: Make a guess for the offset
const itemHeight = getItemHeight(index); // Implement a function to get the item height based on the index
const guess = index * itemHeight;
// Step 2: Scroll to the guessed offset
viewport.scrollToOffset(guess);
// Step 3: Wait for the viewport to finish scrolling
await new Promise(resolve => {
const subscription = viewport.scrolledIndexChange.subscribe(() => {
subscription.unsubscribe();
resolve();
});
});
// Step 4: Get the rendered range
const renderedRange = viewport.getRenderedRange();
// Step 5: Check if the item is in the viewport
if (index < renderedRange.start) {
// The guess was too high, correct offset and scroll again
return scrollToIndex(viewport, index);
} else if (index >= renderedRange.end) {
// The guess was too low, correct offset and scroll again
const correctedOffset = (index + 1) * itemHeight - viewport.getViewportSize().height;
viewport.scrollToOffset(correctedOffset);
return scrollToIndex(viewport, index);
} else {
// The guess is good enough, scroll the element into view
const element = viewport.elementRef.nativeElement.querySelector(`[cdkvirtualforitemindex="${index}"]`);
if (element) {
element.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'nearest' });
return true;
}
return false;
}
}
// Example usage:
const indexToScroll = 2; // Replace with the desired index
await scrollToIndex(yourViewportInstance, indexToScroll);