介绍
在网格拖拽功能的设计中,直接进行元素交换或删除可能会导致生硬的交互体验,因此在交互过程中引入精细化的动画效果至关重要。鸿蒙系统的开发框架提供了丰富的 API 以支持这一需求,通过结合 Grid 组件、attributeModifier 和 animateTo 函数,可以实现高质量的拖拽与删除动画。onItemDragStart 用于配置拖拽中的动态视觉反馈,onItemDrop 则负责精确处理数据交换逻辑。整体流程中,动画的加入不仅优化了交互流畅度,还显著提升了用户体验的连贯性与视觉吸引力。
效果预览图
实现思路
onItemDragStart: 实现开始拖拽网格元素时触发,开始拖拽网格元素时触发。返回void表示不能拖拽。
onItemDrop: 绑定该事件的网格元素可作为拖拽释放目标,当在网格元素内停止拖拽时触发。
1. 声明数组列表,自定义属性ItemBean包含icon,name。在aboutToAppear函数中直接获取。
export interface ItemBean {
icon: ResourceStr;
name: ResourceStr;
}
// 网格数组数据
export const FunctionItemBean: ItemBean[] = [
{name:'分享',icon:$r("app.media.share")},
{name:'安全',icon:$r("app.media.secure")},
{name:'定位',icon:$r("app.media.orientation")},
{name:'邮箱',icon:$r("app.media.mail")},
{name:'保险',icon:$r("app.media.insurance")},
{name:'自定义',icon:$r("app.media.custom")},
{name:'资质',icon:$r("app.media.aptitude")},
{name:'通知',icon:$r("app.media.notification")},
{name:'更多',icon:$r("app.media.more")},
{name:'礼物',icon:$r("app.media.gift")},
{name:'删除',icon:$r("app.media.delete")},
]
2. 利用组件Grid进行渲染列表
@Component
export struct ExclusiveCustomPage {
@State functionList:ItemBean[] = []
aboutToAppear() {
this.functionList = FunctionItemBean;
}
// 网格每项内容
@Builder
IconWithNameView(item: ItemBean, index: number){
Column() {
Image(item?.icon)
.width(24).height(24)
.draggable(false)//是否支持拖拽
Text(item?.name).margin({top:8})
}.width(75).height(75)
.backgroundImage($r('app.media.custom_background'))
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundImageSize(ImageSize.Cover)
}
build() {
Column(){
Text('自定义排序')
Column(){
Text('拖动可调整入口位置的排序').fontColor('#999999')
Column({ space: 5 }) {
Grid() {
ForEach(this.functionList, (item: ItemBean, index: number) => {
GridItem() {
this.IconWithNameView(item,index)
}
})
}
.columnsTemplate('repeat(auto-fit, 75)')
.rowsGap(10)
.width('100%')
.height('100%')
}.width('100%').height('100%').margin({ top: 5 })
}
}.width('100%').height('100%').padding({left:20,right:20})
}
}
3. 结合动画animateTo,onTouch属性,长按网格时触发动画
@State ScaleXY:ScaleOptions = {x:1,y:1}
//是否按压
@State isPress:boolean = false
.onTouch((event: TouchEvent) => {
// 按下时
if(event.type === TouchType.Down){
this.CurrentIndex = index
this.isPress = true
setTimeout(()=>{
if(this.isPress){
animateTo({duration:100, curve:Curve.Linear,onFinish:()=>{
}},()=>{
this.ScaleXY = {x:1.2,y:1.2}
})
}
},200)
}
// 松开时
if (event.type === TouchType.Up) {
this.isPress = false
animateTo({duration:400, curve:Curve.EaseIn,onFinish:()=>{
}},()=>{
this.ScaleXY = {x:1,y:1}
})
}
})
4. 值得注意的是将supportAnimation设置为true,支持在拖拽时显示动画效果,并且设置editMode为true,Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem。添加onItemDragStart和onItemDrop实现网格交互。
.supportAnimation(true)
.editMode(true) //设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => {
this.CurrentIndex = itemIndex;
return this.pixelMapBuilder(itemIndex) //设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => { //绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
//改变数组结构
this.changeIndex(itemIndex, insertIndex)
})
//改变数组结构
changeIndex(itemIndex: number, insertIndex: number) {
let temp: ItemBean;
temp = this.functionList[itemIndex];
this.functionList[itemIndex] = this.functionList[insertIndex];
this.functionList[insertIndex] = temp;
}
5. 完整代码示例
FunctionItemBean.ets
* Copyright (c) 2024 LiuJiaWen
* @FileName: FunctionItemBean
* @Author: typeliu
* @Time: 2024/10/31 16:03
* @Description: 网格数据
*/
export interface ItemBean {
icon: ResourceStr;
name: ResourceStr;
}
export const FunctionItemBean: ItemBean[] = [
{name:'分享',icon:$r("app.media.share")},
{name:'安全',icon:$r("app.media.secure")},
{name:'定位',icon:$r("app.media.orientation")},
{name:'邮箱',icon:$r("app.media.mail")},
{name:'保险',icon:$r("app.media.insurance")},
{name:'自定义',icon:$r("app.media.custom")},
{name:'资质',icon:$r("app.media.aptitude")},
{name:'通知',icon:$r("app.media.notification")},
{name:'更多',icon:$r("app.media.more")},
{name:'礼物',icon:$r("app.media.gift")},
{name:'删除',icon:$r("app.media.delete")},
]
ExclusiveCustomPage.ets
* Copyright (c) 2024 LiuJiaWen
* @FileName: ExclusiveCustomPage
* @Author: typeliu
* @Time: 2024/10/31 14:23
* @Description: 专属自定义
*/
import { FunctionItemBean, ItemBean } from "../model/FunctionItemBean"
@Entry
@Component
export struct ExclusiveCustomPage {
@State functionList:ItemBean[] = []
// 拖动时放大倍数
@State ScaleXY:ScaleOptions = {x:1,y:1}
@State isPress:boolean = false//是否按压
// 当前移动的Item索引
@State CurrentIndex:number = -1
aboutToAppear() {
this.functionList = FunctionItemBean;
}
@Builder
pixelMapBuilder(itemIndex: number) { //拖拽过程样式
this.IconWithNameView(this.functionList[itemIndex],itemIndex)
}
@Builder
IconWithNameView(item: ItemBean, index: number){
Column() {
Image(item?.icon).width(24).height(24).draggable(false)//是否支持拖拽
Text(item?.name).margin({top:8})
}.width(75).height(75)
.scale(this.CurrentIndex === index?this.ScaleXY:{x:1,y:1})
.backgroundImage($r('app.media.custom_background'))
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundImageSize(ImageSize.Cover)
}
changeIndex(itemIndex: number, insertIndex: number) {
let temp: ItemBean;
temp = this.functionList[itemIndex];
this.functionList[itemIndex] = this.functionList[insertIndex];
this.functionList[insertIndex] = temp;
}
build() {
Column(){
Text('自定义排序')
Column(){
Text('拖动可调整入口位置的排序').fontColor('#999999')
Column({ space: 5 }) {
Grid() {
ForEach(this.functionList, (item: ItemBean, index: number) => {
GridItem() {
this.IconWithNameView(item,index)
}
.onTouch((event: TouchEvent) => {
// 按下时
if(event.type === TouchType.Down){
this.CurrentIndex = index
this.isPress = true
setTimeout(()=>{
if(this.isPress){
animateTo({duration:100, curve:Curve.Linear,onFinish:()=>{
}},()=>{
this.ScaleXY = {x:1.2,y:1.2}
})
}
},200)
}
// 松开时
if (event.type === TouchType.Up) {
this.isPress = false
animateTo({duration:400, curve:Curve.EaseIn,onFinish:()=>{
}},()=>{
this.ScaleXY = {x:1,y:1}
})
}
})
})
}
.columnsTemplate('repeat(auto-fit, 75)')
.rowsGap(10)
.width('100%')
.height('100%')
// 知识点:支持GridItem拖拽动画。
.supportAnimation(true)
.editMode(true) //设置Grid是否进入编辑模式,进入编辑模式可以拖拽Grid组件内部GridItem
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { //第一次拖拽此事件绑定的组件时,触发回调。
this.CurrentIndex = itemIndex;
return this.pixelMapBuilder(itemIndex) //设置拖拽过程中显示的图片。
})
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => { //绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。
this.changeIndex(itemIndex, insertIndex)
})
}.width('100%').height('100%').margin({ top: 5 })
}
}.width('100%').height('100%').padding({left:20,right:20})
}
}