鸿蒙 Next 实现一个带表头的横向和纵向滑动的列表
- 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) }}
版权声明: 本文为 InfoQ 作者【auhgnixgnahz】的原创文章。
原文链接:【http://xie.infoq.cn/article/b428155cbb376d0174ebb1908】。文章转载请联系作者。
auhgnixgnahz
还未添加个人签名 2018-07-10 加入
还未添加个人简介









评论