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

2025-06-27 22:44:51
107次阅读
0个评论

实现思路: 1.头部表头使用一个横向的list展示表头列表信息 2.左边固定列用一个纵向的list展示固定信息 3.右边使用垂直list展示数据项,横向list展示每条数据项的内容 设计一个草图: 草图.png

###基本布局开始实现: 1.定义数据结构:

@ObservedV2
class ListItemData {
  @Trace text: string = '';
  @Trace id: string = '';
}
@ObservedV2
class ListData {
  @Trace id: string = '';
  @Trace fundName: string = '';
  @Trace textDataSource: ListItemData[] = []
}

@ObservedV2
class 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)
  }

效果如下: 顶部.gif 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)
  }

效果如下: 左侧.gif

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

@Builder
rightBuilder() {
  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)
}

效果如下: 右侧.gif ###数据展示完了,但是滑动毫无关联。。。接下来将滑动关联起来 ###滑动处理: 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 };
})

实现效果: 上下关联初步实现.gif 可以发现,左右滑动只是当前展示的同步了,如果是新加载出来的数据没有实现同步滑动,因此还需要处理一下新添加的数据,保持和上面滑动后的状态一致。 3.当右侧垂直list有新的item滑入时,保持滑动距离和之前滑动的一样,这样就可以保证所有数据左右滑动的位置时一样的。

  // 记录右侧横向列表滚动的距离
  @Local remainOffset: number = 0;
//右侧横向list增加,滚动组件滑动时触发,记录滑动偏移量
onDidScroll(() => {
  this.remainOffset = this.listScrollerArr[index]!.currentOffset().xOffset;
})
//右侧垂直list增加滚动回调,当有新的item出现时,保持所有可见范围的item
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 });
  }
})

最终效果: 最终效果.gif 全部代码:

@ObservedV2
class ListItemData {
  @Trace text: string = '';
  @Trace id: string = '';
}
@ObservedV2
class ListData {
  @Trace id: string = '';
  @Trace fundName: string = '';
  @Trace textDataSource: ListItemData[] = []
}
@ObservedV2
class 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
@ComponentV2
struct 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)
  }
}
收藏00

登录 后评论。没有帐号? 注册 一个。