HarmonyOS鸿蒙Next中List组件滚动到顶部,追加数据,保持滚动条不动的问题

HarmonyOS鸿蒙Next中List组件滚动到顶部,追加数据,保持滚动条不动的问题 list组件滚动到顶部,在顶部追加数据,且保持滚动条不动。一开始在list中添加了自定义组件。尝试了很多方法都无法实现。使用简单代码测试发现。使用同一句代码实现跳转到当前index。

this.scroller.scrollToIndex(currentIndex);

当直接在listitem 中使用text组件显示内容时,可以在加载内容后,滚动条保持不动,可见内容也没有变化。

当将text封装到@component中以后(没有其他多余代码),scroller跳转就会不准确,导致可见内容下移等问题。


更多关于HarmonyOS鸿蒙Next中List组件滚动到顶部,追加数据,保持滚动条不动的问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

4 回复

开发者您好,我这边根据您的问题描述采用做了一下验证,将List滚动到顶部,在顶部追加数据,使用 this.scroller.scrollToIndex(currentIndex); 是可以保持滚动条不变维持当前显示数据的。

Text组件或者封装的Component都是正常的。您这边如果出现了scroller跳转不准确,导致可见内容下移的为每桶,是否方便提供一个完整Demo便于我们这边分析定位问题。

以下是我这边的验证代码,您可以看下是否是您需要的场景:

// ListDataSource.ets
export class ListDataSource implements IDataSource {
  private list: number[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(list: number[]) {
    this.list = list;
  }

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

  getData(index: number): number {
    return this.list[index];
  }

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

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

  // 通知LazyForEach组件需要重载所有子组件
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }

  // 通知控制器数据删除
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    });
  }

  // 通知控制器添加数据
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    });
  }

  // 在指定索引位置删除一个元素
  public deleteItem(index: number): void {
    this.list.splice(index, 1);
    this.notifyDataDelete(index);
  }

  // 在指定索引位置插入一个元素
  public insertItem(index: number, data: number): void {
    this.list.splice(index, 0, data);
    console.log(JSON.stringify(this.list));
    this.notifyDataAdd(index);
  }

  public reloadData(): void {
    this.notifyDataReload();
  }
}

@Component
struct ItemList {
  @Prop item:number;
  build() {
    Text('ItemList+' + this.item)
      .width('100%')
      .height(100)
      .fontSize(16)
      .textAlign(TextAlign.Center)
      .borderRadius(10)
      .backgroundColor(0xFFFFFF)
  }
}

@Entry
@Component
struct ListExample {
  private arr: ListDataSource =
    new ListDataSource([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
  @State currentIndex: number = 0;
  private scroller: ListScroller = new ListScroller();
  build() {
    Column() {
      List({ space: 20, initialIndex: 0, scroller: this.scroller }) {
        LazyForEach(this.arr, (item: number) => {
          ListItem() {
            ItemList({item: item})
          }
        }, (item: number) => item.toString())
      }
      .listDirection(Axis.Vertical) // 排列方向
      .scrollBar(BarState.Off)
      .friction(0.6)
      .divider({
        strokeWidth: 2,
        color: 0xFFFFFF,
        startMargin: 20,
        endMargin: 20
      }) // 每行之间的分界线
      .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring
      .onScrollIndex((firstIndex: number, lastIndex: number, centerIndex: number) => {
        console.info('first' + firstIndex);
        console.info('last' + lastIndex);
        console.info('center' + centerIndex);
        this.currentIndex = firstIndex;
      })
      .onScrollVisibleContentChange((start: VisibleListContentInfo, end: VisibleListContentInfo) => {
        console.info(' start index: ' + start.index +
          ' start item group area: ' + start.itemGroupArea +
          ' start index in group: ' + start.itemIndexInGroup);
        console.info(' end index: ' + end.index +
          ' end item group area: ' + end.itemGroupArea +
          ' end index in group: ' + end.itemIndexInGroup);
      })
      .onDidScroll((scrollOffset: number, scrollState: ScrollState) => {
        console.info(`onScroll scrollState = ScrollState` + scrollState + `, scrollOffset = ` + scrollOffset);
      })
      .width('90%')
      .height('90%')

      Button('顶部新增3条').onClick(() => {
        const newArr = [-1, -2, -3];
        newArr.forEach((item: number) => {
          this.arr.insertItem(0, item);
        })
        this.currentIndex = this.currentIndex + newArr.length;
        // this.scroller.scrollToIndex(this.currentIndex)
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xDCDCDC)
    .padding({ top: 5 })
  }
}

更多关于HarmonyOS鸿蒙Next中List组件滚动到顶部,追加数据,保持滚动条不动的问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


这个例子可用,我的例子里面,使用的是foreach,追加数据使用的是this.list.unshift(…newlist)。后来改写成lazyforeach,我的类也采用的 extends Array<T> implements IDataSource 方式,即继承了数组,又实现IDataSource。在lazyforeach中调用的追加数据,仍然使用的是this.list.unshift(…newlist)。相同的代码发生了更严重的BUG。采用这个例子中的newlist.forEach((item)=>{ this.list.insertItem(0,item) })这种写法后追加数据后,滚动顺滑,不会有错位的情况了,非常感谢。

在HarmonyOS Next中,List组件追加数据时保持滚动条不动,可通过ScrollerscrollToIndex方法实现。先记录当前滚动位置,数据追加后,使用scrollToIndex将视图滚动到之前记录的索引位置。需注意数据更新与滚动操作的时序控制,确保UI正确渲染。

在HarmonyOS Next中,List组件在顶部追加数据后保持滚动条位置不变,是一个常见的需求。你遇到的问题核心在于List组件的渲染机制与自定义组件(@Component)的生命周期/测量时机存在差异

问题分析:

  1. 直接使用Text组件:List在数据更新后,能快速完成布局计算。scrollToIndex(currentIndex)执行时,新的列表项(特别是新追加的顶部项)的尺寸已经确定,滚动到指定索引的位置是精确的。
  2. 使用自定义组件封装Text:自定义组件的构建和首次测量可能需要更多时间或发生在不同的UI周期。当数据更新后,立即执行scrollToIndex(currentIndex)时,新的自定义组件可能尚未完成完整的布局测量。此时List计算出的“当前索引位置”是基于旧(或不完整)的组件尺寸,导致滚动偏移。

解决方案: 关键在于确保在List完成基于新数据集的布局后,再执行滚动调整

方法一:使用延迟或异步任务 在追加数据后,将滚动操作放到下一个UI任务队列中执行,为自定义组件的布局测量留出时间。

// 假设在List的控制器listController中操作
this.listData.unshift(...newData); // 在顶部追加数据

// 使用setTimeout或Promise微任务延迟执行
setTimeout(() => {
  // 这里的索引值需要根据你追加的数据量来计算
  // 例如,追加了n条数据,希望保持原第一条数据的位置,则滚动到索引 n
  let targetIndex = newData.length; // 示例:滚动到新追加数据的末尾(即原第一条数据的位置)
  this.listController.scrollToIndex(targetIndex);
}, 0);

// 或使用Promise
Promise.resolve().then(() => {
  this.listController.scrollToIndex(targetIndex);
});

方法二:利用List的onReachStart或布局完成事件 如果业务逻辑允许,可以在触发顶部追加数据的位置(如onReachStart)进行数据更新,并利用事件回调的时机执行滚动。

// 在List组件上设置
List() {
  // ...
}
.onReachStart(() => {
  // 1. 追加数据
  // 2. 在onReachStart回调中,可能也需要短暂延迟来确保布局
  setTimeout(() => {
    this.listController.scrollToIndex(targetIndex);
  }, 10);
})

方法三:精确计算并补偿偏移量(进阶) 如果上述方法仍有微小抖动,可能需要手动计算并补偿。通过listController.getCurrentOffset()获取滚动前偏移量,在数据更新后滚动到原偏移量 + 新追加内容的总预估高度。但这种方法需要你能够准确获取或估算出自定义组件的高度,实现较为复杂。

推荐实践: 优先采用方法一的延迟方案(setTimeoutPromise),延迟时间即使设为0,也能有效将滚动操作与当前数据更新、布局计算分离到不同的UI周期,绝大多数情况下能解决问题。如果对体验要求极高,可结合方法二,在onReachStart回调中进行数据追加和延迟滚动。

总结: 此问题的根源是数据更新与UI渲染的同步问题。自定义组件引入了更复杂的生命周期,导致List在数据变更后立即计算滚动位置时获取的布局信息不准确。通过将滚动操作异步化,等待布局完成,即可实现“追加数据,滚动条不动”的效果。

回到顶部