HarmonyOS NEXT中的高级网格拖拽与动画设计

61 阅读3分钟

介绍

在网格拖拽功能的设计中,直接进行元素交换或删除可能会导致生硬的交互体验,因此在交互过程中引入精细化的动画效果至关重要。鸿蒙系统的开发框架提供了丰富的 API 以支持这一需求,通过结合 Grid 组件、attributeModifieranimateTo 函数,可以实现高质量的拖拽与删除动画。onItemDragStart 用于配置拖拽中的动态视觉反馈,onItemDrop 则负责精确处理数据交换逻辑。整体流程中,动画的加入不仅优化了交互流畅度,还显著提升了用户体验的连贯性与视觉吸引力。

效果预览图

微信图片_20241118163459.jpg

实现思路

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})
  }
}