news 2026/4/17 1:46:19

鸿蒙6.0应用开发——Grid网格元素拖拽交换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙6.0应用开发——Grid网格元素拖拽交换

【高心星出品】

文章目录

  • Grid网格元素拖拽交换
    • 概述
    • 实现原理
      • 关键技术
      • 开发流程
    • 相同大小网格元素,长按拖拽
      • 场景描述
      • 开发步骤
    • 网格元素长按后,显示抖动动画
      • 场景描述
      • 开发步骤

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拖拽过程中的动画效果。

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

场景描述

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

示意效果图如下。

开发步骤

  1. 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)

    代码逻辑走读:

    1. 网格布局定义:使用Grid()定义一个网格布局容器。
    2. 循环生成网格项:通过ForEach循环遍历this.numbers数组,为每个数字创建一个GridItem
    3. 图片组件定义:在每个GridItem中,使用Image组件加载图片,图片的资源路径由$r(app.media.image${item})生成,其中item是当前循环的数字。
    4. 图片属性设置:设置图片的宽度为100%,高度根据this.curBp的值动态调整,不可拖动且动画持续时间为300毫秒。
    5. 网格属性设置:设置网格的宽度、滚动条状态、列模板、列间距、行间距和高度,这些属性值也根据this.curBp的值动态调整。
  2. 给Grid组件设置editMode为true,即Grid进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。设置supportAnimation

    为true,即Grid拖拽元素时支持动画。

    .editMode(true).supportAnimation(true)

    代码逻辑走读:

    1. 调用.editMode(true)方法,启用编辑模式,这意味着用户可以对当前界面进行编辑操作。
    2. 调用.supportAnimation(true)方法,启用动画效果支持,界面元素可以执行动画展示。
  3. 定义拖拽过程中的数组交换逻辑。

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

    代码逻辑走读:

    1. 定义changeIndex方法,接受两个参数index1index2
    2. 使用splice方法从this.numbers数组中移除位于index1位置的元素,并将该元素存储在变量tmp中。
    3. 使用splice方法在this.numbers数组的index2位置插入tmp数组中的第一个元素(即原index1位置的元素)。
  4. 给Grid组件绑定onItemDragStart和onItemDrop事件,在onItemDragStart回调中设置拖拽过程中显示的图片,并在onItemDrop

    中完成交换数组位置的逻辑。

    onItemDragStart回调在开始拖拽网格元素时触发,onItemDrop回调当在网格元素内停止拖拽时触发。

    .onItemDragStart((_,itemIndex:number)=>{this.imageNum=this.numbers[itemIndex];returnthis.pixelMapBuilder();}).onItemDrop((_,itemIndex:number,insertIndex:number,isSuccess:boolean)=>{if(!isSuccess||insertIndex>=this.numbers.length){return;}this.changeIndex(itemIndex,insertIndex);})

    代码逻辑走读:

    1. 拖拽开始事件处理
      • 当用户开始拖拽列表项时,.onItemDragStart回调函数被触发。
      • 通过itemIndex获取当前拖拽项对应的数字,并更新this.imageNum
      • 调用this.pixelMapBuilder()方法,构建或更新像素图。
    2. 拖拽结束事件处理
      • 当用户释放拖拽时,.onItemDrop回调函数被触发。
      • 检查isSuccess是否为falseinsertIndex是否超出this.numbers的长度。如果条件满足,则直接返回,不进行后续操作。
      • 如果拖拽成功且插入位置有效,调用this.changeIndex(itemIndex, insertIndex)方法,更新列表项的索引位置。

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

场景描述

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

示意效果图如下。

开发步骤

  1. 使用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})

    代码逻辑走读:

    1. Grid布局初始化
      • 使用Grid()组件初始化一个网格布局,设置其宽度和高度为100%,启用编辑模式,并禁用滚动条。
      • 根据当前的断点设置列模板,以适应不同的屏幕尺寸。
    2. 数据遍历与渲染
      • 使用ForEach循环遍历this.numbers数组,为每个数字创建一个GridItem
      • 每个GridItem包含一个Stack组件,用于堆叠内容。
    3. Stack组件构建
      • Stack中,首先创建一个Column组件,包含两个Image组件,分别用于显示图标和底部图标。
      • 设置Column的宽度、高度、对齐方式、背景颜色、边框圆角和动画效果。
    4. 编辑模式下的删除功能
      • 如果处于编辑模式(this.isEdit为真),在Stack中添加一个Image组件作为关闭按钮。
      • 设置关闭按钮的位置和点击事件,点击后通过animateTo动画和filter方法从this.numbers中移除当前项。
    5. 旋转和拖拽功能
      • 为每个GridItem设置旋转和拖拽属性,根据this.dragItemthis.offsetXthis.offsetY动态调整位置。
      • 设置GridItemzIndex,确保拖拽时的层级关系。
    6. 整体布局调整
      • 设置网格的列间距和行间距,以及顶部外边距,以完成整体布局的美化。
  2. 添加抖动动画。

    privatejumpWithSpeed(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();}}

    代码逻辑走读:

    1. 方法定义:定义了一个名为jumpWithSpeed的私有方法,该方法接受一个speed参数,类型为number
    2. 条件判断:
      • 如果this.isEdittrue,则执行动画逻辑。
      • 如果this.isEditfalse,则调用stopJump方法停止跳跃。
    3. 动画逻辑:
      • 设置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,表示动画结束。
  3. 定义stopJump()方法,执行后,能使网格元素停止抖动。

    private stopJump() { this.getUIContext().animateTo({ delay: 0, tempo: 5, duration: 0, curve: Curve.Smooth, playMode: PlayMode.Normal, iterations: 1 }, () => { this.rotateZ = 0; }) }

    代码逻辑走读:

    1. 定义一个私有方法stopJump,用于停止UI元素的动画。
    2. 调用getUIContext()获取当前UI上下文。
    3. 使用animateTo方法配置动画参数:
      • delay: 0:动画立即开始,没有延迟。
      • tempo: 5:设置动画的速度为5。
      • duration: 0:动画持续时间为0,表示动画立即完成。
      • curve: Curve.Smooth:使用平滑的曲线类型。
      • playMode: PlayMode.Normal:动画播放模式为正常模式。
      • iterations: 1:动画迭代次数为1,表示动画只执行一次。
    4. 在动画执行完成后,通过回调函数将rotateZ设置为0,重置元素的旋转角度,从而停止动画效果。
  4. 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);}elseif(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);}elseif(this.offsetY<=-this.FIX_VP_Y/2&&(this.offsetX<=44&&this.offsetX>=-44)){this.up(index);this.stopJump();this.jumpWithSpeed(5);}elseif(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);}elseif(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;})})))

    代码逻辑走读:

    1. 长按手势配置
      • 使用LongPressGesture配置长按手势,设置repeat: true表示长按可以重复触发。
      • 当长按动作发生时,检查this.isEdit是否为false,如果是,则设置this.isEdittrue,并调用this.jumpWithSpeed(5)方法。
    2. 拖拽手势配置
      • 使用PanGesture配置拖拽手势,限制为单指拖拽,不限制方向和距离。
      • 拖拽开始时,记录当前拖拽的元素this.dragItem,并初始化偏移量this.dragRefOffSetXthis.dragRefOffSetY
      • 拖拽过程中,根据手势事件的偏移量更新元素的offsetXoffsetY,并调用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.dragRefOffSetXthis.dragRefOffSetY
      • 拖拽过程中,根据手势事件的偏移量更新元素的offsetXoffsetY,并调用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
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 1:45:54

M2LOrder模型STM32嵌入式开发实战:从CubeMX到代码生成

M2LOrder模型STM32嵌入式开发实战&#xff1a;从CubeMX到代码生成 最近在做一个基于STM32的智能家居控制器项目&#xff0c;用CubeMX配置完时钟、GPIO、串口这些基础外设后&#xff0c;看着生成的工程框架&#xff0c;心里既踏实又有点发愁。踏实的是硬件初始化部分基本不用操…

作者头像 李华
网站建设 2026/4/16 22:14:15

新手必看!Qwen2.5-Coder-1.5B保姆级教程:3步开启代码生成之旅

新手必看&#xff01;Qwen2.5-Coder-1.5B保姆级教程&#xff1a;3步开启代码生成之旅 1. 认识你的AI编程助手 1.1 Qwen2.5-Coder-1.5B是什么&#xff1f; Qwen2.5-Coder-1.5B是一个专门为代码生成和编程辅助设计的轻量级AI模型。它来自阿里云的Qwen系列&#xff0c;经过5.5万…

作者头像 李华
网站建设 2026/4/14 6:41:28

三维点云处理-特征点描述 8.1 PFHFPFH

一、特征点的描述 1) PFH PFH方法核心特点&#xff1a; 实现旋转不变性捕捉局部表面几何变化基于点对法向量与相对位置计算参数复杂度缺陷&#xff1a;n个特征点需计算nk次运算敏感性问题&#xff1a;结果严重依赖法向量精度2) FPFH FPFH改进方案&#xff1a;简化计算&#xff…

作者头像 李华