基于List和Scroller由简单到复杂列表布局开发实践
场景描述
在多列表页面开发中,数据展示往往有联动关系,
场景一:单列表布局多长列表页面,如门户首页、商城首页
场景二:双列表滚动联动,如城市选择
场景三:多列表滚动横向纵向联动,如汽车参数对比,股票信息列表
方案描述
场景一:
单列表布局多长列表页面,如门户首页、商城首页效果图
方案
运用List组件作为整个首页长列表的容器,通过ListItem对不同模块进行定制。
1. Refresh包裹List实现下拉刷新
2. ListItem-0嵌套Swiper实现轮播图。
3. ListItem-1嵌套Grid实现快捷入口。
4. ListItem-2嵌套Column实现秒杀
5. ListItemGroup实现商品分类列表
6. 最底部ListItem实现触底自动加载
核心代码
build() {
Column() {
// 搜索框 置顶
if (this.searchSticky) {
this.searchBarBuilder()
}
// 下拉刷新组件
Refresh({ refreshing: $$this.isRefreshing }) {
// List组件作为长列表布局
List({ space: 10 }) {
// 搜索框跟随
if (!this.searchSticky) {
ListItem() {
this.searchBarBuilder()
}
}
// ListItem 自定义Swiper轮播图模块
ListItem() {
this.bannerBuilder()
}
// ListItem 自定义Grid快接入口模块
ListItem() {
this.quickBuilder()
}
// ListItem 自定义Column秒杀模块
ListItem() {
this.flashBuilder()
}
// ListItemGroup 商品分类列表
this.productsBuilder()
// 最后ListItem 自定义触底加载更多
ListItem() {
this.footerLoadingBuilder()
}.height(50).width('100%').backgroundColor(0xeeeeee)
}
.sticky(StickyStyle.Header)
.edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
.height('100%')
.layoutWeight(2)
// List组件触底模拟网络请求
.onReachEnd(() => {
if (this.productsArray.length >= 20) {
this.noMoreData = true
return
}
setTimeout(() => {
this.productsArray.push('商品' + (this.productsArray.length + 1))
}, 2000)
})
}
// 下拉刷新模拟网络请求
.onRefreshing(() => {
setTimeout(() => {
this.productsArray = ['商品1', '商品2', '商品3', '商品4', '商品5']
this.noMoreData = false
this.isRefreshing = !this.isRefreshing
}, 2000)
})
.layoutWeight(1)
.width('95%')
}
}
场景二:
双列表滚动同向联动,如城市选择
效果图
方案
整体运用Stack组件(List组件+List组件)布局,左List作为城市列表,右List快捷导航列表,通过ListItem对对应数据进行渲染。
1.左List用ListItemGroup对城市数据进行分组
2.右List用ListItem对首字母进行渲染
3.通过右List首字母导航点击可以切换左List滚动到对应分组
核心代码
@State private selectGroupIndex: number = -1 //导航栏选中index
private cityScroller: ListScroller = new ListScroller() // 城市列表Scoller控制器
private navgationScroller: ListScroller = new ListScroller() // 导航列表Scoller控制器
private isClickScroll:boolean = false // 导航列表点击标记为true,城市列表触摸滚动为false
build() {
Stack({alignContent : Alignment.End}) {
this.cityList()
this.navigationList()
}
.width('100%')
.height('100%').backgroundColor(0xFFFFFF)
}
// 城市列表
@Builder
cityList() {
List({ scroller: this.cityScroller }) {
ListItemGroup({ header: this.itemHead('当前城市') }) {
ListItem() {
Text(this.currentCity)
......
}
}
ListItemGroup({ header: this.itemHead('热门城市') }) {
ForEach(this.hotCities, (hotCity: string) => {
ListItem() {
Text(hotCity)
......
}
})
}
// A~Z城市分组
ForEach(this.groupNameList, (item: string) => {
ListItemGroup({ header: this.itemHead(item) }) {
ForEach(this.getCitiesWithGroupName(item), (cityItem: City) => {
ListItem() {
Text(cityItem.city)
......
}
}, (item: City) => item.city)
}
})
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.sticky(StickyStyle.Header)
.onTouch(()=>{
// 城市列表触摸滚动,isClickScroll=false,防止滚动过程中与导航列表触发滚动冲突
this.isClickScroll = false
})
.onScrollIndex((start: number, end: number, center: number)=>{
// 通过selectGroupIndex状态变量与start联动控制导航列表选中状态
if(!this.isClickScroll)
this.selectGroupIndex = start - 2
})
}
// 导航列表@Builder
navigationList() {
List({scroller:this.cityScroller1}) {
ForEach(this.groupNameList, (item: string, index: number) => {
ListItem() {
Text(item)
......
.onClick(() => {
// 导航列表选中isClickScroll=true,防止与城市列表滚动过程中带动导航列表状态变化
this.isClickScroll = true
this.selectGroupIndex = index
// 通过导航选中selectGroupIndex与Scroller控制城市列表滚动到对应位置
this.cityScroller.scrollToIndex(index + 2, true, ScrollAlign.START)
})
}
}, (item: string) => item)
}
.listDirection(Axis.Vertical)
.backgroundColor(Color.Transparent)
.width('10%')
}
场景三:
多列表滚动横向纵向联动,如汽车参数对比,股票信息列表
效果图
方案
1.Column组件(Row组件1 + Row组件2)整体布局上下两部分,Row1代表上部分,Row2代表下部分
2.上部分Row组件1(Column组件+ List组件0),Column组件用来布局固定信息,List组件0用来渲染底部内容区域表头,与下部分List组件3+进行联动滚动,如股票参数,车型列表。
3.下部分Row组件2(List组件1 + Scroll组件(List组件2)),List组件1渲染每条信息的头部,内部用ListItemGroup进行分组渲染,竖向滚动;Scroll组件用来包裹详细内容数据List组件2,与List组件1进行竖向滚动联动;List组件2用来渲染内容数据,与List组件0进行横向滚动联动。
4.List组件2作为内容数据容器,ListItem中嵌套List组件3+横向滚动,联动List组件0进行横向滚动。
核心代码
export class ShowData {
sticky?:string
sub?: string[];
scrollerArray?: Scroller[] = [];
}
@State remainOffset: number = 0 // 内容行在横向滚动时回调的offset
private bottomRightScroller: Scroller = new Scroller() //下部分左侧标题List(行标题)
private bottomLeftScroller: Scroller = new Scroller() // 下部分右侧内容List(内容)
private topRightScroller: Scroller = new Scroller() // 上部分右侧类型List(列标题)
// 整体布局
build() {
Column() {
// 上部分
this.topFixed()
// 下部分
Row() {
this.leftList()
this.rightList()
Line().height('100%').width(0.5).backgroundColor('#EEEEEE').position({ x: LeftItemWidth })
}
.justifyContent(FlexAlign.Start)
.alignItems(VerticalAlign.Top)
}.height('100%')
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Start)
}
// 上部分整体Row(Column + List)
@Builder
topFixed() {
Row() {
// 上部分左侧固定信息
Column() {
.......
}
.......
.padding(10)
// 分割线
Line().height(100).width(0.5).backgroundColor(0xeeeeee)
// 上部分右侧车型横向滚动列表
List({ scroller: this.topRightScroller/* 绑定Scroller控制器与其他控制器联动*/ }) {
ForEach(this.topRightArr, (item: string, index: number) => {
ListItem() {
.......
}
}, (item: string) => item)
}
.......
.onScrollFrameBegin((offset: number, state: ScrollState) => {
// 关键联动,通过对象保存的Scroller控制器数组遍历保持offset同步
this.dataSource.getAllData().forEach(showData => {
showData.scrollerArray!.forEach(scroller => {
scroller.scrollTo({ xOffset: this.topRightScroller.currentOffset().xOffset + offset, yOffset: 0 })
})
})
return { offsetRemain: offset }
})
}.height(100).width('100%')
}
// 下部分右侧内容显示区域纵向List(ListItem(List))
@Builder
rightList() {
List({ initialIndex: 0, scroller: this.bottomRightScroller }) {
// 通过LazyForEach加载每一行
LazyForEach(this.dataSource, (item: ShowData, index: number) => {
ListItemGroup({ header: this.rightStickyHeader(index) }) {
ForEach(item.sub, (subItem: string, index1: number) => {
// 自定义ListItem中包含横向滚动List
ItemComponent({
scroller: item.scrollerArray![index1],
scrollCallBack: (value) => {
// value为子List横向滚动onScrollFrameBegin回传offset,在手指拖动时保持联动一致
// 顶部车型List跟随联动
this.topRightScroller.scrollTo({ xOffset: value, yOffset: 0 })
// 通过对象保存的Scroller数组跟随保持联动
this.dataSource.getAllData().forEach(showData => {
showData.scrollerArray!.forEach(scroller => {
if (scroller != item.scrollerArray![index1]) {
scroller.scrollTo({ xOffset: value, yOffset: 0 })
}
})
})
},
remainOffsetCallBack: (value) => {
// 滚动过程中回传保持同步的offset值
this.remainOffset = value
}
})
}, (item: string) => item)
}
}, (item: ShowData, index: number) => item.sticky! + index)
}
.......
.onScrollFrameBegin((offset: number, state: ScrollState) => {
// 内容List纵向滚动带动左侧标题List跟随滚动
this.bottomLeftScroller.scrollTo({
xOffset: 0,
yOffset: this.bottomRightScroller.currentOffset().yOffset + offset,
animation: false
})
return { offsetRemain: offset }
})
.onScroll(() => {
// 内容List纵向滚动过程中,每一行中子List的Scroller滚动到remainOffset与已显示的行位置保持一致
this.dataSource.getAllData().forEach(showData => {
showData.scrollerArray!.forEach(scroller => {
scroller.scrollTo({ xOffset: this.remainOffset, yOffset: 0 })
})
})
})
.......
}
@Component
struct ItemComponent {
private arr: string[] = [
'1', '2', '3', '4', '5', '6', '7', '8']
private dataSource = new CommonDataSource<string>()
private scroller?: Scroller = undefined // 内容行List绑定Scroller
private scrollCallBack?: (param: number) => void // 触摸滚动过程中回调实时offset
private remainOffsetCallBack?: (param: number) => void // 滚动时回调同步offset
aboutToAppear(): void {
this.dataSource.setData(this.arr)
}
// 下部分参数列表每行数据List
@Builder
RightSingleLineList() {
List({ scroller: this.scroller }) {
LazyForEach(this.dataSource, (item: string, index: number) => {
ListItem() {
......
}
.width(RightItemWidth)
}, (item: string) => item)
}
......
.onScroll(() => {
// 通过callBack回调行在横向滚动时,Scroller当前的offset
if (this.remainOffsetCallBack)
this.remainOffsetCallBack(this.scroller!.currentOffset().xOffset)
})
.onScrollFrameBegin((offset: number, state: ScrollState) => {
// 触摸滚动实时跟随回调
if (this.scrollCallBack) {
this.scrollCallBack(this.scroller!.currentOffset().xOffset + offset)
}
return { offsetRemain: offset }
})
}
build() {
Column() {
this.RightSingleLineList()
Line().width("100%").height(0.5).backgroundColor(0xeeeeee)
}.height(ItemHeight)
}
}
其他常见问题
1.滑动卡顿
LazyForEach数据懒加载:数据量大的List尽量用LazyForEach加载数据,可明显优化性能,经过测试列数为100以上,LazyForEach也无明显卡顿。
2.错位分析
查看左右List行高是否一致,ListItemGroup高度是否一致;onScrollFrameBegin联动回调中是否跟随保持一致。
3.嵌套滚动
如需要外层附加其他滚动,可运用嵌套属性.nestedScroll进行联动。