介绍
本示例主要介绍了List组件实现二级联动(Cascading List)的场景。 该场景多用于商品种类的选择、照片不同类型选择等场景。
效果图
使用说明:
- 滑动二级列表侧控件(点击没用),一级列表随之滚动。(当最后一次触屏在一级列表,则滑动二级列表,一级列表固定不动)
- 点击一级列表(滑动没用),二级列表随之滚动。
- 点击一级列表可视区域边界时,选中类别向中间移动
实现思路
- 使用两个List。
- 一二级列表分别绑定不同的Scroller对象,一级列表(tagLists)绑定classifyScroller对象,二级列表绑定scroller对象。
- 维护records数组,一个item数量的前缀和,records[i]表示第i+1种类别的第一个item之前有多少个item,这个数值等于records[i]表示第i+1种类别的第一个item在itemList的下标
- 使用List的onTouch,.onScrollIndex组件方法,判断最后一次触屏是否在一级列表,和一级列表的可视区域
- 实现itemFindClassIndex(itemIndex:number),itemFindClassIndex(itemIndex:number);
- 点击一级列表后,通过一级列表的索引获取二级列表的索引,调用scrollToIndex方法将一二级列表滚动到指定索引值
- 滑动二级列表触发组件滚动事件后,获取到列表可视区域第一个item对应的索引值,通过二级列表索引获取一级列表索引,调用scrollToIndex方法将一级列表滚动到指定索引值
- 监听curClass变量,onClassChange点击一级列表可视区域边界时,一级列表将选中类别向中间移动
样例代码
interface IRange {
start: number;
end: number;
}//可视区间的开始和结尾
@Entry
@Component
struct Index {
@State itemList:string[]=[]; // 二级列表数据
@State classList:string[]=[]; // 一级列表数据
@State @Watch('onClassChange') curClass:number=-1//当前类别下标
readonly eachClassCount:number[]=[6,4,4,6,5,6,4,4,6,5];//每一个类别item的数量
private classScroller:Scroller=new Scroller();
private itemScroller:Scroller=new Scroller();
private records:number[]=[]; //一个前缀和 二级列表分组count数量
private classVisualRange:IRange={start:0,end:0};
private isClickClassList:boolean=false; //上一次点击是否点击的是类别 true:滑动二级列表,一级列表不跟着一起变化 flase:滑动二级列表,一级列表跟着一起变化
aboutToAppear(): void {
/*
造数据
*/
for(let i=0;i<10;i++){
this.classList[this.classList.length]=`第${i+1}类`
for(let j=0;j<this.eachClassCount[i];j++){
this.itemList[this.itemList.length]=`第${i+1}类 第${j+1}个`
}
}
this.records[0]=0;
for(let i=1;i<=10;i++){//最后多一个,方便二级item寻找一级class
this.records[i]=this.records[i-1]+this.eachClassCount[i-1];
}
}
itemFindClassIndex(itemIndex:number):number{
let classIndex:number=0;
for(let i=0;i<10;i++){
if(this.records[i]<=itemIndex&&itemIndex<this.records[i+1]){
classIndex=i;
break;
}
}
return classIndex;
}
classFindItemIndex(classIndex:number):number{
return this.records[classIndex];
}
onClassChange(){
const start=this.classVisualRange.start,end=this.classVisualRange.end;
if(this.curClass===start||this.curClass===start+1){
this.classScroller.scrollToIndex(Math.max(0,this.curClass-1),true)//向上一格作为可视区域第一个
}
else if(this.curClass===end||this.curClass===end-1){
this.classScroller.scrollToIndex(Math.min(10,this.curClass+1),true)//向下一格作为可视区域第一个
}
}
build() {
Row() {
/**
* 一级列表
*/
List({scroller:this.classScroller,space:10, initialIndex: 0}){
ForEach(this.classList,(classItem:string,index:number)=>{
ListItem(){
Text(classItem).width('100%').height('15%').backgroundColor(this.curClass===index?Color.Green:Color.Pink)
.onClick(()=>{
let itemIndex=this.classFindItemIndex(index);
this.curClass=index;
this.itemScroller.scrollToIndex(itemIndex,true)
})
}
})
}.width('30%').height('100%')
.margin({left:20,right:20}).scrollBar(BarState.Off)
.onTouch(()=>{
this.isClickClassList=true;
})
.onScrollIndex((start,end)=>{
this.classVisualRange.start=start;
this.classVisualRange.end=end;
})
/**
* 二级列表
*/
List({scroller:this.itemScroller,space:10}){
ForEach(this.itemList,(item:string,index:number)=>{
ListItem(){
Text(item).width('100%').height('17%').backgroundColor('#999999')
.onClick(()=>{
let classIndex=this.itemFindClassIndex(index);
this.curClass=classIndex;
this.classScroller.scrollToIndex(classIndex,true)
})
}
})
}.width('70%').height('100%').margin({left:20,right:20}).scrollBar(BarState.Off)
.onTouch(()=>{
this.isClickClassList=false;
})
// 性能知识点:onScrollIndex事件在列表滚动时频繁执行,在回调中需要尽量减少耗时和冗余操作,例如减少不必要的日志打印
.onScrollIndex((start,end)=>{//二级列表滑动,判断一级列表是否一起滑动
if(!this.isClickClassList){
let classIndex=this.itemFindClassIndex(start);
this.curClass=classIndex;
this.classScroller.scrollToIndex(classIndex,true)
/**
* scrollToIndex(value: number, smooth?: boolean, align?: ScrollAlign)
* 性能知识点:开启smooth动效时,会对经过的所有item进行加载和布局计算,当大量加载item时会导致性能问题。
*/
}
})
}
.height('100%')
.width('100%')
}
}
扩展
- 把ForEach换成LazyEach,懒加载
- 当种类较多时,要实现“点击一级列表可视区域边界时,选中类别向中间移动”,改进本案例会出现的问题
- 当使用ListItemGroup时,每一个ListItemGroup占List的一个位置,不计ListItemGroup内的ListItem数量