HarmonyOS鸿蒙Next中grid拖动的时候,怎么让第一个item不响应?

HarmonyOS鸿蒙Next中grid拖动的时候,怎么让第一个item不响应? 类似于鸿蒙微信的这种效果,加号永远固定在第1个。


更多关于HarmonyOS鸿蒙Next中grid拖动的时候,怎么让第一个item不响应?的实战教程也可以访问 https://www.itying.com/category-93-b0.html

5 回复

针对在 HarmonyOS Grid 组件中实现第一个 item 固定不参与拖拽的需求(类似微信加号固定效果),以下是专业解决方案:

实现原理 通过拦截拖拽事件并添加条件判断,控制特定索引的 GridItem 不响应拖拽操作。核心是利用 onItemDragStart事件判断起始位置索引,当为第一个 item 时阻止拖拽行为。

解决方案 1. 拦截拖拽起始事件onItemDragStart回调中判断当前拖拽项的索引,若为 0(第一个 item)则返回 undefined阻止拖拽:

.onItemDragStart((event: ItemDragInfo, itemIndex: number) => {
  // 拦截第一个item的拖拽
  if (itemIndex === 0) {
    return undefined; // 禁止拖拽
  }
  this.currentIndex = itemIndex;
  return this.pixelMapBuilder(); // 正常拖拽显示效果
})

2. 防止其他项覆盖固定位置onItemDrop回调中阻止其他项目放置到第一个位置:

.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number) => {
  // 保护第一个位置不被覆盖
  if (insertIndex === 0) return;
  // 正常交换逻辑
  [this.data[itemIndex], this.data[insertIndex]] = [this.data[insertIndex], this.data[itemIndex]];
})

3. 完整配置示例

Grid() {
  ForEach(this.data, (item, index) => {
    GridItem() {
      Image(item.icon)
        .width(50)
        .height(50)
    }
    // 固定第一个item的样式
    .opacity(index === 0 ? 1 : 0.9)
  })
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.editMode(true)
.onItemDragStart((event, index) => {
  return index === 0 ? undefined : this.pixelMapBuilder();
})
.onItemDrop((event, srcIdx, dstIdx) => {
  if (dstIdx === 0) return;
  // 执行数据交换...
})

关键注意事项

  1. 数据源处理 确保数据源第一个元素为固定项(如加号图标),且不参与排序计算

  2. 视觉区分 通过透明度/边框等样式强化固定项的视觉标识:

    .borderWidth(index === 0 ? 2 : 0)
    .borderColor(Color.Blue)
    
  3. 组合逻辑 若需要实现跨 Grid 拖拽,参考通用拖拽接口 (onDragStart/onDrop),同样需在事件中判断 itemIndex === 0

提示:此方案适用于 HarmonyOS 5.0+ 的 ArkTS 开发,实际效果需配合数据绑定更新机制。固定项的逻辑应同时体现在数据层和视图层。

信息推荐

多个Grid组件间GridItem相互拖拽时出现异常如何解决

HarmonyOS Grid组件Item拖拽,如何加上长按震动?(API12+)

HarmonyOS 如何实现Item跨Grid拖拽?(API12+)

更多关于HarmonyOS鸿蒙Next中grid拖动的时候,怎么让第一个item不响应?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


你好,参考如何解决拖拽功能和长按功能的冲突问题,自行实现拖拽功能,不使用Grid自带的。

在isDraggable方法中配置指定的index是否可拖拽和排序。

// 通过元素的索引,控制对应元素是否能移动排序
isDraggable(index: number): boolean {
  console.info(`index: ${index}`);
  // return index > -1; // 恒成立,所有元素均可移动排序
  return index > 0 // 第一个不可拖拽和排序
}
import curves from '@ohos.curves';

@Entry
@Component
struct Index {
  // 元素数组
  @State numbers: number[] = [];
  // 多列
  private str: string = '';
  row: number = 4;
  // 元素数组中最后一个元素的索引
  @State lastIndex: number = 0;
  @State dragItem: number = -1;
  @State scaleItem: number = -1;
  item: number = -1;
  private dragRefOffsetX: number = 0;
  private dragRefOffsetY: number = 0;
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  private FIX_VP_X: number = 108;
  private FIX_VP_Y: number = 120;

  aboutToAppear() {
    for (let i = 1; i <= 20; i++) {
      this.numbers.push(i);
    }
    this.lastIndex = this.numbers.length - 1;
    // 多列
    for (let i = 0; i < this.row; i++) {
      this.str = this.str + '1fr ';
    }
  }

  itemMove(index: number, newIndex: number): void {
    console.info('index:' + index + ' newIndex:' + newIndex);
    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 + this.row)) {
      return;
    }
    this.offsetY -= this.FIX_VP_Y;
    this.dragRefOffsetY += this.FIX_VP_Y;
    // 多列
    this.itemMove(index, index + this.row);
  }

  // 向下滑(右下角为空)
  down2(index: number): void {
    if (!this.isDraggable(index + 3)) {
      return;
    }
    this.offsetY -= this.FIX_VP_Y;
    this.dragRefOffsetY += this.FIX_VP_Y;
    this.itemMove(index, index + 3);
  }

  // 向上滑
  up(index: number): void {
    if (!this.isDraggable(index - this.row)) {
      return;
    }
    this.offsetY += this.FIX_VP_Y;
    this.dragRefOffsetY -= this.FIX_VP_Y;
    this.itemMove(index, index - this.row);
  }

  // 向左滑
  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 + this.row + 1)) {
      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 + this.row + 1);
  }

  // 向右上滑
  upperRight(index: number): void {
    if (!this.isDraggable(index - (this.row - 1))) {
      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 - (this.row - 1));
  }

  // 向左下滑
  lowerLeft(index: number): void {
    if (!this.isDraggable(index + (this.row - 1))) {
      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 + (this.row - 1));
  }

  // 向左上滑
  upperLeft(index: number): void {
    if (!this.isDraggable(index - (this.row + 1))) {
      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 - (this.row + 1));
  }

  // 通过元素的索引,控制对应元素是否能移动排序
  isDraggable(index: number): boolean {
    console.info(`index: ${index}`);
    // return index > -1; // 恒成立,所有元素均可移动排序
    return index > 0 // 第一个不可拖拽和排序
  }

  build() {
    Column() {
      Grid() {
        ForEach(this.numbers, (item: number) => {
          GridItem() {
            Text(item + '')
              .fontSize(16)
              .width('100%')
              .textAlign(TextAlign.Center)
              .height(100)
              .borderRadius(10)
              .backgroundColor(0xFFFFFF)
              .shadow(this.scaleItem == item ? {
                radius: 70,
                color: '#15000000',
                offsetX: 0,
                offsetY: 0
              } :
                {
                  radius: 0,
                  color: '#15000000',
                  offsetX: 0,
                  offsetY: 0
                })
              .animation({ curve: Curve.Sharp, duration: 300 });
          }
          // 添加震动
          .onTouch(() => {
          })
          .onAreaChange((oldVal, newVal) => {
            // 多列
            this.FIX_VP_X = Math.round(newVal.width as number);
            this.FIX_VP_Y = Math.round(newVal.height as number);
            console.info(`oldVal:${JSON.stringify(oldVal)}`);
          })
          // 指定固定GridItem不响应事件
          .hitTestBehavior(this.isDraggable(this.numbers.indexOf(item)) ? HitTestMode.Default : HitTestMode.None)
          .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 })
          .padding(10)
          .gesture(
            GestureGroup(GestureMode.Sequence,
              LongPressGesture({ repeat: true, duration: 50 })
                .onAction((event?: GestureEvent) => {
                  console.info(`event: ${event}`);
                  this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
                    this.scaleItem = item;
                  });
                })
                .onActionEnd(() => {
                  this.getUIContext().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.offsetY = event.offsetY - this.dragRefOffsetY;
                  this.offsetX = event.offsetX - this.dragRefOffsetX;
                  console.info(`移动过程中event.offsetY:${event.offsetY}`,
                    ` this.dragRefOffsetY: ${this.dragRefOffsetY}`, `this.offsetY: ${this.offsetY}`);
                  console.info(`移动过程中event.offsetX:${event.offsetX}`,
                    ` this.dragRefOffsetX: ${this.dragRefOffsetX}`, `this.offsetX: ${this.offsetX}`);
                  this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                    let index = this.numbers.indexOf(this.dragItem);
                    console.info('index= ', index);
                    if (this.offsetY >= this.FIX_VP_Y / 2 &&
                      (this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
                      && (index + this.row <= this.lastIndex)) {
                      // 向下滑
                      this.down(index);
                    } else if (this.offsetY <= -this.FIX_VP_Y / 2 &&
                      (this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
                      && index - this.row >= 0) {
                      // 向上滑
                      this.up(index);
                    } else if (this.offsetX >= this.FIX_VP_X / 2 &&
                      (this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
                      && !(((index - (this.row - 1)) % this.row == 0) || index == this.lastIndex)) {
                      // 向右滑
                      this.right(index);
                    } else if (this.offsetX <= -this.FIX_VP_X / 2 &&
                      (this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
                      && !(index % this.row == 0)) {
                      // 向左滑
                      this.left(index);
                    } else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
                      && ((index + this.row + 1 <= this.lastIndex && !((index - (this.row - 1)) % this.row == 0)) ||
                        !((index - (this.row - 1)) % this.row == 0))) {
                      // 向右下滑
                      this.lowerRight(index);
                    } else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
                      && !((index - this.row < 0) || ((index - (this.row - 1)) % this.row == 0))) {
                      // 向右上滑
                      this.upperRight(index);
                    } else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
                      && (!(index % this.row == 0) && (index + (this.row - 1) <= this.lastIndex))) {
                      // 向左下滑
                      this.lowerLeft(index);
                    } else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
                      && !((index <= this.row - 1) || (index % this.row == 0))) {
                      // 向左上滑
                      this.upperLeft(index);
                    } else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
                      && (index == this.lastIndex)) {
                      // 向右下滑(右下角为空)
                      this.down2(index);
                    }
                  });
                })
                .onActionEnd(() => {
                  this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                    this.dragItem = -1;
                  });
                  this.getUIContext().animateTo({
                    curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
                  }, () => {
                    this.scaleItem = -1;
                  });
                })
            )
              .onCancel(() => {
                this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                  this.dragItem = -1;
                });
                this.getUIContext().animateTo({
                  curve: curves.interpolatingSpring(14, 1, 170, 17)
                }, () => {
                  this.scaleItem = -1;
                });
              })
          );
        }, (item: number) => item.toString());
      }
      .width('90%')
      .editMode(true)
      .scrollBar(BarState.Off)
      // 多列
      .columnsTemplate(this.str);
    }
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    .width('100%')
    .height('100%')
    .backgroundColor('#f1f3f5')
    .padding({ top: 5 });
  }
}

非常简单:

onItemDragStart

开始拖拽网格元素时触发。返回void表示不能拖拽。

手指长按GridItem时触发该事件。

由于拖拽检测也需要长按,且事件处理机制优先触发子组件事件,GridItem上绑定LongPressGesture时无法触发拖拽。如有长按和拖拽同时使用的需求可以使用通用拖拽事件。

拖拽浮起的网格元素可在应用窗口内移动,若需限制移动范围,可通过自定义手势实现,具体参考示例16(实现GridItem自定义拖拽)。

不支持拖动到Grid边缘时自动滚动,可使用通用拖拽实现,具体参考示例17(通过拖拽事件实现griditem拖拽)。

元服务API: 从API version 11开始,该接口支持在元服务中使用。

系统能力: SystemCapability.ArkUI.ArkUI.Full

参数:

参数名 类型 必填 说明
event ItemDragInfo 拖拽点的信息。
itemIndex number 被拖拽网格元素索引值。

Demo如下:

previewableImage

// GridDataSource.ets
export class GridDataSource implements IDataSource {
  private list: string[] = [];
  private listeners: DataChangeListener[] = [];
  constructor(list: string[]) {
    this.list = list;
  }
  totalCount(): number {
    return this.list.length;
  }
  getData(index: number): string {
    return this.list[index];
  }
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }
  // 通知控制器数据位置变化
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
  // 交换元素位置
  public swapItem(from: number, to: number): void {
    let temp: string = this.list[from];
    this.list[from] = this.list[to];
    this.list[to] = temp;
    this.notifyDataMove(from, to);
  }
}
import { GridDataSource } from './GridDataSource';
@Entry
@Component
struct GridExample {
  numbers: GridDataSource = new GridDataSource([]);
  scroller: Scroller = new Scroller();
  @State text: string = 'drag';
  @Builder pixelMapBuilder() { //拖拽过程样式
    Column() {
      Text(this.text)
        .fontSize(16)
        .backgroundColor(0xF9CF93)
        .width(80)
        .height(80)
        .textAlign(TextAlign.Center)
    }
  }
  aboutToAppear() {
    let list: string[] = [];
    for (let i = 1; i <= 15; i++) {
      list.push(i + '');
    }
    this.numbers = new GridDataSource(list);
  }
  changeIndex(index1: number, index2: number) { //交换数组位置
    this.numbers.swapItem(index1, index2);
  }
  build() {
    Column({ space: 5 }) {
      Grid(this.scroller) {
        LazyForEach(this.numbers, (day: string) => {
          GridItem() {
            Text(day)
              .fontSize(16)
              .backgroundColor(0xF9CF93)
              .width(80)
              .height(80)
              .textAlign(TextAlign.Center)
          }
        }, (day: string) => day)
      }
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .width('90%')
      .backgroundColor(0xFAEEE0)
      .height(300)
      .editMode(true) //设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
      .supportAnimation(true) // 设置支持动画
      .onItemDragStart((event: ItemDragInfo, itemIndex: number) => { //第一次拖拽此事件绑定的组件时,触发回调。
        this.text = this.numbers.getData(itemIndex);
        return this.pixelMapBuilder(); //设置拖拽过程中显示的图片。
      })
      .onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => { //绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
        // isSuccess=false时,说明drop的位置在grid外部;insertIndex > length时,说明有新增元素的事件发生
        if (!isSuccess || insertIndex >= this.numbers.totalCount()) {
          return;
        }
        console.info('itemIndex:' + itemIndex + ', insertIndex:' + insertIndex); //itemIndex拖拽起始位置,insertIndex拖拽插入位置
        this.changeIndex(itemIndex, insertIndex);
      })
    }.width('100%').margin({ top: 5 })
  }
}

在HarmonyOS Next中,可通过设置GridItemonDragStart事件来控制拖拽响应。若需禁止第一个item拖拽,可在Grid组件的onDragStart回调中判断索引,当索引为0时调用event.stopPropagation()阻止拖拽事件传递。示例代码片段如下:

Grid() {
  ForEach(this.items, (item, index) => {
    GridItem() {
      // 内容
    }
    .onDragStart((event: DragEvent) => {
      if (index === 0) {
        event.stopPropagation()
      }
    })
  })
}

此方法直接中断事件传递,使第一个item无法触发拖拽。

在HarmonyOS Next中,要实现Grid组件拖动时第一个Item(如固定加号)不参与排序,核心思路是将第一个Item与后续可排序的Item进行逻辑分离。以下是两种主流且简洁的实现方案:

方案一:使用Grid嵌套Grid或混合布局(推荐)

这是最直接模拟微信效果的方法。将固定的第一个Item和可拖动的Grid区域拆分为两个独立部分。

  1. 布局结构:使用RowColumn作为根容器。

    • 第一个子组件是固定的“加号”按钮(使用ButtonImage组件)。
    • 第二个子组件是一个Grid组件,用于承载所有可拖拽排序的Item
  2. 关键点

    • 可拖拽的Grid组件设置editMode(true)以启用拖拽排序。
    • 固定的“加号”Item完全位于此可拖拽Grid之外,因此不会受其任何拖拽事件影响。
    • 数据源myDataList仅包含需要排序的项目,不包含“加号”。

示例代码片段:

Row({ space: 10 }) {
  // 1. 固定的加号Item,独立于可拖拽Grid
  Button('+')
    .width(80)
    .height(80)

  // 2. 可拖拽排序的Grid区域
  Grid() {
    ForEach(this.myDataList, (item: MyDataType) => {
      GridItem() {
        // 你的Item内容
        Text(item.name)
      }
    })
  }
  .editMode(true) // 启用编辑(拖拽)模式
  .onItemMove((from: number, to: number) => {
    // 处理数据交换逻辑
    this.arrayMove(this.myDataList, from, to);
  })
  .columnsTemplate('1fr 1fr 1fr') // 根据需求调整
  .width('100%')
}

方案二:在单个Grid内通过条件判断禁用拖拽

如果希望所有Item视觉上仍在同一个Grid容器内,可以通过editMode的回调动态控制。

  1. 原理:利用GridonEditModeChange或结合editMode属性,在拖拽开始时判断是否为第一个Item。
  2. 实现:为每个GridItem添加长按事件,在长按触发拖拽前进行拦截。或者,在onItemMoveStart事件中判断起始索引,如果from === 0,则通过返回false或阻止默认行为来取消拖拽(需注意API的支持程度,HarmonyOS Next的Grid组件可能未直接提供此事件的拦截)。
  3. 注意:此方法通常需要更精细的手势控制,可能涉及自定义拖拽逻辑,实现起来比方案一更复杂,且可能无法完全达到系统原生拖拽动画的流畅度。

总结

对于大多数类似“固定首项”的场景,方案一(布局分离)是更清晰、稳定且易于维护的选择。它直接避免了第一个Item进入拖拽逻辑,与微信的实现思路一致。将固定元素与可排序列表分离,是UI组件设计的常见最佳实践。

在具体实现时,只需注意可拖拽Grid的数据源不要包含固定项,并处理好布局样式即可。

回到顶部