HarmonyOS 鸿蒙Next中在使用拖拽onDragStart时,如果拖拽元素设置了rotate的角度angle会导致拖拽开始时发生位移,该怎么解决?

HarmonyOS 鸿蒙Next中在使用拖拽onDragStart时,如果拖拽元素设置了rotate的角度angle会导致拖拽开始时发生位移,该怎么解决?

@ComponentV2
export struct Drag  {
  @Local dataList: Array<number> = []
  @Local dragData: number | null = null

  aboutToAppear(): void {
    for (let index = 0; index < 10; index++) {
      this.dataList.push(index)
    }
  }

  build() {
    Column() {
      Text(this.dragData + '')
      Grid() {
        ForEach(this.dataList, (item: number)=> {
          GridItem() {
            Text(item+ '')
          }.height(80)
          .backgroundColor('#ffffff')
          .visibility(item===this.dragData?Visibility.Hidden: Visibility.Visible)
          .rotate({
            z: 1,
            angle: 2
          })
          .onDragStart((event?: DragEvent, extraParams?: string) => {
            this.dragData = item
          })
          .onDragEnd((event?: DragEvent, extraParams?: string)=> {
            this.dragData = null
          })
          .onDrop((event?: DragEvent, extraParams?: string) => {

          })
        })
      }.columnsTemplate('1fr 1fr')
      .columnsGap(12)
      .rowsGap(12)
      .padding(12)
      .backgroundColor(Color.Orange)
    }
  }
}

更多关于HarmonyOS 鸿蒙Next中在使用拖拽onDragStart时,如果拖拽元素设置了rotate的角度angle会导致拖拽开始时发生位移,该怎么解决?的实战教程也可以访问 https://www.itying.com/category-93-b0.html

7 回复

方案1:使用外层容器包裹(推荐)

将 rotate 应用到内层元素,而不是被拖拽的 GridItem 本身:

@ComponentV2
export struct Drag {
  @Local dataList: Array<number> = []
  @Local dragData: number | null = null

  aboutToAppear(): void {
    for (let index = 0; index < 10; index++) {
      this.dataList.push(index)
    }
  }

  build() {
    Column() {
      Text(this.dragData + '')
      Grid() {
        ForEach(this.dataList, (item: number) => {
          GridItem() {
            // 将 rotate 移到内层容器
            Column() {
              Text(item + '')
            }
            .width('100%')
            .height('100%')
            .justifyContent(FlexAlign.Center)
            .rotate({
              z: 1,
              angle: 2
            })
          }
          .height(80)
          .backgroundColor('#ffffff')
          .visibility(item === this.dragData ? Visibility.Hidden : Visibility.Visible)
          .onDragStart((event?: DragEvent, extraParams?: string) => {
            this.dragData = item
          })
          .onDragEnd((event?: DragEvent, extraParams?: string) => {
            this.dragData = null
          })
          .onDrop((event?: DragEvent, extraParams?: string) => {
          })
        })
      }
      .columnsTemplate('1fr 1fr')
      .columnsGap(12)
      .rowsGap(12)
      .padding(12)
      .backgroundColor(Color.Orange)
    }
  }
}

方案2:自定义拖拽预览图

使用 onDragStart 返回自定义的拖拽预览,避免变换影响:

@ComponentV2
export struct Drag {
  @Local dataList: Array<number> = []
  @Local dragData: number | null = null

  aboutToAppear(): void {
    for (let index = 0; index < 10; index++) {
      this.dataList.push(index)
    }
  }

  @Builder
  dragPreview(item: number) {
    Column() {
      Text(item + '')
    }
    .width(80)
    .height(80)
    .backgroundColor('#ffffff')
    .justifyContent(FlexAlign.Center)
    .rotate({
      z: 1,
      angle: 2
    })
  }

  build() {
    Column() {
      Text(this.dragData + '')
      Grid() {
        ForEach(this.dataList, (item: number) => {
          GridItem() {
            Text(item + '')
          }
          .height(80)
          .backgroundColor('#ffffff')
          .visibility(item === this.dragData ? Visibility.Hidden : Visibility.Visible)
          .rotate({
            z: 1,
            angle: 2
          })
          .onDragStart((event?: DragEvent, extraParams?: string) => {
            this.dragData = item
            // 返回自定义拖拽预览
            return this.dragPreview(item)
          })
          .onDragEnd((event?: DragEvent, extraParams?: string) => {
            this.dragData = null
          })
          .onDrop((event?: DragEvent, extraParams?: string) => {
          })
        })
      }
      .columnsTemplate('1fr 1fr')
      .columnsGap(12)
      .rowsGap(12)
      .padding(12)
      .backgroundColor(Color.Orange)
    }
  }
}

方案3:使用 dragPreview 属性(API 10+)

GridItem() {
  Text(item + '')
}
.height(80)
.backgroundColor('#ffffff')
.visibility(item === this.dragData ? Visibility.Hidden : Visibility.Visible)
.rotate({
  z: 1,
  angle: 2
})
.dragPreview(this.dragPreview(item)) // 设置拖拽预览
.onDragStart((event?: DragEvent, extraParams?: string) => {
  this.dragData = item
})

方案4:动态移除 rotate

在拖拽开始时移除旋转,拖拽结束后恢复:

@ComponentV2
export struct Drag {
  @Local dataList: Array<number> = []
  @Local dragData: number | null = null
  @Local isDragging: boolean = false
  // ...

  build() {
    Column() {
      Text(this.dragData + '')
      Grid() {
        ForEach(this.dataList, (item: number) => {
          GridItem() {
            Text(item + '')
          }
          .height(80)
          .backgroundColor('#ffffff')
          .visibility(item === this.dragData ? Visibility.Hidden : Visibility.Visible)
          .rotate({
            z: 1,
            // 拖拽时移除旋转
            angle: this.isDragging ? 0 : 2
          })
          .onDragStart((event?: DragEvent, extraParams?: string) => {
            this.isDragging = true
            this.dragData = item
          })
          .onDragEnd((event?: DragEvent, extraParams?: string) => {
            this.isDragging = false
            this.dragData = null
          })
          .onDrop((event?: DragEvent, extraParams?: string) => {
          })
        })
      }
      // ...
    }
  }
}

更多关于HarmonyOS 鸿蒙Next中在使用拖拽onDragStart时,如果拖拽元素设置了rotate的角度angle会导致拖拽开始时发生位移,该怎么解决?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


感谢,方案1是可以的,但是方案3自定义缩率图好像不行,

当拖拽元素设置rotate角度后,拖拽起始点坐标系计算会出现偏移。这是由于系统默认以元素原始位置作为拖拽起始点,而旋转后的视觉坐标与逻辑坐标存在差异。

使用DragEvent.setDragDisplayOffset方法手动校正坐标偏移:

.onDragStart((event?: DragEvent, extraParams?: string) => {
  this.dragData = item
  // 校正旋转导致的坐标偏移
  event?.setDragDisplayOffset({
    x: 0,  // 横向偏移量(根据实际旋转角度调整)
    y: 0   // 纵向偏移量(根据实际旋转角度调整)
  })
})

试了一下,onDragStart不支持这个,onDragStart是一个通用的拖拽事件,它在计算拖拽起始点时,是基于元素的布局位置,而没有考虑到元素被rotate后的视觉位置。可以用自定义拖拽手势,参考这个:

/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { curves, display } from '@kit.ArkUI';
import { i18n } from '@kit.LocalizationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Component
export struct JitterAnimation {
  @StorageProp('currentBreakpoint') curBp: string = 'sm';
  @State isFoldAble: boolean = false;
  @State foldStatus: number = 2;
  @State numbers: number[] = [1, 2, 3, 4, 5];
  @State isShowDelete: boolean = false;
  @State isEdit: boolean = false;
  @State rotateZ: number = 0;
  @State dragItem: number = -1;
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State isEnglish: boolean = false;
  private dragRefOffSetX: number = 0;
  private dragRefOffSetY: number = 0;
  private FIX_VP_X: number = 92;
  private FIX_VP_Y: number = 84;
  private leftArr: number[] = [];
  private rightArr: number[] = [];
  private downArr: number[] = [];

  aboutToAppear(): void {
    try {
      this.isFoldAble = display.isFoldable();
      let foldStatus: display.FoldStatus = display.getFoldStatus();
      if (foldStatus === 2) {
        this.FIX_VP_X = 81;
      } else if (foldStatus === 1) {
        this.FIX_VP_X = 110;
      }
      if (this.isFoldAble) {
        this.foldStatus = foldStatus;
        let callback: Callback<number> = () => {
          let data: display.FoldStatus = display.getFoldStatus();
          this.foldStatus = data;
          if (this.foldStatus === 2) {
            this.FIX_VP_X = 81;
          } else if (this.foldStatus === 1) {
            this.FIX_VP_X = 110;
          }
        }
        display.on('change', callback);
      }
      let systemLanguage = i18n.System.getSystemLanguage();
      if (systemLanguage === 'en-Latn-US') {
        this.isEnglish = true;
      }
      for (let i = 0; i < this.numbers.length; i++) {
        if (this.numbers[i] % 4 === 1) {
          this.leftArr.push(i);
        } else if (this.numbers[i] % 4 === 0) {
          this.rightArr.push(i);
        }
      }
      let n = this.numbers.length % 4;
      let m = Math.floor(this.numbers.length / 4);
      let downNum = (m - 1) * 4 + n;
      for (let j = 0; j < downNum; j++) {
        this.downArr.push(j);
      }
    } catch (error) {
      let err = error as BusinessError;
      hilog.error(0x0000, 'JitterAnimation',
        `getFoldStatus failed. code=${err.code}, message=${err.message}`);
    }
  }
  itemMove(index: number, newIndex: number): void {
    if (!this.isDraggable(newIndex)) {
      return;
    }
    let tmp = this.numbers.splice(index, 1);
    this.numbers.splice(newIndex, 0, tmp[0]);
  }

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

  down(index: number): void {
    if (!this.isDraggable(index + 5)) {
      return;
    }
    this.offsetY -= this.FIX_VP_Y;
    this.dragRefOffSetY += this.FIX_VP_Y;
    this.itemMove(index, index + 4);
  }

  up(index: number): void {
    if (this.curBp === 'md') {
      if (!this.isDraggable(index - 5)) {
        return;
      }
      this.offsetY += this.FIX_VP_Y;
      this.dragRefOffSetY -= this.FIX_VP_Y;
      this.itemMove(index, index - 5);
    } else {
      if (!this.isDraggable(index - 4)) {
        return;
      }
      this.offsetY += this.FIX_VP_Y;
      this.dragRefOffSetY -= this.FIX_VP_Y;
      this.itemMove(index, index - 4);
    }
  }

  isDraggable(index: number): boolean {
    return index >= 0;
  }

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

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

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

  @Builder
  NavDestinationTitle() {
    Column() {
      Text($r('app.string.details_page'))
        .fontSize(20)
        .lineHeight(42)
        .fontWeight(700)
        .width('100%')
        .padding({ left: 12 })
        .fontColor(Color.White)
        .opacity(0.9)
    }
    .width('100%')
  }

  build() {
    NavDestination() {
      Scroll() {
        Column() {
          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: 1,
                angle: 2,
                // 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 })
              // [StartExclude GridItem3_start]
              // [Start gesture3_start]
              .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;
                      })
                    })
                )
              )
              // [End gesture3_start]
              // [EndExclude GridItem3_start]
            }, (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 })
          // [End GridItem3_start]
        }
        .width(this.curBp === 'md' ? '80%' : '100%')
        .height('100%')
        .padding({
          left: 16,
          right: 16,
          top: 12,
          bottom: 16
        })
      }
      .scrollBar(BarState.Off)
      .padding({ bottom: 50 })
      .onClick(() => {
        this.isEdit = false;
        this.stopJump();
      })
    }
    .title(this.NavDestinationTitle())
    .backButtonIcon($r('app.media.back2'))
    .backgroundImage($r('app.media.backgroundImage'))
    .backgroundImageSize({ width: '100%', height: '100%' })
  }
}

在HarmonyOS鸿蒙Next中,拖拽元素设置rotate角度导致onDragStart位移的问题,可通过调整旋转中心点解决。使用transform-origin属性固定元素的旋转基准点,例如设置为"center"。同时确保元素布局属性不影响拖拽初始位置,避免使用可能引发重排的样式。检查元素在旋转状态下的实际坐标与拖拽事件获取坐标是否一致,必要时通过调整元素位置或拖拽事件参数进行校准。

在HarmonyOS Next中,当拖拽元素设置了rotate角度时,拖拽起始位置会发生偏移,这是因为旋转后的坐标系与原始坐标系不一致导致的。

解决方案是在onDragStart回调中通过event参数获取正确的拖拽位置信息:

.onDragStart((event?: DragEvent, extraParams?: string) => {
  this.dragData = item
  if (event) {
    // 获取拖拽起始位置
    const dragX = event.getX()
    const dragY = event.getY()
    // 根据实际需求调整拖拽位置
  }
})

或者使用transform属性替代rotate来实现旋转效果,因为transform不会影响拖拽坐标系:

.transform({
  rotate: { z: 1, angle: 2 }
})

这样可以避免旋转导致的拖拽位置偏移问题。

回到顶部