Read the original article:Implementing Smooth Infinite Scroll Using the List Component
Requirement Description
When using the List component to implement an infinite scroll calendar scenario, the UI flickers when loading more new data. How can we achieve a smooth scrolling effect?
Background Knowledge
- The List component contains a series of list items with the same width, suitable for displaying similar data continuously in multiple lines, such as images and text.
- The
onReachStartcallback is triggered when the list reaches its starting position. It is triggered once during initialization if the initial index is 0, once when the list scrolls to the starting position, once when the list is swiped past the starting position, and once again when the list bounces back to the starting position. - The
onScrollIndexcallback is triggered when a child component is swiped into or out of the list's display area. When calculating the index value, the ListItemGroup is considered as a whole and occupies one index value; the index values of individual list items within the ListItemGroup are not calculated separately.
Implementation Steps
Scroll the list to the top, add elements to the beginning of the array in the onReachStart method, and scroll to the specified index. The specific implementation is as follows:
Implemented in the State Management V2 scenario: Use the onReachStart callback of the List component. When the list scrolls to the top, the onReachStart callback is triggered. When this event occurs, insert the first element of the array at the beginning of the array and call the scrollToIndex method to scroll the list to the specified index position. Additionally, listen for changes in the scroll index in the onScrollIndex method and update the start variable to achieve an infinite scrolling effect for the List.
// Define a class, marked as observable
// Define an array within the class, marked as trackable
@ObservedV2
class ArrayHolderVTwo {
@Trace arr: Array<number> = [];
// constructor
constructor(count: number) {
for (let i = 0; i < count; i++) {
this.arr.push(i);
}
}
}
@Entry
@ComponentV2
struct Index {
@Local arrayHolder: ArrayHolderVTwo = new ArrayHolderVTwo(10);
@Local totalCount: number = this.arrayHolder.arr.length;
scroller: Scroller = new Scroller();
private iCount: number = 1;
private start: number = 1;
build() {
Column({ space: 5 }) {
List({ space: 20, initialIndex: 0, scroller: this.scroller }) {
Repeat(this.arrayHolder.arr)
.virtualScroll({ totalCount: this.totalCount })
.templateId(() => {
return 'number';
})
.template('number', (r) => {
ListItem() {
Column() {
Row() {
Text(r.item.toString());
Text(r.item.toString());
Text(r.item.toString());
};
Row() {
Text(r.item.toString());
Text(r.item.toString());
Text(r.item.toString());
};
Row() {
Text(r.item.toString());
Text(r.item.toString());
Text(r.item.toString());
};
};
}
.margin({ bottom: 5 });
})
.each((r) => {
ListItem() {
Text(r.index! + ':' + r.item + 'eachMessage');
};
});
}.height('100%')
.onScrollIndex((start) => {
this.start = start;
})
.onReachStart(() => {
// The element is the one immediately before the currently displayed element on the screen.
this.arrayHolder.arr.unshift(this.iCount);
this.scroller.scrollToIndex(this.start + 1); // Scroll to the specified index
this.iCount++;
});
}
.width('100%')
.margin({ top: 5 })
.position({
left: 20
});
}
}
The specific demonstration is as follows:
Scenario 2: This is implemented in the state management V1 scenario. The implementation method is similar to that in scenario 1.
The sample code is as follows:
@Observed
class ArrayHolder {
@Track arr: Array<number> = [];
// constructor
constructor(count: number) {
for (let i = 0; i < count; i++) {
this.arr.push(i);
}
}
}
// BasicDataSource implements the IDataSource interface, which is used to manage listener listening and notify LazyForEach of data updates.
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: string[] = [];
public totalCount(): number {
return this.originDataArray.length;
}
public getData(index: number): string {
return this.originDataArray[index];
}
// This method is called by the framework side to add a listener to the data source of the LazyForEach component.
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}
// This method is called by the framework side to remove the listener from the corresponding LazyForEach component at the data source.
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 1);
}
}
// Notifies LazyForEach to reload all child components.
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
});
}
// Instruct the LazyForEach component to add a child component at the index specified by index.
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
// Notifies the LazyForEach component that the data at the index index has changed and the child component needs to be rebuilt.
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
});
}
// Instruct the LazyForEach component to delete the child component at the index specified by index.
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
});
}
// Instructs the LazyForEach component to exchange the child components at the from and to indexes.
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
});
}
notifyDatasetChange(operations: DataOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operations);
});
}
}
class MyDataSource extends BasicDataSource {
private dataArray: string[] = [];
public totalCount(): number {
return this.dataArray.length;
}
unshiftData(data: string, index: number): void {
this.dataArray.unshift(data);
this.notifyDataAdd(index);
}
public getData(index: number): string {
return this.dataArray[index];
}
public pushData(data: string): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
public deleteData(index: number): void {
if (index >= 0 && index < this.dataArray.length) {
this.dataArray.splice(index, 1);
this.notifyDataDelete(index);
}
}
}
@Entry
@Component
struct RepeatScrollPage {
arrayHolder: ArrayHolder = new ArrayHolder(10);
@State totalCount: number = this.arrayHolder.arr.length;
@State dataSource: MyDataSource = new MyDataSource();
scroller: Scroller = new Scroller();
private iCount: number = 0;
// fix
private start: number = 1;
@State end: number = 1;
// end
aboutToAppear(): void {
// Add numbers 1-10 as the data source.
for (let i = 1; i <= 10; i++) {
this.dataSource.pushData(i.toString());
}
}
build() {
Column({ space: 5 }) {
List({ space: 20, initialIndex: 0, scroller: this.scroller }) {
LazyForEach(this.dataSource, (r: string) => {
ListItem() {
Column() {
Row() {
Text(r);
Text(r);
Text(r);
Text(r);
};
Row() {
Text(r);
Text(r);
Text(r);
Text(r);
};
Row() {
Text(r);
Text(r);
Text(r);
Text(r);
};
};
}
.margin({ bottom: 5 });
}, (item: string, index: number) => item + index);
}.height('100%')
.onScrollIndex((start, end) => {
this.start = start;
this.end = end;
})
// end
.onReachStart(() => {
// The element is the one immediately before the currently displayed element on the screen.
this.dataSource.unshiftData(this.iCount.toString(), this.start);
// fix
let rect = this.scroller.getItemRect(this.start + 1);// Obtains the size and position of the child component.
this.scroller.scrollToIndex(this.start + 1); // Scroll to the specified index
this.scroller.scrollBy(0, -rect.y); // Slide for a specified distance
// end
this.totalCount = this.dataSource.totalCount();
this.iCount++;
});
}
.width('100%')
.margin({ top: 5 })
.position({
left: 20
});
}
}
The specific demonstration is as follows:
Code Snippet / Configuration
When the ListItems are of different types, the scroll offset is compared with the height of the ListItems when onScrollStart is triggered, so that more data can be loaded in advance.
@Entry
@Component
struct IndexThree {
@State arr: Array<string> = [];
@State page: number = 1;
pageSize = 10;
scroller: Scroller = new Scroller();
aboutToAppear(): void {
let listData = new ListData();
let list = listData.getData(this.page, this.pageSize);
list.forEach(item => {
this.arr.push(item);
});
}
build() {
RelativeContainer() {
List({ scroller: this.scroller }) {
ListItem() {
Text('Type1');
}
.height(200)
.width('100%')
.margin({ bottom: 5 })
.backgroundColor('#ace');
ListItem() {
Text('Type2');
}
.height(100)
.width('100%')
.margin({ bottom: 5 })
.backgroundColor('#acF');
ListItem() {
WaterFlow() {
ForEach(this.arr, (item: number, index: number) => {
FlowItem() {
Text('Type3: ' + item.toString());
}
.height(index % 2 === 0 ? 150 : 200)
.width('100%')
.margin({ bottom: 5 })
.backgroundColor('#ace');
});
}
.columnsGap(7)
.columnsTemplate('1fr 1fr');
};
}
.onScrollStart(() => {
if (this.scroller.currentOffset().yOffset > this.scroller.getItemRect(2).height - 1000) {
this.page++;
let listData = new ListData();
let list = listData.getData(this.page, this.pageSize);
list.forEach(item => {
this.arr.push(item);
});
}
});
}
.height('100%')
.width('100%');
}
}
export class ListData {
data: Array<string> = [];
totalCount: number = 100;
constructor() {
for (let index = 0; index < this.totalCount; index++) {
this.data.push(index.toString());
}
}
getData(page: number, pageSize: number) {
let startIndex = (page - 1) * pageSize;
return this.data.slice(startIndex, startIndex + pageSize);
}
}
The specific demonstration is as follows:



Top comments (0)