HarmonyOS鸿蒙Next中grid拖动的时候,怎么让第一个item不响应?
HarmonyOS鸿蒙Next中grid拖动的时候,怎么让第一个item不响应?
类似于鸿蒙微信的这种效果,加号永远固定在第1个。
更多关于HarmonyOS鸿蒙Next中grid拖动的时候,怎么让第一个item不响应?的实战教程也可以访问 https://www.itying.com/category-93-b0.html
针对在 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;
// 执行数据交换...
})
关键注意事项
-
数据源处理 确保数据源第一个元素为固定项(如加号图标),且不参与排序计算
-
视觉区分 通过透明度/边框等样式强化固定项的视觉标识:
.borderWidth(index === 0 ? 2 : 0) .borderColor(Color.Blue) -
组合逻辑 若需要实现跨 Grid 拖拽,参考通用拖拽接口 (
onDragStart/onDrop),同样需在事件中判断itemIndex === 0
提示:此方案适用于 HarmonyOS 5.0+ 的 ArkTS 开发,实际效果需配合数据绑定更新机制。固定项的逻辑应同时体现在数据层和视图层。
信息推荐
多个Grid组件间GridItem相互拖拽时出现异常如何解决
更多关于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如下:

// 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中,可通过设置GridItem的onDragStart事件来控制拖拽响应。若需禁止第一个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区域拆分为两个独立部分。
-
布局结构:使用
Row或Column作为根容器。- 第一个子组件是固定的“加号”按钮(使用
Button或Image组件)。 - 第二个子组件是一个
Grid组件,用于承载所有可拖拽排序的Item。
- 第一个子组件是固定的“加号”按钮(使用
-
关键点:
- 可拖拽的
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的回调动态控制。
- 原理:利用
Grid的onEditModeChange或结合editMode属性,在拖拽开始时判断是否为第一个Item。 - 实现:为每个
GridItem添加长按事件,在长按触发拖拽前进行拦截。或者,在onItemMoveStart事件中判断起始索引,如果from === 0,则通过返回false或阻止默认行为来取消拖拽(需注意API的支持程度,HarmonyOS Next的Grid组件可能未直接提供此事件的拦截)。 - 注意:此方法通常需要更精细的手势控制,可能涉及自定义拖拽逻辑,实现起来比方案一更复杂,且可能无法完全达到系统原生拖拽动画的流畅度。
总结
对于大多数类似“固定首项”的场景,方案一(布局分离)是更清晰、稳定且易于维护的选择。它直接避免了第一个Item进入拖拽逻辑,与微信的实现思路一致。将固定元素与可排序列表分离,是UI组件设计的常见最佳实践。
在具体实现时,只需注意可拖拽Grid的数据源不要包含固定项,并处理好布局样式即可。

