鸿蒙开发:案例集合List:ListItem拖拽(交换位置,过渡动画)(性能篇)

🎯 案例集合List:ListItem拖拽(交换位置,过渡动画)(性能篇)

🌍 案例集合List

🚪 最近开启了学员班级点我欢迎加入(学习相关知识可获得定制礼盒)

🏷️ 效果图

📖 参考

🧩 拆解

  • 原生(onMove)Repeat (Api 19+特性)(推荐)
javascript 复制代码
/**
 * 模拟数据
 */
const LIST_DATA: string[] = [
  "购物", "体育", "服装", "军事", "电商", "娱乐", "科技", "艺术", "新闻", "电商", "农村",
  "旅游", "教育", "健康", "美食", "房产", "汽车", "财经", "文化", "音乐", "电影", "书籍",
  "宠物", "家居", "美妆", "母婴", "职场", "环保", "历史", "游戏"
]

@ComponentV2
export struct listDragAndDrop {
  @Local listData: string[] = LIST_DATA

  @Builder
  listItemBuilder(obj: RepeatItem<string>) {
    Text(obj.item)
      .width('100%')
      .height(110)
      .fontSize(20)
      .fontColor(Color.White)
      .textAlign(TextAlign.Center)
      .borderRadius(20)
      .backgroundColor('#803c6ad0')
  }

  build() {
    Column() {
      List() {
        Repeat<string>(this.listData)
          .each((obj: RepeatItem<string>) => {
            ListItem() {
              this.listItemBuilder(obj)
            }
            .margin({
              left: 10,
              right: 10,
              top: 5,
              bottom: 5
            }) // 不添加这个属性长按放大的时候组件会被遮盖
          })
          .virtualScroll({ totalCount: this.listData.length })
          .onMove((from: number, to: number) => {
            const exchangeItem = this.listData.splice(from, 1)
            this.listData.splice(to, 0, exchangeItem[0])
          })
      }
      .width('100%')
      .height('100%')
      .scrollBar(BarState.Off) // 关闭导航条
      .edgeEffect(EdgeEffect.None) // 关闭边缘动效
    }
    .width('100%')
    .height('100%')
    .padding({ left: 15, right: 15 })
  }
}
  • 原生(onMove)拖拽(LazyForEach + @Reusable)
javascript 复制代码
/**
 * 懒加载
 */
export class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = []

  public totalCount(): number {
    return 0
  }

  public getData(index: number): T | undefined {
    return undefined
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener')
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      console.info('remove listener')
      this.listeners.splice(pos, 1)
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded()
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index)
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to)
    })
  }
}

/**
 * 扩展懒加载
 */
export class CommonLazyModel<T> extends BasicDataSource<T> {
  dataArray: Array<T> = new Array()

  /**
   * 获取数据长度
   * @returns
   */
  public totalCount(): number {
    return this.dataArray.length
  }

  /**
   * 获取某项数据
   * @param index
   * @returns
   */
  public getData(index: number): T {
    return this.dataArray[index]
  }

  /**
   * 添加所有数据
   * @param data
   */
  public setAllData(data: Array<T>) {
    this.dataArray = [...data]
    this.notifyDataReload()
  }

  /**
   * 交换数据
   * 无需调用DataChangeListener接口通知数据源变化
   * @param from
   * @param to
   */
  public exchange(from: number, to: number) {
    const exchange = this.dataArray.splice(from, 1)
    this.dataArray.splice(to, 0, exchange[0])
  }

  /**
   * 获取数据
   * @returns
   */
  public getAllData() {
    return this.dataArray
  }

  /**
   * 插入数据
   * @param index
   * @param data
   */
  public addData(index: number, data: T): void {
    this.dataArray.splice(index, 0, data)
    this.notifyDataAdd(index)
  }

  /**
   * 追加数据
   * @param data
   */
  public pushData(data: T): void {
    this.dataArray.push(data)
    this.notifyDataAdd(this.dataArray.length - 1)
  }
}

/**
 * 模拟数据
 */
const LIST_DATA: string[] = [
  "购物", "体育", "服装", "军事", "电商", "娱乐", "科技", "艺术", "新闻", "电商", "农村",
  "旅游", "教育", "健康", "美食", "房产", "汽车", "财经", "文化", "音乐", "电影", "书籍",
  "宠物", "家居", "美妆", "母婴", "职场", "环保", "历史", "游戏"
]

/**
 * TODO: 拖拽不支持使用Repeat,所以只能使用懒加载 + 组件复用 || All in 官方的 scrollerComponents 长列表方案库
 * TODO: LazyForEach + @Reusable 方式 列表数据过小不触发回收/复用
 */
@Component
export struct listDragAndDrop {
  private listData: CommonLazyModel<string> = new CommonLazyModel()

  aboutToAppear(): void {
    for (let i = 0; i < LIST_DATA.length; i++) {
      this.listData.pushData(LIST_DATA[i] + "_" + i.toString())
    }
  }

  build() {
    Column() {
      List({ space: 10 }) {
        LazyForEach(this.listData, (item: string) => {
          ListItem() {
            ListItemCase({ name: item })
          }
        }, (item: string) => item)
          .onMove((from: number, to: number) => this.listData.exchange(from, to))
      }
      .width('100%')
      .height('100%')
      .scrollBar(BarState.Off)
      .edgeEffect(EdgeEffect.None)
    }
    .width('100%')
    .height('100%')
    .padding({ left: 15, right: 15 })
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

@Reusable
@Component
export struct ListItemCase {
  @State name: string = ''

  aboutToReuse(params: Record<string, Object>): void {
    this.name = params.name as string
    console.info(`${this.name}---复用`)
  }

  aboutToRecycle() {
    console.info(`${this.name}---回收`)
  }

  build() {
    Text(this.name)
      .width('100%')
      .height(110)
      .fontSize(20)
      .fontColor(Color.White)
      .textAlign(TextAlign.Center)
      .backgroundColor('#803c6ad0')
  }
}
  • 原生(onItemDragStart, onItemDragMove, onItemDrop)拖拽(LazyForEach + @Reusable)
javascript 复制代码
/**
 * 懒加载
 */
export class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = []

  public totalCount(): number {
    return 0
  }

  public getData(index: number): T | undefined {
    return undefined
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener')
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      console.info('remove listener')
      this.listeners.splice(pos, 1)
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded()
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index)
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to)
    })
  }
}

/**
 * 扩展懒加载
 */
export class CommonLazyModel<T> extends BasicDataSource<T> {
  dataArray: Array<T> = new Array()

  /**
   * 获取数据长度
   * @returns
   */
  public totalCount(): number {
    return this.dataArray.length
  }

  /**
   * 获取某项数据
   * @param index
   * @returns
   */
  public getData(index: number): T {
    return this.dataArray[index]
  }

  /**
   * 添加所有数据
   * @param data
   */
  public setAllData(data: Array<T>) {
    this.dataArray = [...data]
    this.notifyDataReload()
  }

  /**
   * 交换数据
   * @param idx_1
   * @param idx_2
   */
  public swapPositions(idx_1: number, idx_2: number) {
    // 利用中间常量存交换值
    const exchange = this.dataArray.splice(idx_1, 1)
    this.dataArray.splice(idx_2, 0, exchange[0])
    // 需要主动调用通知数据更新
    this.notifyDataReload()
  }

  /**
   * 获取数据
   * @returns
   */
  public getAllData() {
    return this.dataArray
  }

  /**
   * 插入数据
   * @param index
   * @param data
   */
  public addData(index: number, data: T): void {
    this.dataArray.splice(index, 0, data)
    this.notifyDataAdd(index)
  }

  /**
   * 追加数据
   * @param data
   */
  public pushData(data: T): void {
    this.dataArray.push(data)
    this.notifyDataAdd(this.dataArray.length - 1)
  }
}

/**
 * 模拟数据
 */
const LIST_DATA: string[] = [
  "购物", "体育", "服装", "军事", "电商", "娱乐", "科技", "艺术", "新闻", "电商", "农村",
  "旅游", "教育", "健康", "美食", "房产", "汽车", "财经", "文化", "音乐", "电影", "书籍",
  "宠物", "家居", "美妆", "母婴", "职场", "环保", "历史", "游戏"
]

@Component
export struct listDragAndDrop {
  /**
   * 模拟数据
   */
  private listData: CommonLazyModel<string> = new CommonLazyModel()
  /**
   * 列表构造器
   */
  private listScroller: Scroller = new Scroller()
  /**
   * 当前拖拽某个item的下标
   */
  @State curListItemDragIdx: number = -1
  /**
   * 可视区域的开头组件下标
   */
  @State scrollerStart: number = 0
  /**
   * 可视区域的结尾组件下标
   */
  @State scrollerEnd: number = 0

  aboutToAppear(): void {
    for (let i = 0; i < LIST_DATA.length; i++) {
      this.listData.pushData(LIST_DATA[i] + "_" + i.toString())
    }
  }

  /**
   * 拖拽样式
   */
  @Builder
  draggingBuilder(curListItemDragIdx: number) {
    Text(this.listData.getData(curListItemDragIdx))
      .width('100%')
      .height(110)
      .fontSize(20)
      .fontColor(Color.White)
      .textAlign(TextAlign.Center)
      .backgroundColor('#803c6ad0')
  }

  build() {
    Column() {
      List({ space: 10, scroller: this.listScroller }) {
        LazyForEach(this.listData, (item: string, idx: number) => {
          ListItem() {
            ListItemCase({
              name: item,
              index: idx,
              curListItemDragIdx: this.curListItemDragIdx
            })
          }
        }, (item: string) => item)
      }
      .width('100%')
      .height('100%')
      .scrollBar(BarState.Off)
      .edgeEffect(EdgeEffect.None)
      .onScrollIndex((start: number, end: number, center: number) => {
        this.scrollerStart = start
        this.scrollerEnd = end
      })
      // 开始拖拽列表元素时触发
      .onItemDragStart((event: ItemDragInfo, curListItemDragIdx: number) => {
        // 拖拽中显示的内容
        return this.draggingBuilder(curListItemDragIdx)
      })
      .onItemDragMove((event: ItemDragInfo, itemIndex: number, insertIndex: number) => {
        // 更新当前拖拽到某个item的下标
        this.curListItemDragIdx = insertIndex

        // 向下滑动
        if (insertIndex === this.scrollerEnd && this.scrollerEnd <= this.listData.totalCount() - 1) {
          this.listScroller.scrollToIndex(insertIndex - 5, true)
        }

        // 向上滑动
        if (insertIndex === this.scrollerStart && this.scrollerStart !== 0) {
          this.listScroller.scrollToIndex(insertIndex - 1, true)
        }
      })
      // 拖拽结束
      .onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
        // 当前手势没释放 | 插入位置小于0 | 插入位置大于列表长度
        if (!isSuccess || insertIndex < 0 || insertIndex >= this.listData.totalCount()) {
          return
        }
        this.curListItemDragIdx = -1
        this.listData.swapPositions(itemIndex, insertIndex)
      })
    }
    .width('100%')
    .height('100%')
    .padding({ left: 15, right: 15 })
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

@Reusable
@Component
export struct ListItemCase {
  @State name: string = ''
  @Prop index: number = 0
  @Prop curListItemDragIdx: number = -1
  private itemScale: number = 0.75

  aboutToReuse(params: Record<string, Object>): void {
    this.name = params.name as string
    console.info(`${this.name}---复用`)
  }

  aboutToRecycle() {
    console.info(`${this.name}---回收`)
  }

  build() {
    Text(this.name)
      .width('100%')
      .height(110)
      .fontSize(20)
      .fontColor(Color.White)
      .textAlign(TextAlign.Center)
      .backgroundColor('#803c6ad0')
      .scale({
        x: this.curListItemDragIdx === this.index ? this.itemScale : 1,
        y: this.curListItemDragIdx === this.index ? this.itemScale : 1
      })
      .animation({ curve: Curve.Smooth, duration: 200 })
  }
}

🌸🌼🌺

相关推荐
食品一少年2 小时前
开源鸿蒙 PC · Termony 自验证环境搭建与外部 HNP 集成实践(DAY4-10)(2)
华为·harmonyos
waeng_luo3 小时前
[鸿蒙2025领航者闯关] 鸿蒙应用中如何管理组件状态?
前端·harmonyos·鸿蒙·鸿蒙2025领航者闯关·鸿蒙6实战·开发者年度总结
不老刘3 小时前
HarmonyOS ArkTS IconFont 实践指南
harmonyos·鸿蒙·iconfont
盐焗西兰花14 小时前
鸿蒙学习实战之路:Tabs 组件开发场景最佳实践
学习·华为·harmonyos
盐焗西兰花14 小时前
鸿蒙学习实战之路 - 瀑布流操作实现
学习·华为·harmonyos
lqj_本人16 小时前
Flutter 适配鸿蒙桌面快捷入口完整指南
flutter·华为·harmonyos
春卷同学16 小时前
足球游戏 - Electron for 鸿蒙PC项目实战案例
游戏·electron·harmonyos
春卷同学19 小时前
篮球游戏 - Electron for 鸿蒙PC项目实战案例
游戏·electron·harmonyos
赵财猫._.19 小时前
【Flutter x 鸿蒙】第一篇:环境搭建与第一个鸿蒙Flutter应用运行
flutter·华为·harmonyos