HarmonyOS鸿蒙Next开发中,如何实现网格列表拖拽

HarmonyOS鸿蒙Next开发中,如何实现网格列表拖拽 网格拖拽,此功能很是常见,一般用于频道的编辑或者条目顺序的排列,在鸿蒙的开发中,针对网格的编辑,系统也给出了相关的Api,通过onItemDragStart和在onItemDrop即可轻松实现,onItemDragStart用于设置拖拽过程中的显示,onItemDrop是进行数据交换逻辑处理。我们来详细的看一下,具体的使用方式。

3 回复

根据官方提供,我们随便实现了一个简单的拖拽效果:

@Entry
  @Component
  struct Index {
    @State numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    @Builder
    itemLayout(text: string) {
      Text(text)
        .fontSize(16)
        .backgroundColor(Color.Pink)
        .width(80)
        .height(80)
        .textAlign(TextAlign.Center)
    }

    changeIndex(index1: number, index2: number) { //交换数组位置
      let temp = this.numbers[index1];
      this.numbers[index1] = this.numbers[index2];
      this.numbers[index2] = temp;
    }

    build() {
      Column() {
        Grid() {
          ForEach(this.numbers, (n: number) => {
            GridItem() {
              this.itemLayout(n.toString())
            }
          })
        }
        .columnsTemplate('1fr 1fr 1fr')
          .columnsGap(10)
          .rowsGap(10)
          .editMode(true) //设置Grid是否进入编辑模式
          .onItemDragStart((event: ItemDragInfo, itemIndex: number) => { //第一次拖拽此事件绑定的组件时,触发回调。
            return this.itemLayout(this.numbers[itemIndex].toString()) //设置拖拽过程中显示
          })
          .onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number,
                       isSuccess: boolean) => {
                         if (!isSuccess || insertIndex >= this.numbers.length) {
                           return
                         }
                         this.changeIndex(itemIndex, insertIndex)
                       })
      }.width('100%')
        .height("100%")
    }
  }

cke_4734.png

以上代码我们就很简单的实现了一个网格拖拽,实际运行之后,长按确实发生了拖拽,和对应的item也进行了交换,但是并没有一个移动交换的过程,这是因为没有设置拖拽动画方法,如果想要拖拽过程中有移动动画,只需要设置supportAnimation为true即可,除此之外,还有一个非常重要的属性,那就是editMode,为true即为编辑模式,false即不可编辑。

拖拽的代码很是简单,毕竟官方也给出了相关案例,其中有两个点是未给出的,那就是禁止拖拽,和禁止与其交换。

比如,第一个和第二个条目,不能拖拽,如何进行设置呢?再比如,任何条目都不能和第一个进行交换,又该如何设置呢?

禁止拖拽

还是上述的代码,例如,把第一个条目禁止,不让其执行拖拽,实现起来很是简单,在onItemDragStart方法里,如果触摸的是指定索引,不让其执行即可。

.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { 
  //第一次拖拽此事件绑定的组件时,触发回调。
  if (itemIndex == 0) {
    //禁止拖拽
    return
  }
  return this.itemLayout(this.numbers[itemIndex].toString()) //设置拖拽过程中显示
})

需要注意的是,以上的代码仅仅是做到了禁止拖拽,但是,有一个潜在的问题是,别的条目是可以和它进行交换的,交换过之后,由于它的索引发生了变化,就变得可以拖拽了,如果想实现真正的,即便被交换后也不能拖拽,那就不能判断索引了,可以以唯一值进行判断,比如item数据换成对象,在对象里定义唯一值。

禁止交换

实际的开发中,除了某个条目禁止拖拽之外,也有不能和它进行交换的逻辑,比如第一个条目,就是固定的,不仅仅禁止拖拽,也不能和其进行交换,这种情况下如何进行实现呢?

onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number,
            isSuccess: boolean) => {
              //第一个禁止交换
              if (insertIndex <= 0) {
                return
              }
              if (!isSuccess || insertIndex >= this.numbers.length) {
                return
              }
              this.changeIndex(itemIndex, insertIndex)
            })

很是简单,如果是第一个,直接不执行即可,以上的代码虽然实现了禁止交换,但是有一个问题,那就是实际的动画已经执行了,只是最后的交换没有执行而已,在视觉上仍然存在很大的问题。

图片

我们想要的效果是,保留移动拖拽的动画,只是拖拽到第一个时,动画不执行,其他的该怎么执行就怎么执行。

那么,在设置supportAnimation动画的时候,就不能直接设置为true了,而是移动到条目一的位置后,设置为fasle,其他再设置为true。

如何拿到第一个item的坐标呢,其实每一个组件都有一个方法,onAreaChange,通过这个属性,我们就可以拿到任何一个组件的宽高还有,xy的坐标,当然,你也可以通过计算的方式,毕竟,屏幕的宽高,还有条目的宽高你都知道。

手势移动的坐标可以通过onTouch方法进行获取,在Move事件中进行判断,如果移动到了条目一的范围之内,取消动画,否则就执行动画。

图片

简单实现

目前呢这个网格拖拽功能已经做了一层封装,放到了refresh库中,大家如果想直接使用,可以依赖这个库:

"dependencies": { "@abner/refresh": "^1.4.7"}

简单案例如下:

import { GridDropView } from '@abner/refresh'

@Entry
  @Component
  struct Index {
    @State numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    @Builder
    itemLayout(item: Object, _: number) {
      Text(item.toString())
        .fontSize(16)
        .backgroundColor(Color.Pink)
        .width(80)
        .height(80)
        .textAlign(TextAlign.Center)
    }



    build() {
      Column() {
        GridDropView({
          columnsTemplate:"1fr 1fr 1fr",
          items: this.numbers,
          itemLayout: this.itemLayout,
          dropLayout: this.itemLayout,
          rowsGap: 10,
          columnsGap: 10,
          isEditMode: true,
          prohibitDrop: [0, 1], //禁止拖拽的索引
          prohibitMaxSwap: 0, //禁止交换
          onDropData: (items) => {
            console.log("拖拽结束:" + JSON.stringify(items))
          }
        })
      }.width('100%')
        .height("100%")

    }
  }

注意事项

实现拖拽,最重要的三个方法就是,打开编辑状态editMode,实现onItemDragStart和onItemDrop,设置拖拽移动动画和交换数据,如果想到开启补位动画,还需要实现supportAnimation方法。

更多关于HarmonyOS鸿蒙Next开发中,如何实现网格列表拖拽的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS鸿蒙Next中,网格列表拖拽可通过ArkUI的Grid组件结合拖动手势实现。使用Grid的onDragStart、onDragEnter、onDragMove、onDrop等事件回调处理拖拽逻辑。通过设置GridItem的allowDrop属性控制是否允许放置。拖拽过程中,利用事件参数获取拖拽源与目标信息,动态更新数据源以完成位置交换。

在HarmonyOS Next中,实现网格列表的拖拽功能主要依赖于ListItem组件的拖拽事件和Grid组件的配合。您提到的onItemDragStartonItemDrop是正确的核心API。

关键实现步骤:

  1. 启用拖拽:在GridListItem子组件上,通过.onDragStart方法设置onItemDragStart回调。在此回调中,您需要:

    • 使用event.pixelMap设置拖拽时跟随手指的预览图(可获取当前Item的截图)。
    • 调用event.setData传递当前拖拽项的关键数据(如其在数据源中的索引index)。
  2. 处理放置:在Grid组件上,通过.onDrop方法设置onItemDrop回调。在此回调中:

    • 通过event.getData获取步骤1中传递的拖拽源数据。
    • 通过event.getTargetIndex或相关位置信息获取拖拽目标位置。
    • 执行数据源中两个位置数据的交换或插入逻辑。
    • 更新Grid的数据源(@State@Observed修饰的数组),UI将自动刷新。

简要示例代码结构:

@Entry
@Component
struct DragGridExample {
  @State items: Array<string> = ['A', 'B', 'C', 'D', 'E', 'F']

  build() {
    Grid() {
      ForEach(this.items, (item: string, index: number) => {
        ListItem() {
          Text(item)
            .fontSize(30)
            .width('100%')
            .height('100%')
            .textAlign(TextAlign.Center)
            .backgroundColor(Color.Orange)
        }
        .onDragStart((event: DragEvent) => {
          // 1. 设置拖拽预览图(此处示例为生成一个简单的像素图,实际建议使用当前视图的截图)
          let color = new ArrayBuffer(4)
          let opts: image.PixelMapOptions = { ... }
          event.pixelMap = image.createPixelMap(color, opts)
          // 2. 传递拖拽项索引
          event.setData('index', index)
        })
      })
    }
    .onDrop((event: DragEvent) => {
      // 1. 获取拖拽源索引
      let fromIndex: number = event.getData('index')
      // 2. 获取目标位置索引(此处需根据event详细信息计算,可能与Grid的布局方式有关)
      let toIndex: number = this.calculateTargetIndex(event)
      // 3. 交换数据
      if (fromIndex !== toIndex && toIndex >= 0 && toIndex < this.items.length) {
        let temp = this.items[fromIndex]
        this.items.splice(fromIndex, 1)
        this.items.splice(toIndex, 0, temp)
        // 4. 状态更新驱动UI刷新
        this.items = [...this.items]
      }
    })
  }

  calculateTargetIndex(event: DragEvent): number {
    // 根据event.offsetX, event.offsetY及Grid的列数、Item尺寸计算具体插入位置
    // 返回目标索引
    return 0 // 此处为示例,需实现具体逻辑
  }
}

注意事项:

  • 索引计算onItemDrop回调中的目标索引toIndex通常需要您根据拖拽释放时的坐标(event.offsetX/Y)、Grid的列数以及每个网格项的尺寸自行计算得出,这是实现精准拖拽排序的关键。
  • 性能:对于复杂或数据量大的列表,建议在数据交换时使用状态管理进行优化,避免不必要的UI重绘。
  • 视觉反馈:可在拖拽过程中,通过修改源Item或目标位置的样式(如透明度、背景色)来提供更好的视觉交互反馈。

此方法利用了HarmonyOS ArkUI的声明式UI与状态管理特性,通过数据源的变化自动更新视图,是实现拖拽排序的推荐方式。

回到顶部