鸿蒙Next实现一个带表头的横向和纵向滑动的列表
2025-06-27 22:44:51
107次阅读
0个评论
实现思路: 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)
}
}
00
- 0回答
- 0粉丝
- 0关注
相关话题
- 鸿蒙开发:实现一个超简单的网格拖拽
- 鸿蒙开发:简单实现一个服务卡片
- 鸿蒙开发:如何实现一个hvigor插件
- 鸿蒙开发:实现一个标题栏吸顶
- HarmonyOS实战:快速实现一个上下滚动的广告控件
- 鸿蒙-自定义布局-实现一个可限制行数的 Flex
- 创建一个登录界面
- 鸿蒙开发:自定义一个Toast
- 鸿蒙开发:一个轻盈的上拉下拉刷新组件
- 鸿蒙-做一个简版的富文本解析控件
- 鸿蒙Next使用Canvas绘制一个汽车仪表盘
- 41. [HarmonyOS NEXT Row案例九] 打造流畅可滑动列表项:滑动操作按钮的高级实现
- HarmonyOS NEXT边学边玩:从零实现一个影视App(六、视频播放页的实现)
- 鸿蒙开发:自定义一个简单的标题栏
- 鸿蒙开发:自定义一个任意位置弹出的Dialog