鸿蒙-List和Grid拖拽排序:仿微信小程序删除效果

0 阅读2分钟

前言

今天来实现一下拖拽排序功能。对于鸿蒙中的控件来说,我们可以通过将draggable属性设置为true,并在onDragStart等接口中实现数据传输相关内容来实现拖拽能力,但对于 List 和 Grid 来讲,有几个特殊的用法。

List 的拖拽排序

准确来讲,应该是List + ForEach/LazyForEach/Repeat 生成的ListItem组件才会生效。 我们可以通过ForEach/LazyForEach/RepeatonMove回调来完成拖拽排序

List({ space: 20 }) {
  ForEach(this.numberData, (item: string) => {
    ListItem(){
      Text(`${item}`)
        .width('100%')
        .height(80)
        .textAlign(TextAlign.Center)
        .backgroundColor(Color.White)
        .fontColor(Color.Black)
    } .borderRadius(8)

  }, (item: number) => item.toString())
    .onMove((from: number, to: number) => {
      let tmp = this.numberData.splice(from, 1);
      this.numberData.splice(to, 0, tmp[0]);
    })
}.width('100%').height(500)

这里需要注意下在onMove会调用中处理一下数据,让数据和实际展示内容一致。

看下效果:

list_drag.gif
可以看到,能实现基本的拖拽排序,也可以触发滑动,但无法拖拽出 List 组件的范围。

Grid

由于onMove只能在父组件是List的情况下有效果,在Grid组件中,我们可以使用onItemDragStartonItemDrop回调来实现相同的效果。 相比于onMove回调,onItemDragStartonItemDrop回调给了更多的参数,我们可以做更多的效果,并且还可以将GridItem拖拽到Grid组件的范围之外。缺点就是无法自动触发Grid的滑动。

先看下怎么做拖拽排序:

  1. Grid 设置editMode属性为 true,这样可以拖拽Grid组件内部GridItem。
  2. Grid 设置supportAnimation属性为 true,这样在拖拽的时候会有动画效果,不会太生硬。
  3. 重写onItemDragStart回调,该方法在开始拖拽网格元素时触发。返回void表示不能拖拽。但是需要注意:由于拖拽检测也需要长按,且事件处理机制优先触发子组件事件,GridItem上绑定LongPressGesture时无法触发拖拽。如有长按和拖拽同时使用的需求可以使用通用拖拽事件。
  4. 重写onItemDrop,处理数据。注意:不重写该方法时无法触发拖拽动效。
  5. ForEachLazyForEachRepeat都可以使用

下面看下使用ForEach代码实现:

@Builder
// 拖拽过程样式
pixelMapBuilder(text:string) {
  Column() {
    Text(text)
      .fontSize(16)
      .backgroundColor(0xF9CF93)
      .width(80)
      .height(80)
      .textAlign(TextAlign.Center)
      .borderRadius(8)
  }
}
//数据
data: number[] = [0,1,2,34,5,6,7,8,9,10,11,12,13,14,15]
//Grid
Grid() {
  LazyForEach(this.data, (day: string) => {
    GridItem() {
      Text(day)
        .fontSize(16)
        .backgroundColor(0xF9CF93)
        .width('100%')
        .aspectRatio(1)
        .textAlign(TextAlign.Center)
        .borderRadius(8)
    }
  }, (day: string) => day)
}
.width('100%')
.height(300)
.columnsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.backgroundColor(Color.Orange)
.editMode(true)
.supportAnimation(true)
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // 第一次拖拽此事件绑定的组件时,触发回调。
  console.error('开始拖拽')
  return this.pixelMapBuilder(`${this.data[itemIndex]}` ); // 设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
  console.error( `onItemDrop`)
  if(isSuccess){
    let tmp = this.data.splice(itemIndex, 1);
    this.data.splice(insertIndex, 0, tmp[0]);
  }
})

这里需要注意的是onItemDrop回调中的isSuccess,当该参数为false时,表示松开拖拽时拖拽的项目落在了Grid组件范围之外。如果为true,则处理一下数据。

grid_drag.gif

拖拽删除

既然 Grid 可以 GridItem可以拖拽出 Grid 的范围,并且在 onItemDrop的时候可以拿到坐标信息,我们就可以做一个丐版的微信小程序删除效果了。

grid_drag_delete.gif

我们需要注意的是:当删除区域位于 Grid 组件范围之外的情况下,我们只能通过onItemDrop回调来判断结束拖拽位置的坐标,因为onItemDragMove方法在GridItem拖拽出Grid区域之后就不再回调了。

实现起来也挺简单的

  1. 计算删除组件的坐标
  2. onItemDrop中判断结束拖拽的时候,是否在删除组件的范围内,在的话就删除数据。
@State data: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
private deleteViewHeight: number = 100 //这里固定删除控件的高度
@State deleteViewOffset: number = this.deleteViewHeight //默认不展示删除控件,在`onItemDragStart`回调时再展示
@State screenHeight: number = 0
@State deleteViewRawPositionY: number = 0
@State deleteViewTop: number = 0
@State statusBarHeight: number = 0
@State bottomNavBar: number = 0

计算我们需要的数据

aboutToAppear(): void {
  this.screenHeight = this.getUIContext().px2vp(display.getDefaultDisplaySync().height)
  console.error(`DraggedGridPage:screenHeight -> ${this.screenHeight}`)
  window.getLastWindow(this.getUIContext().getHostContext()).then((win) => {
    this.bottomNavBar = this.getUIContext().px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect.height)
    console.error(`DraggedGridPage bottomNavBar-> ${this.bottomNavBar}`)
    this.statusBarHeight =
      this.getUIContext().px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height)
    console.error(`DraggedGridPage:statusBarHeight-> ${this.getUIContext().px2vp(this.statusBarHeight)}`)
    this.deleteViewTop = this.screenHeight - this.statusBarHeight - this.deleteViewHeight
    console.error(`DraggedGridPage:deleteViewTop-> ${this.deleteViewTop}`)
  })
}

布局逻辑

build() {
    Column() {
      Blank().height(200).width(0)
      Grid(this.scroller) {}
      .onItemDragStart((event: ItemDragInfo, itemIndex: number) => {
        this.text = this.data[itemIndex].toString();
        this.getUIContext().animateTo({
          duration: 1000,
          curve: curves.interpolatingSpring(0, 1, 400, 38)
        }, () => {
          this.deleteViewOffset = 0
        })
        return this.pixelMapBuilder(); // 设置拖拽过程中显示的图片。
      })

      .onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number,
        isSuccess: boolean) => {
        let top = this.deleteViewTop + 80// 80是 GridItem 的高度
        console.error(`onItemDrop: isSuccess->${isSuccess} y->${event.y}     top-> ${top}`)
        if (isSuccess) {
          let tmp = this.data.splice(itemIndex, 1);
          this.data.splice(insertIndex, 0, tmp[0]);
        } else {
          if (event.y > top) {
            console.error(`item 进入删除区域,删除第 ${itemIndex} 个`)
            this.data.splice(itemIndex, 1)
            console.error(`删除后的数据 ${JSON.stringify(this.data)}`)
          } else {
            console.error(`item没有进入删除区域`)
          }
        }
        this.getUIContext().animateTo({
          duration: 1000,
          curve: curves.interpolatingSpring(0, 1, 400, 38)
        }, () => {
          this.deleteViewOffset = this.deleteViewHeight
        })
      })

      Text('删除')
        .fontColor(Color.White)
        .backgroundColor(Color.Red)
        .width('100%')
        .height(this.deleteViewHeight)
        .textAlign(TextAlign.Center)
        .position({ bottom: -this.bottomNavBar - this.deleteViewOffset })
        .onAreaChange((oldValue, newValue) => {
          this.deleteViewRawPositionY = newValue.position.y as number
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink)
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
  }

这样我们就实现了丐版的微信小程序删除效果。 如果想要完全复制:比如在拖拽进入删除组件时有个震动效果,可以参考示例16(实现GridItem自定义拖拽)

代码

github:github.com/huangyuanlo…

gitcode:gitcode.com/huangyuan_x…