DEV Community

HarmonyOS
HarmonyOS

Posted on

How to achieve the automatic scrolling effect of the list.

Read the original article:How to achieve the automatic scrolling effect of the list.

How to achieve the automatic scrolling effect of the list.

Requirement Description

Users wants to achieve both horizontal and vertical scrolling.

Background Knowledge

Auto-scroll in ArkUI is achieved by updating the Scroller offset.

Both directions use the same scrollTo logic with different axes.

Implementation Steps

The automatic scrolling effect is achieved by looping this.scroller.scrollTo through the setInterval timer.

startAutoRoll() {
    // Make sure the offset interval is 10 milliseconds
    this.rollOffset = this.rollOffset - this.rollOffset % 2
    this.IntervalNum = setInterval(() => {
      this.rollOffset += 1
      if (this.rollOffset % this.itemHeight === 0) {
        // Data is subtracted before and added after
        this.data.deleteData(0)
        this.data.pushData(this.nextNum.toString())
        if (this.nextNum === 9) {
          this.nextNum = 0
        } else {
          this.nextNum++
        }
        // Corresponding to data changes to prevent exceeding
        this.rollOffset -= this.itemHeight
        this.scroller.scrollTo({ xOffset: 0, yOffset: this.rollOffset, animation: false })
      } else {
        // Slide animation
        this.scroller.scrollTo({
          xOffset: 0,
          yOffset: this.rollOffset,
          animation: { duration: 10, curve: Curve.Linear }
        })
      }
    }, 10)
  }

Enter fullscreen mode Exit fullscreen mode

Calculate the actual required scrolling amount in the onScrollFrameBegin callback of the List and return it as the return value of the event handling function. The List will scroll according to the actual scrolling amount of the return value.

.onScrollFrameBegin((offset: number, state: ScrollState) => {
          let currOffset = this.scroller.currentOffset().yOffset;
          let newOffset = currOffset + offset;
          let totalHeight = this.itemHeight * 10;
          // Slide up
          if (newOffset < totalHeight * 0.5) {
            newOffset += totalHeight;
            // Decline
          } else if (newOffset > totalHeight * 1.5) {
            newOffset -= totalHeight
          }
          this.rollOffset = newOffset
          return { offsetRemain: newOffset - currOffset }
        })

Enter fullscreen mode Exit fullscreen mode

Code Snippet / Configuration

The complete code example is as follows:
1.Index.ets

import { MyDataSource } from './MyDataSource';
@Entry
@Component
struct Parent {
  private dataSource: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  private nextNum: number = 0
  private data: MyDataSource = new MyDataSource();
  private scroller: Scroller = new Scroller();
  private rollOffset: number = 0
  private IntervalNum: number = 0
  pathStack: NavPathStack = new NavPathStack()
  private itemHeight: number = 60
  startAutoRoll() {
    // Make sure the offset interval is 10 milliseconds
    this.rollOffset = this.rollOffset - this.rollOffset % 2
    this.IntervalNum = setInterval(() => {
      this.rollOffset += 1
      if (this.rollOffset % this.itemHeight === 0) {
        // Data is subtracted before and added after
        this.data.deleteData(0)
        this.data.pushData(this.nextNum.toString())
        if (this.nextNum === 9) {
          this.nextNum = 0
        } else {
          this.nextNum++
        }
        // Corresponding to data changes to prevent exceeding
        this.rollOffset -= this.itemHeight
        this.scroller.scrollTo({ xOffset: 0, yOffset: this.rollOffset, animation: false })
      } else {
        // Slide animation
        this.scroller.scrollTo({
          xOffset: 0,
          yOffset: this.rollOffset,
          animation: { duration: 10, curve: Curve.Linear }
        })
      }
    }, 10)
  }
  // Two floors
  aboutToAppear(): void {
    for (let i = 0; i < 10; i++) {
      this.data.pushData(this.dataSource[i].toString())
    }
    for (let i = 0; i < 10; i++) {
      this.data.pushData(this.dataSource[i].toString())
    }
    this.startAutoRoll()
  }

  build() {
    Navigation(this.pathStack) {
        List({ scroller: this.scroller }) {
          LazyForEach(this.data, (item: string) => {
            ListItem() {
              Column() {
                Text('TEST.' + item.toString())
                  .fontSize(16)
                  .textAlign(TextAlign.Center)
                Row() {
                  Text('Get a reply in 20 seconds')
                    .fontSize(13)
                    .textAlign(TextAlign.Center)
                    .fontColor('#ffa933ba')
                  Row() {
                    Text('TEST')
                      .fontSize(13)
                      .textAlign(TextAlign.Center)
                      .fontColor('#ff656266')
                    Image($r('app.media.startIcon'))
                      .width(12)
                      .height(12)
                  }
                }
                .width('100%')
                .justifyContent(FlexAlign.Center)
                .alignItems(VerticalAlign.Center)

              }

              .alignItems(HorizontalAlign.Center)
              .width('100%')

            }
            .onClick(() => {
              console.info('index:', item)
            })
            .borderRadius(10)
            .backgroundColor('#FFFFFF')
            .height(40)
            .width('90%')
            .margin({
              left: '2%',
              right: '2%',
              top: this.itemHeight * 0.1,
              bottom: this.itemHeight * 0.1
            })
          }, (item: string) => item)
        }
        .scrollBar(BarState.Off)
        .width('100%')
        .layoutWeight(1)
        .backgroundColor("#FFDCDCDC")
        .listDirection(Axis.Vertical)
        .scrollSnapAlign(ScrollSnapAlign.NONE)
        .friction(0.5)
        .onScrollStart(() => {
          clearInterval(this.IntervalNum)
        })
        .onScrollStop(() => {
          this.startAutoRoll()
        })
        .onScrollFrameBegin((offset: number, state: ScrollState) => {
          let currOffset = this.scroller.currentOffset().yOffset;
          let newOffset = currOffset + offset;
          let totalHeight = this.itemHeight * 10;
          // Slide up
          if (newOffset < totalHeight * 0.5) {
            newOffset += totalHeight;
            // Decline
          } else if (newOffset > totalHeight * 1.5) {
            newOffset -= totalHeight
          }
          this.rollOffset = newOffset
          return { offsetRemain: newOffset - currOffset }
        })
      }.height('100%')
    .hideTitleBar(true)
  }
}

Enter fullscreen mode Exit fullscreen mode

2.BasicDataSource.ets

export class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: string[] = [];

  public totalCount(): number {
    return 0;
  }

  public getData(index: number): string {
    return this.originDataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener');
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener');
      this.listeners.splice(pos, 1);
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

3.MyDataSource.ets

import { BasicDataSource } from "./BasicDataSource";

export class MyDataSource extends BasicDataSource {
  private dataArray: string[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index % this.dataArray.length];
  }

  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public moveDataWithoutNotify(from: number, to: number): void {
    let tmp = this.dataArray.splice(from, 1);
    this.dataArray.splice(to, 0, tmp[0])
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  public deleteData(index: number): void {
    this.dataArray.splice(index, 1);
    this.notifyDataDelete(index);
  }
}

Enter fullscreen mode Exit fullscreen mode

Test Results

Tested on HarmonyOS 5.1.0(18) emulator - works as expected

Limitations or Considerations

Not applicable to apps targeting API Level < 11.

Related Documents or Link

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-rendering-control-lazyforeach

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-container-scroll#onscrollstart9

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-container-scroll#onscrollstop9

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-container-scroll#onscrollframebegin9

Written by Emrecan Karakas

Top comments (0)