鸿蒙 Next 实现一个带表头的横向和纵向滑动的列表
- 2025-06-23 北京
本文字数:7197 字
阅读完需:约 24 分钟
实现思路:
1.头部表头使用一个横向的 list 展示表头列表信息
2.左边固定列用一个纵向的 list 展示固定信息
3.右边使用垂直 list 展示数据项,横向 list 展示每条数据项的内容
设计一个草图:

###基本布局开始实现:
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)
}
效果如下:

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
@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)
}
效果如下:

###数据展示完了,但是滑动毫无关联。。。接下来将滑动关联起来
###滑动处理:
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出现时,保持所有可见范围的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 });
}
})
最终效果:

全部代码:
@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)
}
}
版权声明: 本文为 InfoQ 作者【auhgnixgnahz】的原创文章。
原文链接:【http://xie.infoq.cn/article/b428155cbb376d0174ebb1908】。文章转载请联系作者。

auhgnixgnahz
还未添加个人签名 2018-07-10 加入
还未添加个人简介
评论