写点什么

鸿蒙 Next 实现一个带表头的横向和纵向滑动的列表

作者:auhgnixgnahz
  • 2025-06-23
    北京
  • 本文字数:7197 字

    阅读完需:约 24 分钟

实现思路:


1.头部表头使用一个横向的 list 展示表头列表信息


2.左边固定列用一个纵向的 list 展示固定信息


3.右边使用垂直 list 展示数据项,横向 list 展示每条数据项的内容


设计一个草图:



###基本布局开始实现:


1.定义数据结构:


@ObservedV2class ListItemData {  @Trace text: string = '';  @Trace id: string = '';}@ObservedV2class ListData {  @Trace id: string = '';  @Trace fundName: string = '';  @Trace textDataSource: ListItemData[] = []}
@ObservedV2class ListViewModel { @Trace datas: ListData[] = []
loadData() { for (let index = 0; index < 20; index++) { let listData = new ListData(); listData.fundName = '名称' + index for (let index = 0; index < 10; index++) { let item = new ListItemData(); item.text = '内容' + index item.id == index + '' listData.textDataSource.push(item) } } }}
复制代码


2.使用 Text+List 绘制顶部视图


@Builder  titleBuilder() {    Row() {      Column() {        Text('名称')      }      .width(100)      .height(48)      .backgroundColor(Color.White)      .justifyContent(FlexAlign.Center)      .alignItems(HorizontalAlign.Start)      .padding({ left: 16 })      // 头部标题列表      List() {        ForEach(this.titleList, (item: string) => {          ListItem() {            Text(item)              .height(48)              .width(100)              .textAlign(TextAlign.Start)              .padding({ left: 16 })              .backgroundColor(0xFFFFFF)          }        })      }      .listDirection(Axis.Horizontal)      .edgeEffect(EdgeEffect.None)      .scrollBar(BarState.Off)      .layoutWeight(1)
} .height(48) .width('100%') .justifyContent(FlexAlign.Start) }
复制代码


效果如下:



3.添加左侧布局,使用垂直 list 实现


@Builder  leftBuilder() {    List() {      ForEach(this.listViewModel.datas, (item: ListData) => {        ListItem() {          Column() {            Text(item.fundName)              .height('100%')              .backgroundColor(0xFFFFFF)              .layoutWeight(1)              .margin({ left: 16 })            Divider()              .strokeWidth('100%')              .color(0xeeeeee)          }          .justifyContent(FlexAlign.Center)          .alignItems(HorizontalAlign.Start)        }        .height(60)      })    }    .listDirection(Axis.Vertical)    .scrollBar(BarState.Off)    .edgeEffect(EdgeEffect.None)    .width(100)  }
复制代码


效果如下:



4.添加右侧布局,使用垂直 list 包裹多个横向 list


@BuilderrightBuilder() {  List() {    ForEach(this.listViewModel.datas, (item: ListData, index: number) => {      ListItem() {        Column() {          List() {            ForEach(item.textDataSource, (item: ListItemData) => {              ListItem() {                Text(item.text)                  .height('100%')                  .width('100%')                  .textAlign(TextAlign.Start)                  .padding({ left: 16 })                  .backgroundColor(0xFFFFFF)                  .fontColor('#ffe72929')                  .maxLines(1)                  .textOverflow({ overflow: TextOverflow.Ellipsis })              }              .width(100)            })          }          .cachedCount(4)          .height('100%')          .width('100%')          .layoutWeight(1)          .listDirection(Axis.Horizontal)          .scrollBar(BarState.Off)          .nestedScroll({            scrollForward: NestedScrollMode.PARENT_FIRST,            scrollBackward: NestedScrollMode.PARENT_FIRST          })          .edgeEffect(EdgeEffect.None)                  Divider()            .strokeWidth('100%')            .color(0xeeeeee)        }        .height(60)      }    })  }  .height('100%')  .cachedCount(2)  .flingSpeedLimit(1600)  .listDirection(Axis.Vertical)  .scrollBar(BarState.Off)  .edgeEffect(EdgeEffect.None)  .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.PARENT_FIRST })  })  .layoutWeight(1)}
复制代码


效果如下:



###数据展示完了,但是滑动毫无关联。。。接下来将滑动关联起来


###滑动处理:


1.实现左侧名称列表和右侧数据列表垂直滚动关联,分别给两个 list 定义两个控制器,通过事件 onScrollFrameBegin(event: (offset: number, state: ScrollState) => { offsetRemain: number })获取即将发生的滑动量实际滑动量


  // 右侧垂直滚动列表  verticalScroller: Scroller = new Scroller();  // 左侧名称列滚动  leftScroller: Scroller = new Scroller();//左侧list添加滑动回调给右侧垂直list赋值,右侧同理给左侧赋值  onScrollFrameBegin((offset: number, state: ScrollState) =>{      //通过offset保证两个垂直list滑动同步,即两个list的滑动回调分别给另一个赋值      this.verticalScroller.scrollTo({        xOffset:0,        yOffset:this.leftScroller.currentOffset().yOffset+offset,        animation:false      })      return {offsetRemain:offset}    })
复制代码


2.实现头部标题和右侧横向 list 关联滑动,方法和垂直方向同步一样,只不过,右侧的横向 list 有多条数据,因此每一个横向的 list 都需要绑定一个控制器


  // 维护一个list控制器数组,用于保存所有横向列表的 ListScroller  @Local listScrollerArr: ListScroller[] = [];  // 右侧垂直list显示到屏幕起始坐标  @Local startIndex: number = 0;  //右侧垂直list显示到屏幕结束坐标  @Local endIndex: number = 0;//顶部list滑动监听onScrollFrameBegin((offset: number) => {  //刷新当前显示的横向滑动的list  for (let i = this.startIndex; i <= this.endIndex; i++) {    this.listScrollerArr[i].scrollTo({      xOffset: this.topScroller.currentOffset().xOffset + offset,      yOffset: 0,      animation: false    });  }  return { offsetRemain: offset };})//水平list滑动监听onScrollFrameBegin((offset: number) => {  this.topScroller.scrollTo({    xOffset:this.listScrollerArr[index].currentOffset().xOffset + offset,    yOffset: 0,    animation: false  });//需要将其他横向list也同步关联滑动  for (let i = this.startIndex; i <= this.endIndex; i++) {    if (i !== index) {      this.listScrollerArr[i].scrollTo({        xOffset: this.listScrollerArr[index]!.currentOffset().xOffset + offset,        yOffset: 0,        animation: false      });    }  }  return { offsetRemain: offset };})
复制代码


实现效果:



可以发现,左右滑动只是当前展示的同步了,如果是新加载出来的数据没有实现同步滑动,因此还需要处理一下新添加的数据,保持和上面滑动后的状态一致。


3.当右侧垂直 list 有新的 item 滑入时,保持滑动距离和之前滑动的一样,这样就可以保证所有数据左右滑动的位置时一样的。


  // 记录右侧横向列表滚动的距离  @Local remainOffset: number = 0;//右侧横向list增加,滚动组件滑动时触发,记录滑动偏移量onDidScroll(() => {  this.remainOffset = this.listScrollerArr[index]!.currentOffset().xOffset;})//右侧垂直list增加滚动回调,当有新的item出现时,保持所有可见范围的itemonScrollIndex((start: number, end: number) => {  this.startIndex = start;  this.endIndex = end;  // 只滚动当前显示范围内的item  for (let i = start; i <= end; i++) {    this.listScrollerArr[i].scrollTo({ xOffset: this.remainOffset, yOffset: 0, animation: false });  }})
复制代码


最终效果:



全部代码:


@ObservedV2class ListItemData {  @Trace text: string = '';  @Trace id: string = '';}@ObservedV2class ListData {  @Trace id: string = '';  @Trace fundName: string = '';  @Trace textDataSource: ListItemData[] = []}@ObservedV2class ListViewModel {  @Trace datas: ListData[] = [] async  loadData() {    for (let index = 0; index < 20; index++) {      let listData = new ListData();      listData.fundName = '名称' + index      for (let index = 0; index < 10; index++) {        let item = new ListItemData();        item.text = '内容' + index        item.id == index + ''        listData.textDataSource.push(item)      }      this.datas.push(listData)    }  }}@Entry@ComponentV2struct ScrollList {  @Local listViewModel: ListViewModel = new ListViewModel()  // 头部标题列表,每一列的标题  @Local titleList: string[] = [];  // 左侧名称列滚动  leftScroller: Scroller = new Scroller();  // 列表数据垂直滚动  verticalScroller: Scroller = new Scroller();  // 列表数据横向滚动  horizontalScroller: Scroller = new Scroller();  // 头部标题列滚动  topScroller: Scroller = new Scroller();  // 维护一个list控制器数组,用于保存所有横向列表的 ListScroller  @Local listScrollerArr: ListScroller[] = [];  // 右侧垂直list显示到屏幕起始坐标  @Local startIndex: number = 0;  //右侧垂直list显示到屏幕结束坐标  @Local endIndex: number = 0;  // 记录右侧横向列表滚动的距离  @Local remainOffset: number = 0;  async aboutToAppear() {    await this.listViewModel.loadData()    for (let index = 0; index < 10; index++) {      //增加list的控制器      this.titleList.push('标题' + index)    }    for (let index = 0; index < this.listViewModel.datas.length; index++) {      this.listScrollerArr.push(new ListScroller());    }  }  @Builder  titleBuilder() {    Row() {      Column() {        Text('名称')      }      .width(100)      .height(48)      .backgroundColor(Color.White)      .justifyContent(FlexAlign.Center)      .alignItems(HorizontalAlign.Start)      .padding({ left: 16 })      // 头部标题列表      List({ scroller: this.topScroller }) {        ForEach(this.titleList, (item: string) => {          ListItem() {            Text(item)              .height(48)              .width(100)              .textAlign(TextAlign.Start)              .padding({ left: 16 })              .backgroundColor(0xFFFFFF)          }        })      }      .listDirection(Axis.Horizontal)      .edgeEffect(EdgeEffect.None)      .scrollBar(BarState.Off)      .layoutWeight(1)      .onScrollFrameBegin((offset: number) => {        //刷新当前显示的横向滑动的list        for (let i = this.startIndex; i <= this.endIndex; i++) {          this.listScrollerArr[i].scrollTo({            xOffset: this.topScroller.currentOffset().xOffset + offset,            yOffset: 0,            animation: false          });        }        return { offsetRemain: offset };      })    }    .height(48)    .width('100%')    .justifyContent(FlexAlign.Start)  }  @Builder  leftBuilder() {    List({ scroller: this.leftScroller }) {      ForEach(this.listViewModel.datas, (item: ListData) => {        ListItem() {          Column() {            Text(item.fundName)              .height('100%')              .backgroundColor(0xFFFFFF)              .layoutWeight(1)              .margin({ left: 16 })            Divider()              .strokeWidth('100%')              .color(0xeeeeee)          }          .justifyContent(FlexAlign.Center)          .alignItems(HorizontalAlign.Start)        }        .height(60)      })    }    .listDirection(Axis.Vertical)    .scrollBar(BarState.Off)    .edgeEffect(EdgeEffect.None)    .width(100)    .onScrollFrameBegin((offset: number, state: ScrollState) =>{      //通过offset保证两个垂直list滑动同步,即两个list的滑动回调分别给另一个赋值      this.verticalScroller.scrollTo({        xOffset:0,        yOffset:this.leftScroller.currentOffset().yOffset+offset,        animation:false      })      return {offsetRemain:offset}    })  }  @Builder  rightBuilder() {    List({ scroller: this.verticalScroller }) {      ForEach(this.listViewModel.datas, (item: ListData, index: number) => {        ListItem() {          Column() {            List({scroller:this.listScrollerArr[index]}) {              ForEach(item.textDataSource, (item: ListItemData) => {                ListItem() {                  Text(item.text)                    .height('100%')                    .width('100%')                    .textAlign(TextAlign.Start)                    .padding({ left: 16 })                    .backgroundColor(0xFFFFFF)                    .fontColor('#ffe72929')                    .maxLines(1)                    .textOverflow({ overflow: TextOverflow.Ellipsis })                }                .width(100)              })            }            .cachedCount(4)            .height('100%')            .width('100%')            .layoutWeight(1)            .listDirection(Axis.Horizontal)            .scrollBar(BarState.Off)            .nestedScroll({              scrollForward: NestedScrollMode.PARENT_FIRST,              scrollBackward: NestedScrollMode.PARENT_FIRST            })            .edgeEffect(EdgeEffect.None)            .onScrollFrameBegin((offset: number) => {              this.topScroller.scrollTo({                xOffset:this.listScrollerArr[index].currentOffset().xOffset + offset,                yOffset: 0,                animation: false              });              for (let i = this.startIndex; i <= this.endIndex; i++) {                if (i !== index) {                  this.listScrollerArr[i].scrollTo({                    xOffset: this.listScrollerArr[index]!.currentOffset().xOffset + offset,                    yOffset: 0,                    animation: false                  });                }              }              return { offsetRemain: offset };            })            //滚动组件滑动时触发            .onDidScroll(() => {              this.remainOffset = this.listScrollerArr[index]!.currentOffset().xOffset;            })            Divider()              .strokeWidth('100%')              .color(0xeeeeee)          }          .height(60)        }      })    }    .height('100%')    .cachedCount(2)    .flingSpeedLimit(1600)    .listDirection(Axis.Vertical)    .scrollBar(BarState.Off)    .edgeEffect(EdgeEffect.None)    .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.PARENT_FIRST })    .onScrollFrameBegin((offset: number) => {      this.leftScroller.scrollTo({        xOffset: 0,        yOffset: this.verticalScroller.currentOffset().yOffset + offset,        animation: false      });      return { offsetRemain: offset };    })    .onScrollIndex((start: number, end: number) => {      this.startIndex = start;      this.endIndex = end;      // 只滚动当前显示范围内的item      for (let i = start; i <= end; i++) {        this.listScrollerArr[i].scrollTo({ xOffset: this.remainOffset, yOffset: 0, animation: false });      }    })    .layoutWeight(1)  }  build() {    Column() {      // 头部标题      this.titleBuilder()      // 分割线      Divider()        .strokeWidth('100%')        .color(0xeeeeee)      Row() {        // 左侧列        this.leftBuilder()        // 右侧列        this.rightBuilder()      }    }    .height('100%')    .alignItems(HorizontalAlign.Start)  }}
复制代码


发布于: 刚刚阅读数: 3
用户头像

auhgnixgnahz

关注

还未添加个人签名 2018-07-10 加入

还未添加个人简介

评论

发布
暂无评论
鸿蒙Next实现一个带表头的横向和纵向滑动的列表_鸿蒙Next_auhgnixgnahz_InfoQ写作社区