分组列表实践(嵌套列表)

分组列表实践(嵌套列表)

HarmonyOS
2024-05-26 13:59:29
浏览
收藏 0
回答 1
待解决
回答 1
按赞同
/
按时间
fanyu0803

需要支持如下功能点:

1. 分组列表组件

2. 每个独立条目上有复杂布局,特别是输入框、选择按钮等比较难处理的焦点事件等

3. 侧滑删除功能

4. 可以拖拽条目效果

5. 子条目数据会引起列表部分数据或者整体数据的属性更新。

这里总结一个完善的分组列表实践案例,当前实现功能点如下:

  •  分组列表列表的数据懒加载
  •  分组的收起与展开
  •  子列表数据操作————新增与侧滑删除
  •  跨分组拖拽条目

暂未实现功能:

  •  子列表的数据编辑操作

这个功能需要用到@Observed和@ObjectLink修饰,因为数据嵌套的问题,层级多了以后比较复杂暂未实现,等后续深度监听需求落地后更新

使用的核心API

核心代码解释

数据懒加载DataSource的封装

lazyForEach的实现需要实现IDataSource类,这里使用TS泛型对齐进行一个简单的封装。

export default class BasicDataSource<T> implements IDataSource { 
  private listeners: DataChangeListener[] = []; 
  private originDataArray: Array<T> = []; 
 
  public totalCount(): number { 
    return 0; 
  } 
 
  public getData(index: number): T { 
    return this.originDataArray[index]; 
  } 
 
  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听 
  registerDataChangeListener(listener: DataChangeListener): void { 
    if (this.listeners.indexOf(listener) < 0) { 
      console.info('add listener'); 
      this.listeners.push(listener); 
    } 
  } 
 
  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听 
  unregisterDataChangeListener(listener: DataChangeListener): void { 
    const pos = this.listeners.indexOf(listener); 
    if (pos >= 0) { 
      console.info('remove listener'); 
      this.listeners.splice(pos, 1); 
    } 
  } 
 
  // 通知LazyForEach组件需要重载所有子组件 
  notifyDataReload(): void { 
    this.listeners.forEach(listener => { 
      listener.onDataReloaded(); 
    }) 
  } 
 
  // 通知LazyForEach组件需要在index对应索引处添加子组件 
  notifyDataAdd(index: number): void { 
    this.listeners.forEach(listener => { 
      listener.onDataAdd(index); 
    }) 
  } 
 
  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件 
  notifyDataChange(index: number): void { 
    this.listeners.forEach(listener => { 
      listener.onDataChange(index); 
    }) 
  } 
 
  // 通知LazyForEach组件需要在index对应索引处删除该子组件 
  notifyDataDelete(index: number): void { 
    this.listeners.forEach(listener => { 
      listener.onDataDelete(index); 
    }) 
  } 
 
  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换 
  notifyDataMove(from: number, to: number): void { 
    this.listeners.forEach(listener => { 
      listener.onDataMove(from, to); 
    }) 
  } 
}

MyDataSource实现自己的DataSource具体实现

同样是使用TS泛型来实现,因为后期会有嵌套列表且都需要使用懒加载的场景,这里的类型不能写成定值。

import BasicDataSource from './BasicDataSource'; 
 
export default class MyDataSource<T> extends BasicDataSource<T> { 
  private dataArray: Array<T> = [] 
 
  public totalCount(): number { 
    return this.dataArray.length; 
  } 
 
  public getData(index: number): T { 
    return this.dataArray[index]; 
  } 
 
  public addData(index: number, data: T): void { 
    this.dataArray.splice(index, 0, data); 
    this.notifyDataAdd(index); 
  } 
 
  public pushData(data: T): void { 
    this.dataArray.push(data); 
    this.notifyDataAdd(this.dataArray.length - 1); 
  } 
 
  public deleteData(data: T) { 
    let index = this.dataArray.indexOf(data) 
    this.dataArray.splice(index, 1) 
    this.notifyDataDelete(index) 
  } 
 
  public changeData(data: T, index: number) { 
    this.dataArray[index] = data 
    this.notifyDataChange(index) 
    console.log(`changeData ${this.dataArray.toString()}`) 
  } 
} 
 
@Observed 
export class TimeTable { 
  title: string; 
  projects: string[]; 
 
  constructor(title: string, projects: string[]) { 
    this.title = title 
    this.projects = projects 
  } 
}

同时在这里也定义了具体的数据类TimeTable,因为是对象数组,这里我们需要监听title与projects的变化。所以需要使用@Observed来修饰。

以上一个简单的数据封装就实现了,下面我们来看下组件层面的实现。

嵌套列表数据懒加载功能

Index.ets

这里因为我们需要懒加载功能所以需要new MyDataSource并且指定其类型为TimeTable这是我们的第一层列表,主要是遍历第一层数据讲所有子表全部渲染出来,主要使用还要看嵌套的第二层列表。

import MyDataSource, { TimeTable } from './model/MyDataSource' 
 
@Entry 
@Component 
struct ListItemGroupExample { 
  @State timeTableList: MyDataSource<TimeTable> = new MyDataSource() 
 
  aboutToAppear() { 
    let data1 = new TimeTable("星期一", ['语文', '数学', '英语']) 
    let data2 = new TimeTable("星期二", ['物理', '化学', '生物']) 
    let data3 = new TimeTable("星期三", ['历史', '地理', '政治']) 
    let data4 = new TimeTable("星期四", ['美术', '音乐', '体育']) 
    this.timeTableList.pushData(data1) 
    this.timeTableList.pushData(data2) 
    this.timeTableList.pushData(data3) 
    this.timeTableList.pushData(data4) 
  } 
 
  build() { 
    Column() { 
      List({ space: 20 }) { 
        LazyForEach(this.timeTableList, (item: TimeTable, index?: number) => { 
          ListItem({ style: ListItemStyle.CARD }) { 
            ChildList({ data: item, index: index }) 
          } 
        }, (item: TimeTable) => item.title) 
      } 
      .width('90%') 
      .height("100%") 
      .scrollBar(BarState.Off) 
    }.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 }) 
  } 
}

第二层ChildList实现如下:

因为需要涉及到对象数组的数据更新所以我们需要定义如下参数,由于第二次数据也有列表,并且也需要支持懒加载,所以在第二层我们也需要new MyDataSource这一层的类型就是对应的子表数据类型了,这里只嵌套了两层就直接指定为string了。

@ObjectLink data: TimeTable  // 用于接收上层传来的数据 
@Prop index: number       // 用于知道当前的子列表index 
@State itemHeight: number = 100 // 子表每一项的高度 
@State projects: MyDataSource<string> = new MyDataSource() //  
@State text: string = ""

在组件创建的时候就可以讲上层传进来的数据在aboutToAppear生命周期中初始化。

请注意这里需要使用MyDataSource的新增数据方法来初始化,否则后续无法支持懒加载。

aboutToAppear() { 
  this.data.projects.forEach((item: string) => { 
    this.projects.pushData(item) 
  }) 
}

其他实现与第一层一致,也是套一层List然后lazyForEach遍历渲染即可。

收起展开功能

该功能主要在子列表中实现,主要思路就是改变子列表项高度来实现,核心代码如下:

Row() { 
  Text(this.data.title) 
    .fontSize(20) 
    .padding(10) 
  Row() { 
    Button("新增数据") 
      .fontSize(12) 
      .onClick(() => { 
        animateTo({ duration: 300 }, () => { 
          this.projects.pushData("新增数据") 
        }) 
      }) 
    Button(this.itemHeight ? "收起" : "展开") 
      .fontSize(12) 
      .onClick(() => { 
        this.itemHeight = this.itemHeight ? 0 : 100 
      }) 
  } 
}.width("100%").justifyContent(FlexAlign.SpaceBetween) 
.backgroundColor(0xAABBCC)

子表新增与侧滑删除功能

新增没什么好说的因为子列表已经实现了数据懒加载,在懒加载中直接pushData或者addData即可,侧滑删除侧是调用了ListItem组件的swipeAction属性来实现,实现代码如下:

需要注意的是,现在所有的数据操作都需要使用DataSource中的接口来操作。

@Builder 
itemEnd() { 
  Row() { 
    Text("删除该项") 
  }.padding("4vp").justifyContent(FlexAlign.SpaceEvenly) 
} 
ListItem() { 
  Row() { 
    Text(item) 
      .fontSize(20) 
      .textAlign(TextAlign.Center) 
  } 
  .width("100%") 
  .justifyContent(FlexAlign.Center) 
  .height(this.itemHeight) 
  .animation({ duration: 300, iterations: 1 }) 
  .backgroundColor(0xFFFFFF) 
  .onClick(() => { 
    this.projects.changeData("更新数据", index) 
  }) 
}.swipeAction({ 
  end: { 
    builder: () => { 
      this.itemEnd() 
    }, 
    onAction: () => { 
      // todo: 待实现功能1侧滑删除,显示实现了数据删除但是UI没有更新 
      animateTo({ duration: 300 }, () => { 
        this.projects.deleteData(item) 
      }) 
    }, 
    actionAreaDistance: 40, 
    onEnterActionArea: () => { 
    }, 
    onExitActionArea: () => { 
    } 
  } 
})

跨分组拖拽条目功能

最新的API中已经取消editMode属性,所以拖拽只要在列表中监听onItemDragStart事件即可开启拖拽功能,同时我们还需要监听onItemDrop让每个子列表都是可以放置的目标,在拖拽放置的时候onItemDrop会拿到两个索引itemIndex——拖拽起始位置,和insertIndex——拖拽插入位置,当我们放置目标的时候人如果不属于该子列表的元素itemIndex会返回-1,所以我们可以在onItemDragStart中将我们拖拽这一项的数据缓存在AppStroge中,放置目标的时候若itemIndex为-1则表示当前这一项不来自自己这个子列表,就吧他pushData进入该子列表即可,同时放置完成后原列表要判断insertIndex是否为-1,若是泽说明拖出去了,那么在源列表中deleteData,代码如下所示:

.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { 
  this.text = this.projects.getData(itemIndex) 
  AppStorage.setOrCreate('dropData', this.projects.getData(itemIndex) as string); 
  return this.pixelMapBuilder() //设置拖拽过程中显示的图片。 
}) 
  .onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => { 
    //绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。 
    if (!isSuccess || insertIndex >= this.projects.totalCount()) { 
      return 
    } 
 
    if (insertIndex == -1) { 
      // 拖出的list 
      animateTo({ duration: 300, iterations: 1 }, () => { 
        this.projects.deleteData(this.projects.getData(itemIndex)) 
      }) 
      console.log("拖出去") 
    } 
 
    if (itemIndex == -1) { 
      // 放置的list 
      animateTo({ duration: 300, iterations: 1 }, () => { 
        this.projects.pushData(AppStorage.get("dropData")) 
      }) 
      console.log("放进来") 
    } 
  })

ChildList完整实现代码如下:

@Component 
struct ChildList { 
  @ObjectLink data: TimeTable 
  @Prop index: number 
  @State itemHeight: number = 100 
  @State projects: MyDataSource<string> = new MyDataSource() 
  @State text: string = "" 
 
  aboutToAppear() { 
    this.data.projects.forEach((item: string) => { 
      this.projects.pushData(item) 
    }) 
  } 
 
  @Builder 
  itemEnd() { 
    Row() { 
      Text("删除该项") 
    }.padding("4vp").justifyContent(FlexAlign.SpaceEvenly) 
  } 
 
  @Builder 
  pixelMapBuilder() { //拖拽过程样式 
    Column() { 
      Text(this.text) 
        .width("100%") 
        .height(this.itemHeight) 
        .fontSize(20) 
        .textAlign(TextAlign.Center) 
        .backgroundColor("#ffeaa7") 
    } 
  } 
 
  build() { 
    Column() { 
      // header 
      Row() { 
        Text(this.data.title) 
          .fontSize(20) 
          .padding(10) 
        Row() { 
          Button("新增数据") 
            .fontSize(12) 
            .onClick(() => { 
              animateTo({ duration: 300 }, () => { 
                this.projects.pushData("新增数据") 
              }) 
            }) 
          Button(this.itemHeight ? "收起" : "展开") 
            .fontSize(12) 
            .onClick(() => { 
              this.itemHeight = this.itemHeight ? 0 : 100 
            }) 
        } 
      }.width("100%").justifyContent(FlexAlign.SpaceBetween) 
      .backgroundColor(0xAABBCC) 
 
      List() { 
        LazyForEach(this.projects, (item: string, index: number) => { 
          ListItem() { 
            Row() { 
              Text(item) 
                .fontSize(20) 
                .textAlign(TextAlign.Center) 
            } 
            .width("100%") 
            .justifyContent(FlexAlign.Center) 
            .height(this.itemHeight) 
            .animation({ duration: 300, iterations: 1 }) 
            .backgroundColor(0xFFFFFF) 
            .onClick(() => { 
              this.projects.changeData("更新数据", index) 
            }) 
          } 
          .swipeAction({ 
            end: { 
              builder: () => { 
                this.itemEnd() 
              }, 
              onAction: () => { 
                // todo: 待实现功能1侧滑删除,显示实现了数据删除但是UI没有更新 
                animateTo({ duration: 300 }, () => { 
                  this.projects.deleteData(item) 
                }) 
              }, 
              actionAreaDistance: 40, 
              onEnterActionArea: () => { 
              }, 
              onExitActionArea: () => { 
              } 
            } 
          }) 
        }) 
      } 
      .width("100%") 
      .scrollBar(BarState.Off) 
      .onItemDragStart((event: ItemDragInfo, itemIndex: number) => { 
        this.text = this.projects.getData(itemIndex) 
        AppStorage.setOrCreate('dropData', this.projects.getData(itemIndex) as string); 
        return this.pixelMapBuilder() //设置拖拽过程中显示的图片。 
      }) 
      .onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => { //绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。 
        if (!isSuccess || insertIndex >= this.projects.totalCount()) { 
          return 
        } 
 
        if (insertIndex == -1) { 
          // 拖出的list 
          animateTo({ duration: 300, iterations: 1 }, () => { 
            this.projects.deleteData(this.projects.getData(itemIndex)) 
          }) 
          console.log("拖出去") 
        } 
 
        if (itemIndex == -1) { 
          // 放置的list 
          animateTo({ duration: 300, iterations: 1 }, () => { 
            this.projects.pushData(AppStorage.get("dropData")) 
          }) 
          console.log("放进来") 
        } 
      }) 
 
      // footer 
      Text('共' + this.data.projects.length + "节课") 
        .fontSize(16) 
        .backgroundColor(0xAABBCC) 
        .width("100%") 
        .padding(5) 
    } 
  } 
}

适配的版本信息

IDE版本:DevEco Studio 4.1.1.300

SDK版本:4.1.3.5

分享
微博
QQ
微信
回复
2024-05-27 16:32:54
相关问题
页面和列表嵌套滚动,实现列表吸顶
481浏览 • 1回复 待解决
如何设置分组列表的圆角和间距
710浏览 • 1回复 待解决
如何实现分组列表的吸顶/吸底效果
875浏览 • 1回复 待解决
拖动实现列表重新排序
341浏览 • 1回复 待解决
关于获取应用列表权限问题?
2081浏览 • 1回复 待解决
列表滑动鸿蒙推荐ux设计
688浏览 • 1回复 待解决
关于权限列表条目缺少问题
613浏览 • 1回复 待解决
怎么读取本地音频文件列表?
4767浏览 • 1回复 待解决
键盘拉起时列表无法上下滑动
780浏览 • 1回复 待解决
视频列表的不规则排列
353浏览 • 1回复 待解决
求大佬告知如何扫描Wi-Fi列表
752浏览 • 1回复 待解决
如何实现列表页的单选效果
1004浏览 • 0回复 待解决
如何更新页面列表数据
5559浏览 • 1回复 待解决
Redis数据类型列表list是什么?
1985浏览 • 1回复 待解决
侧滑删除功能的列表有哪些?
421浏览 • 1回复 待解决
使用LazyForEach懒加载列表相关问题
353浏览 • 1回复 待解决