十念 发表于 2024-10-27 08:06:03

HarmonyOS NEXT实战(5.0 )ArkUI开发>拖拽框架事件

 鸿蒙NEXT开发实战往期必看文章:

HarmonyOS NEXT应用开发案例实践总联合(持续更新......)
HarmonyOS NEXT应用开发性能优化实践总结(持续更新......)
一分钟了解”纯血版!鸿蒙HarmonyOS Next应用开发!
“非常详细的” 鸿蒙HarmonyOS Next应用开发学习蹊径!(从零底子入门到醒目)
概述

拖拽框架提供了一种通过鼠标或手势触屏的方式通报数据,即从一个组件位置拖出数据,并拖入到另一个组件位置上进行响应,拖出一方提供数据,拖入一方接收和处理数据。该操作可以让用户方便地移动、复制或删除指定内容。


[*]拖拽操作:在某个能够响应拖出的组件上长按并滑动触发的拖拽举动,当用户释放时,拖拽操作竣事;
[*]拖拽背景(背板):用户所拖动数据的形象化表示,开发者可通过onDragStart的CustomerBuilder或DragItemInfo设置,也可以通过dragPreview通用属性设置;
[*]拖拽内容:拖动的数据,利用UDMF统一API UnifiedData 进行封装;
[*]拖出对象:触发拖拽操作并提供数据的组件;
[*]拖入目标:可接收并处理拖动数据的组件;
[*]拖拽点:鼠标或手指等与屏幕的接触位置,是否进入组件范围的判断是以接触点是否进入范围进行判断。
拖拽流程

​手势拖拽

​对于手势长按触发拖拽的场景,发起拖拽前框架侧会对当前组件是否可拖拽进行校验,针对默承认拖出的组件(Search、TextInput、TextArea、RichEditor、Text、Image、FormComponent、Hyperlink)需要判断是否设置了draggable属性为true(若体系使能分层参数,则draggable默以为true),其他组件需要额外判断是否设置了onDragStart回调函数,在满意上述可拖拽条件下,长按大于等于500ms可触发拖拽,长按800ms开始做预览图的浮起动效。
手势拖拽(手指/手写笔)触发拖拽流程:
https://i-blog.csdnimg.cn/direct/fe4d5409a29f4aca8eff4dbd339319e9.png
​鼠标拖拽

鼠标拖拽属于即拖即走,只要鼠标左键在可拖拽的组件上按下并移动大于1vp就可触发拖拽。
当前支持应用内和跨应用拖拽,提供了多个回调事件供开发者感知拖拽状态并干预体系默认拖拽举动,详细如下:
回调事件说明onDragStart支持拖出的组件产生拖出动作时触发。
该回调可以感知拖拽举动的发起,开发者可通过在 onDragStart 方法中设置拖拽所通报的数据以及自界说拖拽背板图。保举开发者利用pixelmap的方式返回背板图,不保举利用customBuilder的方式,会有额外的性能开销。onDragEnter当拖拽活动的拖拽点进入组件范围内时触发,只有该组件监听了onDrop事件时,此回调才会被触发。onDragMove拖拽点在组件范围内移动时触发;只有该组件监听了onDrop事件时,此回调才会被触发。
在此过程中可通过DragEvent中的setResult方法影响体系部分场景下的外观
1. 设置DragResult.DROP_ENABLED;
2. 设置DragResult.DROP_DISABLED。onDragLeave拖拽点离开组件范围时触发;只有该组件监听了onDrop事件时,此回调才会被触发。
针对以下两种情况默认不会发送onDragLeave事件:
1. 父组件移动到子组件;
2. 目标组件与当前组件布局有重叠;
API version 12开始可通过UIContext中的setDragEventStrictReportingEnabled方法严酷触发onDragLeave事件。onDrop当用户在组件范围内释放时触发,需在此回调中通过DragEvent中的setResult方法设置拖拽效果,否则在拖出方组件的onDragEnd方法中通过getRresult方法只能拿到默认的处理效果DragResult.DRAG_FAILED。
该回调也是开发者干预体系默认拖入处理举动的地方,体系会优先实行开发者的onDrop回调,通过在回调中实行setResult方法来告知体系该如那里理所拖拽的数据;
1. 设置 DragResult.DRAG_SUCCESSFUL,数据完全由开发者自己处理,体系不进行处理;
2. 设置DragResult.DRAG_FAILED,数据不再由体系继续处理;
3. 设置DragResult.DRAG_CANCELED,体系也不需要进行数据处理;
4. 设置DragResult.DROP_ENABLED或DragResult.DROP_DISABLED会被忽略,同设置DragResult.DRAG_FAILED;onDragEnd当用户释放拖拽时,拖拽活动竣事,发起拖出动作的组件会触发该回调。onPreDrag绑定此事件的组件,当触发拖拽发起前的不同阶段时,触发回调。
开发者可以利用该方法监听PreDragStatus中的枚举在发起拖拽前的不同阶段准备数据。
1. ACTION_DETECTING_STATUS:拖拽手势启动阶段。(按下50ms时触发);
2. READY_TO_TRIGGER_DRAG_ACTION:拖拽准备完成,可发起拖拽阶段。(按下500ms时触发);
3. PREVIEW_LIFT_STARTED:拖拽浮起动效发起阶段。(按下800ms时触发);
4. PREVIEW_LIFT_FINISHED:拖拽浮起动效竣事阶段。(浮起动效完全竣事时触发);
5. PREVIEW_LANDING_STARTED:拖拽落回动效发起阶段。(落回动效发起时触发);
6. PREVIEW_LANDING_FINISHED:拖拽落回动效竣事阶段。(落回动效竣事时触发);
7. ACTION_CANCELED_BEFORE_DRAG:拖拽浮升降位动效中断。(已满意READY_TO_TRIGGER_DRAG_ACTION状态后,未达到动效阶段,手指抬手时触发)。 更多用法参考拖拽事件。
拖拽背板图

拖拽移动过程中显示的拖拽背板图,并非是组件本身,其是用户拖动数据的表示,开发者可以将其设置为任意可显示的图像。此中onDragStart 回调返回的customBuilder或pixelmap可以设置拖拽移动过程中的背板图,浮起图默认利用组件本身的截图;dragpreview属性设置的customBuilder或pixelmap可以设置浮起和拖拽过程的背板图;如果开发者没有配置背板图,则体系会默认取组件本身的截图作为浮起及拖拽过程中的背板图。
拖拽背板图当前支持设置透明度、圆角、阴影和含糊,详细用法见:拖拽控制
约束:


[*]对于容器组件,如果内部内容通过position,offset等本领使得绘制地区超出了容器组件范围,则体系截图无法截取到范围之外的内容,此种情况下,如果一定要浮起及拖拽背板能够包罗范围之外的内容,则可考虑通过扩大容器范围或自界说方式进行;
[*]不管是利用自界说builder或是体系默认截图方式,截图都暂时无法应用scale、rotate等图形变换效果。
开发步骤

通用拖拽适配

如下以Image组件为例,先容组件拖拽开发的根本步骤,以及开发中需要留意的事项。

[*]组件使能拖拽


[*] 设置draggable属性为true,并设置onDragStart回调,回调中可以通过UDMF设置拖拽的数据,并返回自界说拖拽背板图;
import UDC from '@ohos.data.unifiedDataChannel';
import UTD from '@ohos.data.uniformTypeDescriptor';

Image($r('app.media.app_icon'))
    .width(100)
    .height(100)
    .draggable(true)
    .onDragStart((event) => {
      let data: UDC.Image = new UDC.Image();
      data.imageUri = 'common/pic/img.png';
      let unifiedData = new UDC.UnifiedData(data);
      event.setData(unifiedData);

      let dragItemInfo: DragItemInfo = {
      pixelMap: this.pixmap,
      extraInfo: "this is extraInfo",
      };
      // onDragStart回调函数中返回自定义拖拽背板图
      return dragItemInfo;
    })
<strong>ts</strong>
[*] 手势场景触发拖拽抵赖底层绑定的长按手势,若开发者在被拖拽组件上也绑定长按手势,则会与底层的长按手势发生竞争,导致拖拽失败。可以用并行手势办理办理此类问题,如下:
.parallelGesture(LongPressGesture().onAction(() => {
   promptAction.showToast({ duration: 100, message: 'Long press gesture trigger' });
}))
<strong>ts</strong>

[*]自界说拖拽背板图


[*] 自界说拖拽背板图的pixmap可以通过设置onPreDrag函数在长按50ms时触发的回调中条件准备;
.onPreDrag((status: PreDragStatus) => {
    if (preDragStatus == PreDragStatus.ACTION_DETECTING_STATUS) {
      this.getComponentSnapshot();
    }
})
<strong>ts</strong>
[*] 详细pixmap的生成可以调用componentSnapshot函数;
@Builder
pixelMapBuilder() {
    Column() {
      Image($r('app.media.startIcon'))
      .width(120)
      .height(120)
      .backgroundColor(Color.Yellow)
    }
}
private getComponentSnapshot(): void {
componentSnapshot.createFromBuilder(()=>{this.pixelMapBuilder()},
(error: Error, pixmap: image.PixelMap) => {
      if(error){
      console.log("error: " + JSON.stringify(error))
      return;
      }
      this.pixmap = pixmap;
})
}
<strong>ts</strong>

[*] 如果开发者要严酷触发onDragLeave事件,可以通过setDragEventStrictReportingEnabled方法设置。
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
import { UIContext } from '@ohos.arkui.UIContext';

export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
      return;
      }
      windowStage.getMainWindow((err, data) => {
      if (err.code) {
          return;
      }
      let windowClass: window.Window = data;
      let uiContext: UIContext = windowClass.getUIContext();
      uiContext.getDragController().setDragEventStrictReportingEnabled(true);
      });
    });
}
}
<strong>ts</strong>
[*] 拖拽过程显示角标样式


[*] 可以通过设置allowDrop界说接收的数据类型影响角标显示,当拖拽数据是界说允许落入的数据类型时,显示COPY角标;当拖拽数据不在界说允许落入的数据类型范围时,显示FORBIDDEN角标;未设置allowDrop时,显示MOVE角标。如下代码表示只接收UnifiedData中界说的HYPERLINK和PLAIN_TEXT类型的数据,其他数据类型将禁止落入;
.allowDrop()
<strong>ts</strong>
[*] 此外在实现onDrop回调的情况下还可以通过在onDragMove中设置DragResult为DROP_ENABLED,并设置DragBehavior为COPY或MOVE控制角标显示。如下代码将移动时的角标强制设置为MOVE;
.onDragMove((event) => {
    event.setResult(DragResult.DROP_ENABLED);
    event.dragBehavior = DragBehavior.MOVE;
})
<strong>ts</strong>

[*]拖拽数据的接收


[*] 需要设置onDrop回调,并在回调中处理拖拽数据,显示设置拖拽效果
.onDrop((dragEvent?: DragEvent) => {
    // 获取拖拽数据
    this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {
    let records: Array<UDC.UnifiedRecord> = event.getData().getRecords();
    let rect: Rectangle = event.getPreviewRect();
    this.imageWidth = Number(rect.width);
    this.imageHeight = Number(rect.height);
    this.targetImage = (records as UDC.Image).imageUri;
    this.imgState = Visibility.None;
    // 显式设置result为successful,则将该值传递给拖出方的onDragEnd
    event.setResult(DragResult.DRAG_SUCCESSFUL);
})
<strong>ts</strong>
[*] 数据的通报是通过UDMF实现的,在数据较大时可能存在时延,因此在初次获取数据失败时建议加1500ms的延长重试机制:

[*] 拖拽发起方可以通过设置onDragEnd回调感知拖拽效果
import promptAction from '@ohos.promptAction';

.onDragEnd((event) => {
    // onDragEnd里取到的result值在接收方onDrop设置
if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
    promptAction.showToast({ duration: 100, message: 'Drag Success' });
} else if (event.getResult() === DragResult.DRAG_FAILED) {
    promptAction.showToast({ duration: 100, message: 'Drag failed' });
}
})
<strong>ts</strong>
多选拖拽适配

API version 12开始Grid组件和List组件中的GridItem和ListItem组件支持多选拖拽,当前只支持onDragStart的方式。如下以Grid为例,先容多选拖拽的根本步骤,以及开发中的留意事项。

[*]组件多选拖拽使能


[*] 创建GridItem子组件并绑定onDragStart函数。同时设置GridItem组件的状态是可选中的;
Grid() {
ForEach(this.numbers, (idx: number) => {
    GridItem() {
      Column()
      .backgroundColor(this.colors)
      .width(50)
      .height(50)
      .opacity(1.0)
      .id('grid'+idx)
    }
    .onDragStart(()=>{})
    .selectable(true)
}, (idx: string) => idx)
}
<strong>ts</strong>
[*] 多选拖拽功能默以为关闭状态,利用多选拖拽需要在dragPreviewOptions接口中的DragInteractionOptions参数中设置isMultiSelectionEnabled为true,表示当前组件是否多选。DragInteractionOptions也有支持组件浮起前默认效果的参数defaultAnimationBeforeLifting,设置该参数为true后组件在浮起前会有一个默认缩小动效。
.dragPreviewOptions({isMultiSelectionEnabled:true,defaultAnimationBeforeLifting:true})
<strong>ts</strong>
[*] 为了保证选中状态,需要设置GridItem子组件selected状态为true。例如,可以通过onClick的调用去设置特定的组件为选中的状态。
.selected(this.isSelectedGrid)
.onClick(()=>{
    this.isSelectedGrid = !this.isSelectedGrid
})
<strong>ts</strong>

[*]多选拖拽性能优化


[*] 多选拖拽情况下,多选会有聚拢的动画效果,聚拢效果触发时,会给当前屏幕内显示的选中组件截图,当选中组件过多,会导致很高的性能开销。多选拖拽支持从dragPreview中获取截图来作为聚拢动效的截图,以节流性能。
.dragPreview({
    pixelMap:this.pixmap
})
<strong>ts</strong>
[*] 截图的获取可以在选中组件时通过调用componentSnapshot中的get方法获取。如下通过获取组件对应id的方法进行截图。
@State previewData: DragItemInfo[] = []
@State isSelectedGrid: boolean[] = []
.onClick(()=>{
    this.isSelectedGrid = !this.isSelectedGrid
    if (this.isSelectedGrid) {
      let gridItemName = 'grid' + idx
      componentSnapshot.get(gridItemName, (error: Error, pixmap: image.PixelMap)=>{
            this.pixmap = pixmap
            this.previewData = {
                pixelMap:this.pixmap
            }
      })
    }
})
<strong>ts</strong>

[*] 多选显示效果
通过stateStyles可以设置选中态和非选中态的显示效果,方便区分。
@Styles
normalStyles(): void{
.opacity(1.0)
}

@Styles
selectStyles(): void{
.opacity(0.4)
}

.stateStyles({
normal : this.normalStyles,
selected: this.selectStyles
})
<strong>ts</strong>
[*] 数量角标适配
多选拖拽的数量角标当前需要应用利用dragPreviewOptions中的numberBadge参数设置,开发者需要根据当前选中的节点数量来设置数量角标。
@State numberBadge: number = 0;

.onClick(()=>{
    this.isSelectedGrid = !this.isSelectedGrid
    if (this.isSelectedGrid) {
      this.numberBadge++;
    } else {
      this.numberBadge--;
}
})
// 多选场景右上角数量角标需要应用设置numberBadge参数
.dragPreviewOptions({numberBadge: this.numberBadge})
<strong>ts</strong>
完备示例

通用拖拽适配案例

import UDC from '@ohos.data.unifiedDataChannel';
import UTD from '@ohos.data.uniformTypeDescriptor';
import promptAction from '@ohos.promptAction';
import { BusinessError } from '@ohos.base';
import image from '@ohos.multimedia.image'
import componentSnapshot from '@ohos.arkui.componentSnapshot'

@Entry
@Component
struct Index {
@State targetImage: string = '';
@State imageWidth: number = 100;
@State imageHeight: number = 100;
@State imgState: Visibility = Visibility.Visible;
@State pixmap: image.PixelMap|undefined = undefined

@Builder
pixelMapBuilder() {
    Column() {
      Image($r('app.media.startIcon'))
      .width(120)
      .height(120)
      .backgroundColor(Color.Yellow)
    }
}

getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) {
    try {
      let data: UnifiedData = event.getData();
      if (!data) {
      return false;
      }
      let records: Array<UDC.UnifiedRecord> = data.getRecords();
      if (!records || records.length <= 0) {
      return false;
      }
      callback(event);
      return true;
    } catch (e) {
      console.log("getData failed, code: " + (e as BusinessError).code + ", message: " + (e as BusinessError).message);
      return false;
    }
}
// 获取UDMF数据,首次获取失败后添加1500ms延迟重试机制
getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) {
    if (this.getDataFromUdmfRetry(event, callback)) {
      return;
    }
    setTimeout(() => {
      this.getDataFromUdmfRetry(event, callback);
    }, 1500);
}
// 调用componentSnapshot中的createFromBuilder接口截取自定义builder的截图
private getComponentSnapshot(): void {
    componentSnapshot.createFromBuilder(()=>{this.pixelMapBuilder()},
      (error: Error, pixmap: image.PixelMap) => {
      if(error){
          console.log("error: " + JSON.stringify(error))
          return;
      }
      this.pixmap = pixmap;
      })
}
// 长按50ms时提前准备自定义截图的pixmap
private PreDragChange(preDragStatus: PreDragStatus): void {
    if (preDragStatus == PreDragStatus.ACTION_DETECTING_STATUS) {
      this.getComponentSnapshot();
    }
}

build() {
    Row() {
      Column() {
      Text('start Drag')
          .fontSize(18)
          .width('100%')
          .height(40)
          .margin(10)
          .backgroundColor('#008888')
      Row() {
          Image($r('app.media.app_icon'))
            .width(100)
            .height(100)
            .draggable(true)
            .margin({ left: 15 })
            .visibility(this.imgState)
            // 绑定平行手势,可同时触发应用自定义长按手势
            .parallelGesture(LongPressGesture().onAction(() => {
            promptAction.showToast({ duration: 100, message: 'Long press gesture trigger' });
            }))
            .onDragStart((event) => {
            let data: UDC.Image = new UDC.Image();
            data.imageUri = 'common/pic/img.png';
            let unifiedData = new UDC.UnifiedData(data);
            event.setData(unifiedData);

            let dragItemInfo: DragItemInfo = {
                pixelMap: this.pixmap,
                extraInfo: "this is extraInfo",
            };
            return dragItemInfo;
            })
            // 提前准备拖拽自定义背板图
            .onPreDrag((status: PreDragStatus) => {
            this.PreDragChange(status);
            })
            .onDragEnd((event) => {
            // onDragEnd里取到的result值在接收方onDrop设置
            if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
                promptAction.showToast({ duration: 100, message: 'Drag Success' });
            } else if (event.getResult() === DragResult.DRAG_FAILED) {
                promptAction.showToast({ duration: 100, message: 'Drag failed' });
            }
            })
      }

      Text('Drag Target Area')
          .fontSize(20)
          .width('100%')
          .height(40)
          .margin(10)
          .backgroundColor('#008888')
      Row() {
          Image(this.targetImage)
            .width(this.imageWidth)
            .height(this.imageHeight)
            .draggable(true)
            .margin({ left: 15 })
            .border({ color: Color.Black, width: 1 })
            // 控制角标显示类型为MOVE,即不显示角标
            .onDragMove((event) => {
            event.setResult(DragResult.DROP_ENABLED)
            event.dragBehavior = DragBehavior.MOVE
            })
            .allowDrop()
            .onDrop((dragEvent?: DragEvent) => {
            // 获取拖拽数据
            this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {
                let records: Array<UDC.UnifiedRecord> = event.getData().getRecords();
                let rect: Rectangle = event.getPreviewRect();
                this.imageWidth = Number(rect.width);
                this.imageHeight = Number(rect.height);
                this.targetImage = (records as UDC.Image).imageUri;
                this.imgState = Visibility.None;
                // 显式设置result为successful,则将该值传递给拖出方的onDragEnd
                event.setResult(DragResult.DRAG_SUCCESSFUL);
            })
            })
      }
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
}
}

<strong>ts</strong> 多选拖拽适配案例

import componentSnapshot from "@ohos.arkui.componentSnapshot";
import image from '@ohos.multimedia.image'

@Entry
@Component
struct GridEts {
@State pixmap: image.PixelMap|undefined = undefined
@State numbers: number[] = []
@State isSelectedGrid: boolean[] = []
@State previewData: DragItemInfo[] = []
@State colors: Color[] =
@State numberBadge: number = 0;

@Styles
normalStyles(): void{
    .opacity(1.0)
}

@Styles
selectStyles(): void{
    .opacity(0.4)
}

onPageShow(): void {
    let i: number = 0
    for(i=0;i<100;i++){
      this.numbers.push(i)
      this.isSelectedGrid.push(false)
      this.previewData.push({})
    }
}

@Builder
RandomBuilder(idx: number) {
    Column()
      .backgroundColor(this.colors)
      .width(50)
      .height(50)
      .opacity(1.0)
}

build() {
    Column({ space: 5 }) {
      Grid() {
      ForEach(this.numbers, (idx: number) => {
          GridItem() {
            Column()
            .backgroundColor(this.colors)
            .width(50)
            .height(50)
            .opacity(1.0)
            .id('grid'+idx)
          }
          .dragPreview(this.previewData)
          .selectable(true)
          .selected(this.isSelectedGrid)
          // 设置多选显示效果
          .stateStyles({
            normal : this.normalStyles,
            selected: this.selectStyles
          })
          .onClick(()=>{
            this.isSelectedGrid = !this.isSelectedGrid
            if (this.isSelectedGrid) {
            this.numberBadge++;
            let gridItemName = 'grid' + idx
            // 选中状态下提前调用componentSnapshot中的get接口获取pixmap
            componentSnapshot.get(gridItemName, (error: Error, pixmap: image.PixelMap)=>{
                this.pixmap = pixmap
                this.previewData = {
                  pixelMap:this.pixmap
                }
            })
            } else {
            this.numberBadge--;
            }
          })
          // 使能多选拖拽,右上角数量角标需要应用设置numberBadge参数
          .dragPreviewOptions({numberBadge: this.numberBadge},{isMultiSelectionEnabled:true,defaultAnimationBeforeLifting:true})
          .onDragStart(()=>{
          })
      }, (idx: string) => idx)
      }
      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
      .columnsGap(5)
      .rowsGap(10)
      .backgroundColor(0xFAEEE0)
    }.width('100%').margin({ top: 5 })
}
}


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: HarmonyOS NEXT实战(5.0 )ArkUI开发>拖拽框架事件