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

实现原理
关键技术
Grid网格元素拖拽交换功能实现是通过Grid容器组件、组合手势、动画属性animateTo结合来实现的。
- Grid组件可以构建网格元素布局。
- 组合手势可以实现元素拖拽交换的效果。
- 显式动画可以给元素拖拽交换的过程中,添加动画效果。
Grid组件当前支持GridItem拖拽动画,通过给Grid容器组件设置supportAnimation为true,即可开启动画效果。但仅支持在滚动模式下(设置rowsTemplate、columnsTemplate其中一个)支持动画。且仅在大小规则的Grid中支持拖拽动画,跨行或跨列场景不支持。因此,在跨行或跨列场景下,需要通过自定义Grid布局、自定义手势和显式动画来实现拖拽交换的效果。
开发流程
在需要拖拽交换的场景中:
- 实现Grid布局,启动editMode编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。
- 给网格元素GridItem绑定相关手势,实现可拖拽操作。
- 使用显式动画animateTo,实现GridItem拖拽过程中的动画效果。
相同大小网格元素,长按拖拽
场景描述
在编辑九宫格等多图的场景中,长按图片(网格元素)可以拖拽交换排序,拖拽图片的过程中,旁边的图片也会即时移动,以产生新的宫格排布。
示意效果图如下。

开发步骤
-
Grid布局及相同大小的GridItem界面开发。其中,scrollBar可设置滚动条状态,值为BarState.Off时,表示不显示滚动条;
columnsTemplate可设置当前网格布局列的数量、固定列宽或最小列宽值;
columnsGap可设置列与列的间距;
rowsGap可设置行与行的间距。
Grid() { ForEach(this.numbers, (item: number) => { GridItem() { Image($r(`app.media.image${item}`)) .width('100%') .height(this.curBp === 'md' ? 131 : 105) .draggable(false) .animation({ curve: Curve.Sharp, duration: 300 }) } }, (item: number) => item.toString()) } .width(this.curBp === 'md' ? '66%' : '100%') .scrollBar(BarState.Off) .columnsTemplate('1fr 1fr 1fr') .columnsGap(this.curBp === 'md' ? 6 : 4) .rowsGap(this.curBp === 'md' ? 6 : 4) .height(this.curBp === 'md' ? 406 : 323)代码逻辑走读:
- 网格布局定义:使用
Grid()定义一个网格布局容器。 - 循环生成网格项:通过
ForEach循环遍历this.numbers数组,为每个数字创建一个GridItem。 - 图片组件定义:在每个
GridItem中,使用Image组件加载图片,图片的资源路径由$r(app.media.image${item})生成,其中item是当前循环的数字。 - 图片属性设置:设置图片的宽度为100%,高度根据
this.curBp的值动态调整,不可拖动且动画持续时间为300毫秒。 - 网格属性设置:设置网格的宽度、滚动条状态、列模板、列间距、行间距和高度,这些属性值也根据
this.curBp的值动态调整。
- 网格布局定义:使用
-
给Grid组件设置editMode为true,即Grid进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。设置supportAnimation
为true,即Grid拖拽元素时支持动画。
.editMode(true) .supportAnimation(true)代码逻辑走读:
- 调用
.editMode(true)方法,启用编辑模式,这意味着用户可以对当前界面进行编辑操作。 - 调用
.supportAnimation(true)方法,启用动画效果支持,界面元素可以执行动画展示。
- 调用
-
定义拖拽过程中的数组交换逻辑。
changeIndex(index1: number, index2: number) { let tmp = this.numbers.splice(index1, 1); this.numbers.splice(index2, 0, tmp[0]) }代码逻辑走读:
- 定义
changeIndex方法,接受两个参数index1和index2。 - 使用
splice方法从this.numbers数组中移除位于index1位置的元素,并将该元素存储在变量tmp中。 - 使用
splice方法在this.numbers数组的index2位置插入tmp数组中的第一个元素(即原index1位置的元素)。
- 定义
-
给Grid组件绑定onItemDragStart和onItemDrop事件,在onItemDragStart回调中设置拖拽过程中显示的图片,并在onItemDrop
中完成交换数组位置的逻辑。
onItemDragStart回调在开始拖拽网格元素时触发,onItemDrop回调当在网格元素内停止拖拽时触发。
.onItemDragStart((_, itemIndex: number) => { this.imageNum = this.numbers[itemIndex]; return this.pixelMapBuilder(); }) .onItemDrop((_, itemIndex: number, insertIndex: number, isSuccess: boolean) => { if (!isSuccess || insertIndex >= this.numbers.length) { return; } this.changeIndex(itemIndex, insertIndex); })代码逻辑走读:
- 拖拽开始事件处理:
- 当用户开始拖拽列表项时,
.onItemDragStart回调函数被触发。 - 通过
itemIndex获取当前拖拽项对应的数字,并更新this.imageNum。 - 调用
this.pixelMapBuilder()方法,构建或更新像素图。
- 当用户开始拖拽列表项时,
- 拖拽结束事件处理:
- 当用户释放拖拽时,
.onItemDrop回调函数被触发。 - 检查
isSuccess是否为false或insertIndex是否超出this.numbers的长度。如果条件满足,则直接返回,不进行后续操作。 - 如果拖拽成功且插入位置有效,调用
this.changeIndex(itemIndex, insertIndex)方法,更新列表项的索引位置。
- 当用户释放拖拽时,
- 拖拽开始事件处理:
网格元素长按后,显示抖动动画
场景描述
在设备列表页面时,如果想要移除设备,在选中设备并长按后,可对网格元素进行编辑。此时,设备图片会有抖动的效果。
示意效果图如下。

开发步骤
-
使用Grid布局及GridItem界面开发。
Grid() { ForEach(this.numbers, (item: number) => { GridItem() { Stack({ alignContent: Alignment.TopEnd }) { 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 }) .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(() => { this.getUIContext().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 }) // ... }, (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 })代码逻辑走读:
- Grid布局初始化:
- 使用
Grid()组件初始化一个网格布局,设置其宽度和高度为100%,启用编辑模式,并禁用滚动条。 - 根据当前的断点设置列模板,以适应不同的屏幕尺寸。
- 使用
- 数据遍历与渲染:
- 使用
ForEach循环遍历this.numbers数组,为每个数字创建一个GridItem。 - 每个
GridItem包含一个Stack组件,用于堆叠内容。
- 使用
- Stack组件构建:
- 在
Stack中,首先创建一个Column组件,包含两个Image组件,分别用于显示图标和底部图标。 - 设置
Column的宽度、高度、对齐方式、背景颜色、边框圆角和动画效果。
- 在
- 编辑模式下的删除功能:
- 如果处于编辑模式(
this.isEdit为真),在Stack中添加一个Image组件作为关闭按钮。 - 设置关闭按钮的位置和点击事件,点击后通过
animateTo动画和filter方法从this.numbers中移除当前项。
- 如果处于编辑模式(
- 旋转和拖拽功能:
- 为每个
GridItem设置旋转和拖拽属性,根据this.dragItem和this.offsetX、this.offsetY动态调整位置。 - 设置
GridItem的zIndex,确保拖拽时的层级关系。
- 为每个
- 整体布局调整:
- 设置网格的列间距和行间距,以及顶部外边距,以完成整体布局的美化。
- Grid布局初始化:
-
添加抖动动画。
private jumpWithSpeed(speed: number) { if (this.isEdit) { this.rotateZ = -1; this.getUIContext().animateTo({ delay: 0, tempo: speed, duration: 1000, curve: Curve.Smooth, playMode: PlayMode.Normal, iterations: -1 }, () => { this.rotateZ = 1; }) } else { this.stopJump(); } }代码逻辑走读:
- 方法定义:定义了一个名为
jumpWithSpeed的私有方法,该方法接受一个speed参数,类型为number。 - 条件判断:
- 如果
this.isEdit为true,则执行动画逻辑。 - 如果
this.isEdit为false,则调用stopJump方法停止跳跃。
- 如果
- 动画逻辑:
- 设置
this.rotateZ为-1,表示动画开始。 - 调用getUIContext().animateTo方法,配置动画参数:
delay: 0:动画立即开始。tempo: speed:使用传入的speed作为动画速度。duration: 1000:动画持续时间为 1000 毫秒(1秒)。curve: Curve.Smooth:使用平滑的动画曲线。playMode: PlayMode.Normal:动画以正常模式播放。iterations: -1:动画无限循环。
- 在动画完成后,将
this.rotateZ设置为1,表示动画结束。
- 设置
- 方法定义:定义了一个名为
-
定义stopJump()方法,执行后,能使网格元素停止抖动。
private stopJump() { this.getUIContext().animateTo({ delay: 0, tempo: 5, duration: 0, curve: Curve.Smooth, playMode: PlayMode.Normal, iterations: 1 }, () => { this.rotateZ = 0; }) }代码逻辑走读:
- 定义一个私有方法
stopJump,用于停止UI元素的动画。 - 调用
getUIContext()获取当前UI上下文。 - 使用animateTo方法配置动画参数:
delay: 0:动画立即开始,没有延迟。tempo: 5:设置动画的速度为5。duration: 0:动画持续时间为0,表示动画立即完成。curve: Curve.Smooth:使用平滑的曲线类型。playMode: PlayMode.Normal:动画播放模式为正常模式。iterations: 1:动画迭代次数为1,表示动画只执行一次。
- 在动画执行完成后,通过回调函数将
rotateZ设置为0,重置元素的旋转角度,从而停止动画效果。
- 定义一个私有方法
-
GridItem绑定组合手势:长按、拖拽。并在手势的回调函数中设置显式动画。
.gesture( GestureGroup(GestureMode.Sequence, LongPressGesture({ repeat: true }) .onAction(() => { if (!this.isEdit) { this.isEdit = true; 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; this.getUIContext().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) && [...this.downArr].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) && ![...this.rightArr].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) && ![...this.leftArr].includes(index)) { this.left(index); this.stopJump(); this.jumpWithSpeed(5); } } }) }) .onActionEnd(() => { this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => { this.dragItem = -1; }) }) ) )代码逻辑走读:
- 长按手势配置:
- 使用
LongPressGesture配置长按手势,设置repeat: true表示长按可以重复触发。 - 当长按动作发生时,检查
this.isEdit是否为false,如果是,则设置this.isEdit为true,并调用this.jumpWithSpeed(5)方法。
- 使用
- 拖拽手势配置:
- 使用
PanGesture配置拖拽手势,限制为单指拖拽,不限制方向和距离。 - 拖拽开始时,记录当前拖拽的元素
this.dragItem,并初始化偏移量this.dragRefOffSetX和this.dragRefOffSetY。 - 拖拽过程中,根据手势事件的偏移量更新元素的
offsetX和offsetY,并调用this.getUIContext().animateTo方法执行动画效果。 - 在动画回调中,根据当前的布局尺寸(
this.curBp)和偏移量判断拖拽方向,执行相应的移动操作(如this.right(index)、this.left(index)、this.up(index)、this.down(index)),并在特定条件下调用this.stopJump()和this.jumpWithSpeed(5)。
拖拽,不限制方向和距离。 - 拖拽开始时,记录当前拖拽的元素
this.dragItem,并初始化偏移量this.dragRefOffSetX和this.dragRefOffSetY。 - 拖拽过程中,根据手势事件的偏移量更新元素的
offsetX和offsetY,并调用this.getUIContext().animateTo方法执行动画效果。 - 在动画回调中,根据当前的布局尺寸(
this.curBp)和偏移量判断拖拽方向,执行相应的移动操作(如this.right(index)、this.left(index)、this.up(index)、this.down(index)),并在特定条件下调用this.stopJump()和this.jumpWithSpeed(5)。 - 拖拽结束时,重置拖拽状态,调用
this.getUIContext().animateTo方法执行动画效果,并将this.dragItem重置为-1。
- 使用
- 长按手势配置:
更多推荐



所有评论(0)