HarmonyOS:Grid网格元素拖拽交换开发示例

23 阅读8分钟

一、概述

Grid网格元素拖拽交换功能在应用中经常会被使用,如当编辑九宫格图片需要拖拽图片改变排序时,就会使用到该功能。当网格中图片进行拖拽交换时,元素排列会跟随图片拖拽的位置而发生变化,并且会有对应的动画效果,以达到良好的用户体验。
Grid网格布局一般由Grid容器组件和子组件GridItem构建生成,Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。网格布局中含有网格元素,当给Grid容器组件设置editMode属性为true时,可开启Grid组件的编辑模式。开启编辑模式后,还需要给GridItem组件绑定长按拖拽等手势。最后,需要添加显式动画,并设置相应的动画效果。最终,呈现出网格元素拖拽交换的动效过程。

二、实现原理

2.1 关键技术

Grid网格元素拖拽交换功能实现是通过Grid容器组件、组合手势、显式动画结合来实现的。

  • Grid组件可以构建网格元素布局。
  • 组合手势可以实现元素拖拽交换的效果。
  • 显式动画可以给元素拖拽交换的过程中,添加动画效果。

注意 Grid组件当前支持GridItem拖拽动画,通过给Grid容器组件设置supportAnimation为true,即可开启动画效果。但仅支持在滚动模式下(设置rowsTemplatecolumnsTemplate其中一个)支持动画。且仅在大小规则的Grid中支持拖拽动画,跨行或跨列场景不支持。因此,在跨行或跨列场景下,需要通过自定义Gird布局、自定义手势和显式动画来实现拖拽交换的效果。

2.2 开发流程

在需要拖拽交换的场景中:

  • 实现Grid布局,启动editMode编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。
  • 给网络元素GridItem绑定相关手势,实现可拖拽操作。
  • 使用显式动画animateTo,实现GridItem拖拽过程中的动画效果。

三、相同大小网格元素,长按拖拽

3.1 场景描述

在编辑九宫格等多图的场景中,长按图片(网格元素)可以拖拽交换排序,拖拽图片的过程中,旁边的图片也会即时移动,以产生新的宫格排布。效果图如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2 开发步骤

  1. Grid布局及相同大小的GridItem界面开发。其中,scrollBar可设置滚动条状态,值为BarState.Off时,表示不显示滚动条;columnsTemplate可设置当前网格布局列的数量、固定列宽或最小列宽值;columnsGap可设置列与列的间距;rowsGap可设置行与行的间距。
  2. 给Grid组件设置editMode为true,即Grid进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。设置supportAnimation为true,即Grid拖拽元素时支持动画。
  3. 定义拖拽过程中的数组交换逻辑。
  4. 给Grid组件绑定onItemDragStart和onItemDrop事件,在onItemDragStart回调中设置拖拽过程中显示的图片,并在onItemDrop中完成交换数组位置的逻辑。 onItemDragStart回调在开始拖拽网格元素时触发,onItemDrop回调当在网格元素内停止拖拽时触发。

3.3 完整代码示例

class ItemBean2 {
  id: string = "";
  name: string = "";
  value: string = "";

  constructor(id: string, name: string, value: string) {
    this.id = id;
    this.name = name;
    this.value = value;
  }
}

@Component
struct TestSameGridItemDrag{
  @State dataList: Array<ItemBean2> = [
    new ItemBean2("1001", "1", "1"),
    new ItemBean2("1002", "2", "2"),
    new ItemBean2("1003", "3", "3"),
    new ItemBean2("1004", "4", "4"),
    new ItemBean2("1005", "5", "5"),
    new ItemBean2("1006", "6", "6"),
  ];

  /**
   * 定义拖拽过程中的数组交换逻辑
   * @param index1
   * @param index2
   */
  changeIndex(index1: number, index2: number) {
    // let temp = this.dataList.splice(index1, 1);
    // this.dataList.splice(index2, 0, temp[0]);

    let temp = this.dataList[index1];
    this.dataList[index1] = this.dataList[index2];
    this.dataList[index2] = temp;

  }

  @Builder
  itemLayout(item: ItemBean2) {
    Text(item.name)
      .fontSize(40)
      .backgroundColor(Color.Gray)
      .width('30%')
      .height(100)
      .textAlign(TextAlign.Center)
  }

  build() {
    Column({ space: 10 }) {
      Button('添加一个元素')
        .fontSize(20)
        .onClick(() => {
          this.dataList.push(new ItemBean2('1007', '7', '7'))
        })

      Button('删除一个元素')
        .fontSize(20)
        .onClick(() => {
          this.dataList.splice(2, 1) //从第二个位置开始删除一个元素
        })

      GridTitle({ title: "相同大小网格元素,长按拖拽" })
      Grid() {
        ForEach(this.dataList, (item: ItemBean2) => {
          GridItem() {
            this.itemLayout(item)
          }
        }, (item: ItemBean2) => item.id)
      }
      .margin({ top: 50 })
      .columnsGap(8)
      .rowsGap(8)
      .scrollBar(BarState.Off) //不显示滚动条
      .editMode(true) //Grid进入编辑模式
      .supportAnimation(true) //拖拽元素时支持动画
      .onItemDragStart((event: ItemDragInfo, itemIndex: number) => {
        return this.itemLayout(this.dataList[itemIndex])
      })
      .onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
        if (!isSuccess) {
          console.log("拖拽失败 isSuccess = " + isSuccess);
          return
        }
        if (insertIndex >= this.dataList.length) {
          console.log("拖拽失败 insertIndex >= this.dataList.length");
          return
        }
        this.changeIndex(itemIndex, insertIndex);
      })
    }
  }
}

@Component
struct GridTitle {
  title: string = ""

  build() {
    Column() {
      Text(this.title)
        .fontColor(Color.Black)
        .fontSize(20)
        .margin({ left: 20 })
    }
  }
}

@Entry
@Component
struct TestGridDemo2 {
  @State message: string = 'Grid网格元素拖拽交换开发实践';

  build() {
    Column() {
      TestSameGridItemDrag()
    }
    .height('100%')
    .width('100%')
  }
}

四、不同大小网格元素,长按拖拽

4.1 场景描述

在一些展示设备的场景中,会有大小不同的网格元素。当用户想改变设备排序时,可以长按设备图片(网格元素)拖拽交换排序,拖拽的过程中,也会改变排列顺序,以产生新的宫格排布。效果图如下:
在这里插入图片描述
在这里插入图片描述

4.2 开发步骤

  1. Grid布局及不同大小的GridItem界面开发。
  2. 定义网格元素移动过程中的相关计算函数,其中itemMove()方法是实现元素交换重新排序的方法。
  3. GridItem绑定组合手势:长按,拖拽。并在手势的回调函数中设置显式动画。

4.3 完整代码示例

import { curves, display } from '@kit.ArkUI';
import { i18n } from '@kit.LocalizationKit';

@Component
struct GridTitle {
  title: string = ""

  build() {
    Column() {
      Text(this.title)
        .fontColor(Color.Black)
        .fontWeight(FontWeight.Bold)
        .fontSize(20)
        .margin({ left: 20 })
    }
  }
}

@Component
struct TestDifferentGridItemDrag {
  @StorageProp('currentBreakpoint') curBp: string = 'sm';
  @State isFoldAble: boolean = false;
  @State foldStatus: number = 2;
  @State numbers: number[] = [];
  @State dragItem: number = -1;
  @State scaleItem: number = -1;
  @State rotateZ: number = 0;
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State isEnglish: boolean = false;
  private dragRefOffSetX: number = 0;
  private dragRefOffSetY: number = 0;
  private FIX_VP_X: number = 180;
  private FIX_VP_Y: number = 84;
  private bigItemIndex: number = 0;

  aboutToAppear(): void {
    for (let i = 0; i < 19; i++) {
      this.numbers.push(i)
    }

    // ohos.display (屏幕属性)
    this.isFoldAble = display.isFoldable(); // 检查设备是否可折叠。
    let foldStatus: display.FoldStatus = display.getFoldStatus(); // 获取可折叠设备的当前折叠状态。
    if (foldStatus === 2) { // 表示设备当前折叠状态为折叠。
      this.FIX_VP_X = 162;
    } else if (foldStatus === 1) { // 表示设备当前折叠状态为完全展开。
      this.FIX_VP_X = 227;
    }
    if (this.isFoldAble) {
      this.foldStatus = foldStatus;
      let callback: Callback<number> = () => {
        let data: display.FoldStatus = display.getFoldStatus();
        this.foldStatus = data;
        if (this.foldStatus === 2) {
          this.FIX_VP_X = 162;
        } else if (this.foldStatus === 1) {
          this.FIX_VP_X = 227;
        }
      }
      display.on('change', callback); // 开启显示设备变化的监听。
    }
    // i18n: 国际化
    let systemLanguage = i18n.System.getSystemLanguage(); // 获取系统语言,为当前系统语言
    if (systemLanguage === 'en-Latn-US') {
      this.isEnglish = true;
    }
  }

  itemMove(index: number, newIndex: number): void {
    if (!this.isDraggable(newIndex)) {
      return;
    }
    let tmp = this.numbers.splice(index, 1);
    this.numbers.splice(newIndex, 0, tmp[0]);
    this.bigItemIndex = this.numbers.findIndex((item) => item === 0);
  }

  isInLeft(index: number) {
    if (index === this.bigItemIndex) {
      return index % 2 == 0;
    }
    if (this.bigItemIndex % 2 === 0) {
      if (index - this.bigItemIndex === 2 || index - this.bigItemIndex === 1) {
        return false;
      }
    } else {
      if (index - this.bigItemIndex === 1) {
        return false;
      }
    }
    if (index > this.bigItemIndex) {
      return index % 2 == 1;
    } else {
      return index % 2 == 0;
    }
  }

  down(index: number): void {
    if ([this.numbers.length - 1, this.numbers.length - 2].includes(index)) {
      return;
    }
    if (this.bigItemIndex - index === 1) {
      return;
    }
    if ([14, 15].includes(this.bigItemIndex) && this.bigItemIndex === index) {
      return;
    }
    this.offsetY -= this.FIX_VP_Y;
    this.dragRefOffSetY += this.FIX_VP_Y;
    if (index - 1 === this.bigItemIndex) {
      this.itemMove(index, index + 1);
    } else {
      this.itemMove(index, index + 2);
    }
  }

  up(index: number): void {
    if (!this.isDraggable(index - 2)) {
      return;
    }
    if (index - this.bigItemIndex === 3) {
      return;
    }
    this.offsetY += this.FIX_VP_Y;
    this.dragRefOffSetY -= this.FIX_VP_Y;
    if (this.bigItemIndex === index) {
      this.itemMove(index, index - 2);
    } else {
      if (index - 2 === this.bigItemIndex) {
        this.itemMove(index, index - 1);
      } else {
        this.itemMove(index, index - 2);
      }
    }
  }

  left(index: number): void {
    if (this.bigItemIndex % 2 === 0) {
      if (index - this.bigItemIndex === 2) {
        return;
      }
    }
    if (this.isInLeft(index)) {
      return;
    }
    if (!this.isDraggable(index - 1)) {
      return;
    }
    this.offsetX += this.FIX_VP_X;
    this.dragRefOffSetX -= this.FIX_VP_X;
    this.itemMove(index, index - 1)
  }

  right(index: number): void {
    if (this.bigItemIndex % 2 === 1) {
      if (index - this.bigItemIndex === 1) {
        return;
      }
    }
    if (!this.isInLeft(index)) {
      return;
    }
    if (!this.isDraggable(index + 1)) {
      return;
    }
    this.offsetX -= this.FIX_VP_X;
    this.dragRefOffSetX += this.FIX_VP_X;
    this.itemMove(index, index + 1)
  }

  isDraggable(index: number): boolean {
    return index >= 0;
  }

  isBigLeft() {
    if ([0, 2, 4, , 6, 8, 10, 12, 14, 16].includes(this.bigItemIndex)) {
      return true;
    }
    return false
  }

  getHeight(item: number) {
    if (item.toString() === '0') {
      return 158;
    } else {
      return 73;
    }
  }

  getRowEnd(item: number) {
    if (item.toString() === '0') {
      return 1;
    } else {
      return 0;
    }
  }

  changeImage(item: number): ResourceStr {
    if (this.curBp === 'md') {
      return item === 0 ? $r('app.media.device_big') : $r(`app.media.device${item % 3}`);
    }
    return item === 0 ? $r('app.media.Equipment_big') :
      this.isEnglish ? $r(`app.media.device${item % 3}_en`) : $r(`app.media.Equipment${item % 3}`)
  }

  build() {
    Column({ space: 10 }) {
      GridTitle({ title: "不同大小网格元素,长按拖拽" })
      // Row({ space: 12 }) {
      //   Button() {
      //     Text($r('app.string.equipment'))
      //       .fontColor(Color.White)
      //       .width('100%')
      //       .height('100%')
      //       .textAlign(TextAlign.Center)
      //   }
      //   .backgroundColor('#0A59F7')
      //   .height(36)
      //   .width(this.isEnglish ? 120 : 60)
      //
      //   Button() {
      //     Text($r('app.string.space'))
      //       .fontColor(Color.White)
      //       .width('100%')
      //       .height('100%')
      //       .textAlign(TextAlign.Center)
      //   }
      //   .backgroundColor('#6e95ba')
      //   .height(36)
      //   .width(this.isEnglish ? 80 : 60)
      //
      //   Button() {
      //     Text($r('app.string.mine'))
      //       .fontColor(Color.White)
      //       .width('100%')
      //       .height('100%')
      //       .textAlign(TextAlign.Center)
      //   }
      //   .backgroundColor('#6e95ba')
      //   .height(36)
      //   .width(this.isEnglish ? 80 : 60)
      // }
      // .padding({ left: 16, right: 16 })
      // .width('100%')
      // .height(56)
      // .justifyContent(FlexAlign.Start)
      // .margin({ top: -4 })

      Row() {
        Grid() {
          ForEach(this.numbers, (item: number) => {
            GridItem() {
              Stack({ alignContent: Alignment.TopEnd }) {
                Image(this.changeImage(item))
                  .width('100%')
                  .borderRadius(16)
                  .objectFit(this.curBp === 'md' ? ImageFit.Fill : ImageFit.Cover)
                  .draggable(false)
                  .animation({ curve: Curve.Sharp, duration: 300 })
              }
            }
            .rowStart(0)
            .rowEnd(this.getRowEnd(item))
            .scale({ x: this.scaleItem === item ? 1.02 : 1, y: this.scaleItem === item ? 1.02 : 1 })
            .zIndex(this.dragItem === item ? 1 : 0)
            .translate(this.dragItem === item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
            .hitTestBehavior(this.isDraggable(this.numbers.indexOf(item)) ? HitTestMode.Default : HitTestMode.None)
            .gesture(
              GestureGroup(GestureMode.Sequence,
                LongPressGesture({ repeat: true })
                  .onAction(() => {
                    animateTo({ curve: Curve.Friction, duration: 300 }, () => {
                      this.scaleItem = item;
                    })
                  })
                  .onActionEnd(() => {
                    animateTo({ curve: Curve.Friction, duration: 300 }, () => {
                      this.scaleItem = -1;
                    })
                  }),
                PanGesture({ fingers: 1, direction: null, distance: 0 })
                  .onActionStart(() => {
                    this.dragItem = item;
                    this.dragRefOffSetX = 0;
                    this.dragRefOffSetY = 0;
                  })
                  .onActionUpdate((event: GestureEvent) => {
                    this.offsetX = event.offsetX - this.dragRefOffSetX;
                    this.offsetY = event.offsetY - this.dragRefOffSetY;
                    animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                      let index = this.numbers.indexOf(this.dragItem);
                      if (this.offsetY >= this.FIX_VP_Y / 2 && (this.offsetX <= 44 && this.offsetX >= -44)) {
                        this.down(index);
                      } else if (this.offsetY <= -this.FIX_VP_Y / 2 && (this.offsetX <= 44 && this.offsetX >= -44)) {
                        this.up(index);
                      } else if (this.offsetX >= this.FIX_VP_X / 2 && (this.offsetY <= 50 && this.offsetY >= -50)) {
                        this.right(index);
                      } else if (this.offsetX <= -this.FIX_VP_Y / 2 && (this.offsetY <= 50 && this.offsetY >= -50)) {
                        this.left(index);
                      }
                    })
                  })
                  .onActionEnd(() => {
                    animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                      this.dragItem = -1;
                    })
                    animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => {
                      this.scaleItem = -1;
                    })
                  })
              )
                .onCancel(() => {
                  animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                    this.dragItem = -1;
                  })
                  animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => {
                    this.scaleItem = -1;
                  })
                })
            )
          }, (item: number) => item.toString())
        }
        .width('100%')
        .height('100%')
        .editMode(true)
        .scrollBar(BarState.Off)
        .columnsTemplate('1fr 1fr')
        .supportAnimation(true)
        .columnsGap(12)
        .rowsGap(12)
        .enableScrollInteraction(true)
      }
      .width('100%')
      .height('100%')
      .padding({
        left: 16,
        right: 16,
        bottom: this.isFoldAble && this.foldStatus === 1 ? 112 : 84
      })

    }
    .width(this.curBp === 'md' ? '67%' : '100%')
    .height('100%')
  }
}

@Entry
@Component
struct TestGridDemo2 {
  @State message: string = 'Grid网格元素拖拽交换开发实践';

  build() {
    Scroll() {
      Column({ space: 20 }) {
        //相同大小网格元素,长按拖拽
        //TestSameGridItemDrag()
        TestDifferentGridItemDrag()
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .height('100%')
    .width('100%')

  }
}

五、两个Grid之间网格元素交换

当场景涉及两个Grid之间的网格元素交换时,可使用GridObjectSortComponent组件来实现。可以点击添加或者移除按钮,对网格元素进行交换。
点击查看HarmonyOS:GridObjectSortComponent(两个Grid之间网格元素交换)

六、网格元素直接拖拽,不需长按

6.1 场景描述

在不需要长按拖拽的场景下,开发者可以将元素设置成直接拖拽,无需长按,即可完成元素的拖拽交换。效果图如下:
在这里插入图片描述
在这里插入图片描述

6.2 开发步骤

  1. 使用Grid布局及GridItem界面开发。
  2. 定义网格元素移动过程中的相关计算函数。
  3. GridItem绑定拖拽手势,并在手势的回调函数中设置显式动画。

6.3 完整示例代码

import { curves, display } from '@kit.ArkUI';
import { i18n } from '@kit.LocalizationKit';

@Component
struct GridTitle {
  title: string = ""

  build() {
    Column() {
      Text(this.title)
        .fontColor(Color.Black)
        .fontWeight(FontWeight.Bold)
        .fontSize(20)
        .margin({ left: 20 })
    }
  }
}

@Component
struct TestGridDirectDragItem {
  @StorageProp('currentBreakpoint') curBp: string = 'sm';
  @State isFoldAble: boolean = false;
  @State foldStatus: number = 2;
  @State numbers: number[] = [];
  @State dragItem: number = -1;
  @State scaleItem: number = -1;
  @State item: number = -1;
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State isEnglish: boolean = false;
  private dragRefOffSetX: number = 0;
  private dragRefOffSetY: number = 0;
  private FIX_VP_X: number = 92;
  private FIX_VP_Y: number = 84;

  aboutToAppear(): void {
    for (let i = 1; i <= 5; i++) {
      this.numbers.push(i);
    }
    this.isFoldAble = display.isFoldable();
    let foldStatus: display.FoldStatus = display.getFoldStatus();
    if (foldStatus === 2) {
      this.FIX_VP_X = 81;
    } else if (foldStatus === 1) {
      this.FIX_VP_X = 110;
    }
    if (this.isFoldAble) {
      this.foldStatus = foldStatus;
      let callback: Callback<number> = () => {
        let data: display.FoldStatus = display.getFoldStatus();
        this.foldStatus = data;
        if (this.foldStatus === 2) {
          this.FIX_VP_X = 81;
        } else if (this.foldStatus === 1) {
          this.FIX_VP_X = 110;
        }
      }
      display.on('change', callback);
    }
    let systemLanguage = i18n.System.getSystemLanguage();
    if (systemLanguage === 'en-Latn-US') {
      this.isEnglish = true;
    }
  }

  itemMove(index: number, newIndex: number): void {
    if (!this.isDraggable(newIndex)) {
      return;
    }
    let tmp = this.numbers.splice(index, 1);
    this.numbers.splice(newIndex, 0, tmp[0]);
  }

  down(index: number): void {
    if (!this.isDraggable(index + 5)) {
      return;
    }
    this.offsetY -= this.FIX_VP_Y;
    this.dragRefOffSetY += this.FIX_VP_Y;
    this.itemMove(index, index + 5)
  }

  up(index: number): void {
    if (this.curBp === 'md') {
      if (!this.isDraggable(index - 5)) {
        return;
      }
      this.offsetY += this.FIX_VP_Y;
      this.dragRefOffSetY -= this.FIX_VP_Y;
      this.itemMove(index, index - 5)
    } else {
      if (!this.isDraggable(index - 4)) {
        return;
      }
      this.offsetY += this.FIX_VP_Y;
      this.dragRefOffSetY -= this.FIX_VP_Y;
      this.itemMove(index, index - 4)
    }
  }

  left(index: number): void {
    if (!this.isDraggable(index - 1)) {
      return;
    }
    this.offsetX += this.FIX_VP_X;
    this.dragRefOffSetX -= this.FIX_VP_X;
    this.itemMove(index, index - 1)
  }

  right(index: number): void {
    if (!this.isDraggable(index + 1)) {
      return;
    }
    this.offsetX -= this.FIX_VP_X;
    this.dragRefOffSetX += this.FIX_VP_X;
    this.itemMove(index, index + 1)
  }

  lowerRight(index: number): void {
    if (!this.isDraggable(index + 3)) {
      return;
    }
    this.offsetX -= this.FIX_VP_X;
    this.dragRefOffSetX += this.FIX_VP_X;
    this.offsetY -= this.FIX_VP_Y;
    this.dragRefOffSetY += this.FIX_VP_Y;
    this.itemMove(index, index + 3);
  }

  upperRight(index: number): void {
    if (!this.isDraggable(index - 3)) {
      return;
    }
    this.offsetX -= this.FIX_VP_X;
    this.dragRefOffSetX += this.FIX_VP_X;
    this.offsetY += this.FIX_VP_Y;
    this.dragRefOffSetY -= this.FIX_VP_Y;
    this.itemMove(index, index - 3);
  }

  lowerLeft(index: number): void {
    if (!this.isDraggable(index + 3)) {
      return;
    }
    this.offsetX += this.FIX_VP_X;
    this.dragRefOffSetX -= this.FIX_VP_X;
    this.offsetY -= this.FIX_VP_Y;
    this.dragRefOffSetY += this.FIX_VP_Y;
    this.itemMove(index, index + 3);
  }

  upperLeft(index: number): void {
    if (!this.isDraggable(index - 3)) {
      return;
    }
    this.offsetX += this.FIX_VP_X;
    this.dragRefOffSetX -= this.FIX_VP_X;
    this.offsetY += this.FIX_VP_Y;
    this.dragRefOffSetY -= this.FIX_VP_Y;
    this.itemMove(index, index - 3);
  }

  isDraggable(index: number): boolean {
    return index >= 0;
  }
  build() {
    Column() {
      Column({ space: 5 }) {
        GridTitle({ title: '网格元素直接拖拽,不需长按' })
        // Row({ space: 12 }) {
        //   Button() {
        //     Text($r('app.string.equipment'))
        //       .fontColor(Color.White)
        //       .width('100%')
        //       .height('100%')
        //       .textAlign(TextAlign.Center)
        //   }
        //   .backgroundColor('#6e95ba')
        //   .height(36)
        //   .width(this.isEnglish ? 120 : 60)
        //
        //   Button() {
        //     Text($r('app.string.space'))
        //       .fontColor(Color.White)
        //       .width('100%')
        //       .height('100%')
        //       .textAlign(TextAlign.Center)
        //   }
        //   .backgroundColor('#0A59F7')
        //   .height(36)
        //   .width(this.isEnglish ? 80 : 60)
        //
        //   Button() {
        //     Text($r('app.string.mine'))
        //       .fontColor(Color.White)
        //       .width('100%')
        //       .height('100%')
        //       .textAlign(TextAlign.Center)
        //   }
        //   .backgroundColor('#6e95ba')
        //   .height(36)
        //   .width(this.isEnglish ? 80 : 60)
        // }
        // .width('100%')
        // .justifyContent(FlexAlign.Start)
        // .margin({ top: -4 })

        Grid() {
          ForEach(this.numbers, (item: number) => {
            GridItem() {
              Column() {
                Image($r(`app.media.space${item}`))
                  .width(44)
                  .height(44)
                  .draggable(false)
                Image($r('app.media.space_bottom'))
                  .width(16)
                  .height(16)
                  .draggable(false)
              }
              .width('100%')
              .height(73)
              .justifyContent(FlexAlign.Center)
              .borderRadius(10)
              .backgroundColor('#F1F3F5')
              .animation({ curve: Curve.Sharp, duration: 300 })
            }
            .scale({ x: this.scaleItem === item ? 1.05 : 1, y: this.scaleItem === item ? 1.05 : 1 })
            .zIndex(this.dragItem === item ? 1 : 0)
            .translate(this.dragItem === item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
            .gesture(
              PanGesture({ fingers: 1, direction: null, distance: 0 })
                .onActionStart(() => {
                  this.dragItem = item;
                  this.dragRefOffSetX = 0;
                  this.dragRefOffSetY = 0;
                })
                .onActionUpdate((event: GestureEvent) => {
                  this.offsetX = event.offsetX - this.dragRefOffSetX;
                  this.offsetY = event.offsetY - this.dragRefOffSetY;
                  animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                    let index = this.numbers.indexOf(this.dragItem);
                    if (this.curBp === 'md') {
                      if (this.offsetX >= this.FIX_VP_X / 2 && (this.offsetY <= 50 && this.offsetY >= -50) &&
                        ![4].includes(index)) {
                        this.right(index);
                      } else if (this.offsetX <= -this.FIX_VP_X / 2 && (this.offsetY <= 50 && this.offsetY >= -50)) {
                        this.left(index);
                      }
                    } else {
                      if (this.offsetY >= this.FIX_VP_Y / 2 && (this.offsetX <= 44 && this.offsetX >= -44) &&
                        ![1, 2, 3, 4].includes(index)) {
                        this.down(index);
                      } else if (this.offsetY <= -this.FIX_VP_Y / 2 && (this.offsetX <= 44 && this.offsetX >= -44)) {
                        this.up(index);
                      } else if (this.offsetX >= this.FIX_VP_X / 2 && (this.offsetY <= 50 && this.offsetY >= -50) &&
                        ![3, 4].includes(index)) {
                        this.right(index);
                      } else if (this.offsetX <= -this.FIX_VP_Y / 2 && (this.offsetY <= 50 && this.offsetY >= -50) &&
                        ![4].includes(index)) {
                        this.left(index);
                      } else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2) {
                        this.lowerRight(index);
                      } else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2) {
                        this.upperRight(index);
                      } else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2) {
                        this.lowerLeft(index);
                      } else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2) {
                        this.upperLeft(index);
                      }
                    }
                  })
                })
                .onActionEnd(() => {
                  animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                    this.dragItem = -1;
                  })
                  animateTo({ curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150 }, () => {
                    this.scaleItem = -1;
                  })
                })
            )
          }, (item: number) => item.toString())
        }
        .width('100%')
        .editMode(true)
        .scrollBar(BarState.Off)
        .columnsTemplate(this.curBp === 'md' ? '1fr 1fr 1fr 1fr 1fr' : '1fr 1fr 1fr 1fr')
        .columnsGap(12)
        .rowsGap(12)
        .margin({ top: 5 })
      }
      .width(this.curBp === 'md' ? '80%' : '100%')
      .height('100%')
      .padding({
        left: 16,
        right: 16,
        top: 12,
        bottom: 16
      })
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .padding({ bottom: 50 })
  }
}


@Entry
@Component
struct TestGridDemo2 {
  @State message: string = 'Grid网格元素拖拽交换开发实践';

  build() {
    Scroll() {
      Column({ space: 20 }) {
        //相同大小网格元素,长按拖拽
        //TestSameGridItemDrag()
        //不同大小网格元素,长按拖拽
        //TestDifferentGridItemDrag()
        //网格元素直接拖拽,不需长按
        TestGridDirectDragItem()
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .height('100%')
    .width('100%')

  }
}

七、网格元素长按后,显示抖动动画

7.1 场景描述

在设备列表页面时,如果想要移除设备,在选中设备并长按后,可对网格元素进行编辑。此时,设备图片会有抖动的效果。效果图如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.2 开发步骤

  1. 使用Grid布局及GridItem界面开发。
  2. 添加抖动动画。
  3. 定义stopJump()方法,执行后,能使网格元素停止抖动。
  4. GridItem绑定组合手势:长按、拖拽。并在手势的回调函数中设置显式动画。

7.3 完整示例代码

import { curves, display } from '@kit.ArkUI';
import { i18n } from '@kit.LocalizationKit';

@Component
struct GridTitle {
  title: string = ""

  build() {
    Column() {
      Text(this.title)
        .fontColor(Color.Black)
        .fontWeight(FontWeight.Bold)
        .fontSize(20)
        .margin({ left: 20 })
    }
  }
}

@Component
struct TestDragItemJitterAnimation {
  @StorageProp('currentBreakpoint') curBp: string = 'sm';
  @State isFoldAble: boolean = false;
  @State foldStatus: number = 2;
  @State numbers: number[] = [1, 2, 3, 4, 5];
  @State isShowDelete: boolean = false;
  @State isEdit: boolean = false;
  @State rotateZ: number = 0;
  @State dragItem: number = -1;
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State isEnglish: boolean = false;
  private dragRefOffSetX: number = 0;
  private dragRefOffSetY: number = 0;
  private FIX_VP_X: number = 92;
  private FIX_VP_Y: number = 84;
  private newItem = 100;

  aboutToAppear(): void {
    this.isFoldAble = display.isFoldable();
    let foldStatus: display.FoldStatus = display.getFoldStatus();
    if (foldStatus === 2) {
      this.FIX_VP_X = 81;
    } else if (foldStatus === 1) {
      this.FIX_VP_X = 110;
    }
    if (this.isFoldAble) {
      this.foldStatus = foldStatus;
      let callback: Callback<number> = () => {
        let data: display.FoldStatus = display.getFoldStatus();
        this.foldStatus = data;
        if (this.foldStatus === 2) {
          this.FIX_VP_X = 81;
        } else if (this.foldStatus === 1) {
          this.FIX_VP_X = 110;
        }
      }
      display.on('change', callback);
    }
    let systemLanguage = i18n.System.getSystemLanguage();
    if (systemLanguage === 'en-Latn-US') {
      this.isEnglish = true;
    }
  }

  itemMove(index: number, newIndex: number): void {
    if (!this.isDraggable(newIndex)) {
      return;
    }
    let tmp = this.numbers.splice(index, 1);
    this.numbers.splice(newIndex, 0, tmp[0]);
  }

  left(index: number): void {
    if (!this.isDraggable(index - 1)) {
      return;
    }
    this.offsetX += this.FIX_VP_X;
    this.dragRefOffSetX -= this.FIX_VP_X;
    this.itemMove(index, index - 1)
  }

  right(index: number): void {
    if (!this.isDraggable(index + 1)) {
      return;
    }
    this.offsetX -= this.FIX_VP_X;
    this.dragRefOffSetX += this.FIX_VP_X;
    this.itemMove(index, index + 1)
  }

  down(index: number): void {
    if (!this.isDraggable(index + 5)) {
      return;
    }
    this.offsetY -= this.FIX_VP_Y;
    this.dragRefOffSetY += this.FIX_VP_Y;
    this.itemMove(index, index + 5)
  }

  up(index: number): void {
    if (this.curBp === 'md') {
      if (!this.isDraggable(index - 5)) {
        return;
      }
      this.offsetY += this.FIX_VP_Y;
      this.dragRefOffSetY -= this.FIX_VP_Y;
      this.itemMove(index, index - 5)
    } else {
      if (!this.isDraggable(index - 4)) {
        return;
      }
      this.offsetY += this.FIX_VP_Y;
      this.dragRefOffSetY -= this.FIX_VP_Y;
      this.itemMove(index, index - 4)
    }
  }

  isDraggable(index: number): boolean {
    return index >= 0;
  }

  /**
   * 定义stopJump()方法,执行后,能使网格元素停止抖动。
   */
  private stopJump() {
    animateTo({
      delay: 0,
      tempo: 5,
      duration: 0,
      curve: Curve.Smooth,
      playMode: PlayMode.Normal,
      iterations: 1
    }, () => {
      this.rotateZ = 0;
    })
  }

  /**
   * 添加抖动动画
   * @param speed
   */
  private jumpWithSpeed(speed: number) {
    if (this.isEdit) {
      this.rotateZ = -1;
      animateTo({
        delay: 0,
        tempo: speed,
        duration: 1000,
        curve: Curve.Smooth,
        playMode: PlayMode.Normal,
        iterations: -1
      }, () => {
        this.rotateZ = 1;
      })
    } else {
      this.stopJump();
    }
  }

  changeIndex(index1: number, index2: number) {
    this.numbers.splice(index2, 0, this.numbers.splice(index1, 1)[0]);
  }

  build() {
    Column({ space: 10 }) {
      GridTitle({ title: '网格元素长按后,显示抖动动画' })
      Row({ space: 10 }) {
        Button('停止抖动')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.isEdit = false;
            this.stopJump();
          })

        Button('添加一个')
          .fontSize(20)
          .onClick(() => {
            if (this.isEdit) {
              promptAction.showToast({ message: "当前处于编辑模式,不支持添加" })
              return
            }
            this.newItem++;
            this.numbers.push(this.newItem);
            console.log("新的 newItem =  " + this.newItem + " , 添加后的 总数量 length = " + this.numbers.length);
            // this.stopJump();
          })

        Button('删除一个')
          .fontSize(20)
          .onClick(() => {
            let tempLength = this.numbers.length;
            console.log("删除一个前的 总数量:" + tempLength)
            if (tempLength > 3) {
              this.numbers.splice(2, 1) //从第二个位置开始删除一个元素
            } else {
              promptAction.showToast({ message: "已达到最少数据了的限制了" })
            }
          })
      }

      Grid() {
        ForEach(this.numbers, (item: number) => {
          GridItem() {
            Stack({ alignContent: Alignment.TopEnd }) {
              Column() {

                if (item > 5) {
                  Image($r(`app.media.space3`))
                    .width(44)
                    .height(44)
                    .draggable(false)
                } else {
                  Image($r(`app.media.space${item}`))
                    .width(44)
                    .height(44)
                    .draggable(false)
                }

                Image($r('app.media.space_bottom'))
                  .width(16)
                  .height(16)
                  .draggable(false)
              }
              .width('100%')
              .height(73)
              .justifyContent(FlexAlign.Center)
              .borderRadius(10)
              .backgroundColor('#F1F3F5')
              .animation({ curve: Curve.Sharp, duration: 300 })
              .onClick(() => {
                return;
              })

              if (this.isEdit) {
                Image($r('app.media.close'))
                  .width(20)
                  .height(20)
                  .objectFit(ImageFit.Contain)
                  .draggable(false)
                  .position({
                    x: this.isFoldAble && this.foldStatus === 2 ? 60 :
                      this.isFoldAble && this.foldStatus === 1 ? 86 : 70,
                    y: -8
                  })
                  .onClick(() => {
                    animateTo({ duration: 300 }, () => {
                      this.numbers = this.numbers.filter((element) => element !== item);
                    })
                  })
              }
            }
          }
          .rotate({
            z: this.rotateZ,
            angle: 1,
            centerX: '50%',
            centerY: '50%'
          })
          .width('100%')
          .zIndex(this.dragItem === item ? 1 : 0)
          .translate(this.dragItem === item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
          .gesture( // GridItem绑定组合手势:长按、拖拽。并在手势的回调函数中设置显式动画
            GestureGroup(GestureMode.Sequence,
              LongPressGesture({ repeat: true })
                .onAction(() => {
                  if (!this.isEdit) {
                    this.isEdit = true;
                    this.stopJump();
                    this.jumpWithSpeed(5);
                  }
                }),
              PanGesture({ fingers: 1, direction: null, distance: 0 })
                .onActionStart(() => {
                  this.dragItem = item;
                  this.dragRefOffSetX = 0;
                  this.dragRefOffSetY = 0;
                })
                .onActionUpdate((event: GestureEvent) => {
                  this.offsetX = event.offsetX - this.dragRefOffSetX;
                  this.offsetY = event.offsetY - this.dragRefOffSetY;
                  animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                    let index = this.numbers.indexOf(this.dragItem);
                    if (this.curBp === 'md') {
                      if (this.offsetX >= this.FIX_VP_X / 2 && (this.offsetY <= 50 && this.offsetY >= -50) &&
                        ![4].includes(index)) {
                        this.right(index);
                        this.stopJump();
                        this.jumpWithSpeed(5);
                      } else if (this.offsetX <= -this.FIX_VP_X / 2 &&
                        (this.offsetY <= 50 && this.offsetY >= -50)) {
                        this.left(index);
                        this.stopJump();
                        this.jumpWithSpeed(5);
                      }
                    } else {
                      if (this.offsetY >= this.FIX_VP_Y / 2 && (this.offsetX <= 44 && this.offsetX >= -44) &&
                        ![1, 2, 3, 4].includes(index)) {
                        this.down(index);
                        this.stopJump();
                        this.jumpWithSpeed(5);
                      } else if (this.offsetY <= -this.FIX_VP_Y / 2 &&
                        (this.offsetX <= 44 && this.offsetX >= -44)) {
                        this.up(index);
                        this.stopJump();
                        this.jumpWithSpeed(5);
                      } else if (this.offsetX >= this.FIX_VP_X / 2 && (this.offsetY <= 50 && this.offsetY >= -50) &&
                        ![3, 4].includes(index)) {
                        this.right(index);
                        this.stopJump();
                        this.jumpWithSpeed(5);
                      } else if (this.offsetX <= -this.FIX_VP_Y / 2 &&
                        (this.offsetY <= 50 && this.offsetY >= -50) &&
                        ![4].includes(index)) {
                        this.left(index);
                        this.stopJump();
                        this.jumpWithSpeed(5);
                      }
                    }
                  })
                })
                .onActionEnd(() => {
                  animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                    this.dragItem = -1;
                  })
                })
            )
          )
        }, (item: number) => item.toString())
      }
      .width('100%')
      .height('100%')
      .editMode(true)
      .clip(false)
      .scrollBar(BarState.Off)
      .columnsTemplate(this.curBp === 'md' ? '1fr 1fr 1fr 1fr 1fr' : '1fr 1fr 1fr 1fr')
      .columnsGap(12)
      .rowsGap(12)
      .margin({ top: 5 })
    }
    .width(this.curBp === 'md' ? '80%' : '100%')
    .height('100%')
    .padding({
      left: 16,
      right: 16,
      top: 12,
      bottom: 16
    })
  }
}


@Entry
@Component
struct TestGridDemo2 {
  @State message: string = 'Grid网格元素拖拽交换开发实践';

  build() {
    Scroll() {
      Column({ space: 20 }) {
        //相同大小网格元素,长按拖拽
        //TestSameGridItemDrag()
        //不同大小网格元素,长按拖拽
       // TestDifferentGridItemDrag()
        //网格元素直接拖拽,不需长按
       // TestGridDirectDragItem()
        //网格元素长按后,显示抖动动画
        TestDragItemJitterAnimation()
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .height('100%')
    .width('100%')

  }
}