三方接入拖拽开发指导
1. API使用指导
应用接入拖拽涉及ArkUI提供的OnDragStart、OnDrop等接口的使用,若应用需要自定义实现拖拽的触发逻辑,则应掌握DragController系接口的。为自定义处理各类拖拽数据,还应掌握 UDMF 相关接口的使用。针对一些资源在网络的场景,如在线Image,还应借助Http相关能力实现在线资源的下载落盘处理。
1.1 ArkUI Drag
ArkUI框架对以下组件实现了默认的拖拽能力,支持对数据的拖出或拖入响应,开发者只需要将这些组件的draggable属性设置为true,即可使用默认拖拽能力。其中,Text、TextInput、TextArea、Hyperlink、Image和RichEditor组件的draggable属性默认为true。默认支持拖出能力的组件(可从组件上拖出数据):Search、TextInput、TextArea、RichEditor、Text、Image、Hyperlink默认支持拖入能力的组件(目标组件可响应拖入数据):Search、TextInput、TextArea、Video开发者也可以通过实现通用拖拽事件来自定义拖拽响应。其他组件需要开发者将draggable属性设置为true,并在onDragStart等接口中实现数据传输相关内容,才能正确处理拖拽。
1.1.1 OnDragStart系列接口(框架能力)
draggable
设置当前组件是否可拖拽,系统默认可拖出的组件有Search、TextInput、TextArea、RichEditor、Text、Image、Hyperlink。
allowDrop
标识当前组件上支持拖拽落入的数据类型,该数据类型是UniformDataType定义标识的。
OnPreDrag
绑定该事件的组件,在触发拖拽前的不同阶段时,触发回调,可在该接口中实现一些在拖拽发起前需要完成的动作,如产生拖拽缩略图等。
DragItemInfo
该接口用于描述拖拽过程中缩略图的相关信息,可设置PixelMap或实现CustomBuilder来实现自定义拖拽背板图。若已设置过PixelMap,则CustomerBuilder将会被忽略, 使用CustomBuilder性能较差于使用PixelMap。
DragItemInfo中extraInfo标识一些系统侧需要处理的额外数据,三方应用一般不涉及。
DragEvent
拖拽事件,在拖拽起拖时可设置对应拖拽数据,在拖拽过程中可获取当前拖拽事件的坐标信息、拖拽数据类型、拖拽数据等信息。
OnDragStart
第一次拖拽此事件绑定的组件时,长按时间 >= 500ms,然后手指移动距离 >= 10vp,则触发回调。针对默认支持拖出能力的组件,如果开发者设置了onDragStart,优先执行开发者的onDragStart,并根据执行情况决定是否使用系统默认的拖出能力,具体为:如果开发者返回了自定义背板图,则不再使用系统默认的拖拽背板图;如果开发者设置了拖拽数据,则不再使用系统默认填充的拖拽数据。文本类组件Text、Search、TextInput、TextArea、RichEditor对选中的文本内容进行拖拽时,不支持背板图的自定义。当onDragStart与菜单预览一起使用或使用了默认支持拖出能力的组件时,预览及菜单项上的自定义内容不支持拖拽。
OnDrop
绑定此事件的组件可作为拖拽释放目标,当在本组件范围内停止拖拽行为时,触发回调。如果开发者没有在onDrop中主动调用event.setResult()设置拖拽接收的结果,则系统按照数据接收成功处理.通常在该事件回调中执行拖拽落位后的数据处理逻辑,如刷新UI、在线资源下载、落入数据发送、落盘等业务逻辑。该回调中不应该同步执行耗时操作,否则可能导致抬手操作后拖拽背板延迟消失的问题,应使用JS提供的异步处理机制避免此类问题。
OnDragEnter
拖拽进入组件范围内时,触发回调,当监听了onDrop事件时,此事件才有效。通常在该事件回调中实现一些动效以标识当前拖拽的响应。
OnDragMove
拖拽过程在该组件范围内移动时,触发该回调,但该事件只在监听OnDrop事件后才会被成功触发。
OnDragLeave
拖拽离开组件范围内时,触发回调,当监听了onDrop事件时,此事件才有效。注意:监听OnDragEnter、OnDragLeave事件时必须监听OnDrop事件,否则不能正常响应事件,且在拖拽过程中存在不能监听到OnDragLeave的情况;若要100%保证能够监听到OnDragLeave事件,则应该使用setDragEventStrictReportingEnabled方法严格触发onDragLeave事件。
DragController (自定义拖拽)
参考示例:
// Step 1:获取到组件UI上下文
let uiContext: UIContext = this.getUIContext() as UIContext;
// Step 2:通过组件UI上下文的DragController 创建 drag action 对象
this.dragAction = uiContext.getDragController().createDragAction(this.customBuilder, dragInfo)
if(!this.dragAction){
return
}
// Step 3: 注册拖拽状态监听,以便处理拖拽结束(已落位的结果)
this.dragAction.on('statusChange', (dragAndDropInfo)=>{
if (dragAndDropInfo.status == dragController.DragStatus.ENDED){ // 已经落位结束
console.info("drag has end with result " + dragAndDropInfo.event.getResult());
if (!this.dragAction) {
return
}
this.dragAction.off('statusChange')
}
})
// Step 4:发起拖拽
this.dragAction.startDrag().then(()=>{}).catch((err:Error)=>{
console.error("start drag Error:" + err.message);
})
主动发起拖拽能力,传入拖拽发起后跟手效果所拖拽的对象以及携带拖拽信息。通过回调返回结果。
参考链接:getDragController,onPreDrag。
1.1.2 ArkUI涉及的参考文档
拖拽事件,拖拽控制,@ohos.arkui.dragController (DragController),通过NDK接口对接拖拽:拖拽事件NDK。
1.2 UDMF
UDMF提供标准化数据定义:提供HarmonyOS跨应用、跨设备的统一数据类型标准,包含标准化数据类型和标准化数据结构。
参考文档:@ohos.data.unifiedDataChannel (标准化数据通路),@ohos.data.commonType (数据通用类型),@ohos.data.uniformTypeDescriptor (标准化数据定义与描述),@ohos.data.uniformDataStruct (标准化数据结构)。
其它接口文档:addEntry,通过标准化数据通路实现数据共享。
1.3 网络 API
若拖拽的数据是在线资源时,则应由拖拽落入方实现在线资源的下载处理逻辑,可参考以下API使用实现对应功能。
参考文档:@ohos.request (上传下载),HTTP数据请求。
2. 拖拽场景指导及示例
2.1 文字拖拽
Text 组件默认可拖拽,若要实现长按选中浮起后拖拽的逻辑,则应给Text组件设置 CopyOptions 属性,并且设置draggable属性为true。
Search、TextInput、TextArea 、RichEditor组件均默认支持可落入,若要实现自定义组件内落入文字,则可按如下方式实现示例代码如下所示
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
@Entry
@Component
struct TextDemo {
@State message: string = '';
controller: RichEditorController = new RichEditorController();
options: RichEditorOptions = { controller: this.controller };
build() {
Column() {
// 文字拖出
Column() {
Text('123456').copyOption(CopyOptions.InApp).draggable(true)
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
// 文字拖入
Column() {
Text(this.message).copyOption(CopyOptions.InApp).draggable(true).width('100%').height('20%').textAlign(TextAlign.Center)
.allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])
// Text组件需在onDrop回调中实现落入数据的处理
.onDrop((event: DragEvent, extraParams: string) => {
let data: UnifiedData = event.getData();
let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
if (records[0].getType() === uniformTypeDescriptor.UniformDataType.PLAIN_TEXT) {
let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;
this.message = plainText.textContent;
}
})
TextArea({ placeholder: 'please input words' }).copyOption(CopyOptions.InApp).draggable(true).width('100%').height('20%').textAlign(TextAlign.Center)
Search({ placeholder: 'please input words' }).copyOption(CopyOptions.InApp).draggable(true).width('100%').height('20%').textAlign(TextAlign.Center)
TextInput({ placeholder: 'please input words' }).copyOption(CopyOptions.InApp).draggable(true).width('100%').height('20%').textAlign(TextAlign.Center)
RichEditor(this.options).draggable(true).width('100%').height('20%')
}
.width('70%').height('50%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}
.width('100%').height('100%')
}
}
2.1.2 长按选中当前Text组件的全部文本
因默认情况长按之后选中改的仅为按压处的几个字符,若要实现长按选中当前Text组件的全部文本,则可在 LongPressGesture 手势中实现回调并结合 Selection 实现该效果.
Text(this.message)
.copyOption(CopyOptions.InApp)
.draggable(true)
. parallelGesture(
LongPressGesture({ repeat: true })
// 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
.onAction((event: GestureEvent) => {
this.beginIndex = 0;
this.endIndex = endIndexOfTextContent;
})
.selection(this.beginIndex, this.endIndex)
2.1.3 消息以气泡形式拖出(IM应用Chat界面消息气泡场景)
该场景本质上是对气泡组件本身进行拖拽,但是在OnDragStart回调中设置拖拽数据为气泡组件上锁承载的文字消息内容,具体实现可如下所示
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
@Entry
@Component
struct ParentComponentDemo {
@State message: string = 'Hello World';
@State text: string = '';
build() {
Column() {
// 拖出
Column() {
Child({text: this.message})
.width('50%').height('50%').draggable(true).backgroundColor(Color.Green)
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.onDragStart((event: DragEvent) => {
let data: unifiedDataChannel.PlainText = new unifiedDataChannel.PlainText();
data.abstract = this.message;
data.textContent = this.message;
(event as DragEvent).setData(new unifiedDataChannel.UnifiedData(data));
})
}
.width('70%').height('40%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
// 拖入
Column() {
Text(this.text)
.width('50%').height('50%').backgroundColor(Color.Red).textAlign(TextAlign.Center)
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.onDrop((event: DragEvent, extraParams: string) => {
let data: UnifiedData = event.getData();
let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
if (records[0].getType() === uniformTypeDescriptor.UniformDataType.PLAIN_TEXT) {
let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;
this.text = plainText.textContent;
}
})
}
.width('70%').height('40%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}
.width('100%').height('100%')
}
}
@Component
struct Child{
@State text: string = '';
build(){
Column(){
Text(this.text).copyOption(CopyOptions.InApp).draggable(true)
}
.width('100%').height('100%')
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}
}
2.2 图片拖拽
Image 组件默认可拖拽,可不设置draggable来实现图片拖拽功能。
2.2.1 本地图片资源拖拽
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'
@Entry
@Component
struct ImageLocal {
@State targetImage: string | PixelMap | null = null;
controller: RichEditorController = new RichEditorController();
options: RichEditorOptions = { controller: this.controller };
build() {
Column() {
// 本地图片资源拖出
Column() {
Image($r('app.media.test'))
.objectFit(ImageFit.Contain).draggable(true)
.width('70%').height('70%')
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
// 本地图片资源拖入
Column() {
Image(this.targetImage)
.objectFit(ImageFit.Contain)
.width('70%').height('70%')
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE]) // 如需匹配扩展名需使用UDS结构
.onDrop((event: DragEvent, extraParams: string) => {
let data: UnifiedData = event.getData();
let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
if (records[0].getType() === uniformTypeDescriptor.UniformDataType.IMAGE) {
let image: unifiedDataChannel.Image = records[0] as unifiedDataChannel.Image;
this.targetImage = image.imageUri;
}
})
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
RichEditor(this.options).draggable(true).width('100%').height('100%')
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop((event: DragEvent, extraParams: string) => {
let data: UnifiedData = event.getData();
let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
if (records[0].getType() === uniformTypeDescriptor.UniformDataType.IMAGE) {
let image: unifiedDataChannel.Image = records[0] as unifiedDataChannel.Image;
this.controller.addImageSpan(image.imageUri);
}
})
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}.width('100%').height('100%')
}
}
2.2.2 在线图片资源拖拽
加载在线图片资源需在module.json5文件中添加网络权限:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
import { uniformTypeDescriptor } from '@kit.ArkData'
import { image } from '@kit.ImageKit';
import request from '@ohos.request';
import { common } from '@kit.AbilityKit';
import fs from '@ohos.file.fs';
import { buffer } from '@kit.ArkTS';
@Entry
@Component
struct ImageOnline {
@State targetImage: string | PixelMap | null = null;
controller: RichEditorController = new RichEditorController();
options: RichEditorOptions = { controller: this.controller };
pixelMap: image.PixelMap | undefined = undefined
context = getContext(this) as common.UIAbilityContext;
filesDir = this.context.filesDir;
build() {
Column() {
// 在线图片资源拖出
Column() {
Image('http://sns-img-hw.xhscdn.com/1040g008313f5sogu045g4a4pehn7be0k6obctpg?imageView2/2/w/1080/format/reif/q/90')
.objectFit(ImageFit.Contain).draggable(true)
.width('70%').height('70%')
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
// 在线图片资源拖入
Column() {
Image(this.targetImage)
.objectFit(ImageFit.Contain)
.width('70%').height('70%')
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop((event: DragEvent, extraParams: string) => {
let records: Array<udmf.UnifiedRecord> = dragEvent.getData().getRecords();
let uri = (records[0] as udmf.Image).imageUri;
if (typeof uri == 'string') {
this.targetImage = uri;
// 落盘到本地
request.downloadFile(this.context, {
url: uri,
filePath: this.filesDir + '/test.png'
}).then((downloadTask: request.DownloadTask) => {
let file = fs.openSync(this.filesDir + '/test.png', fs.OpenMode.READ_WRITE);
let arrayBuffer = new ArrayBuffer(1024);
let readLen = fs.readSync(file.fd, arrayBuffer);
let buf = buffer.from(arrayBuffer, 0, readLen);
console.info(`The content of file: ${buf.toString()}`);
fs.closeSync(file);
})
}
})
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
RichEditor(this.options).draggable(true).width('100%').height('100%')
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop((event: DragEvent, extraParams: string) => {
let arr: Record<string, object> = JSON.parse(extraParams) as Record<string, object>;
let uri = arr['extraInfo'];
if (typeof uri == 'string') {
this.controller.addImageSpan(uri);
}
})
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}.width('100%').height('100%')
}
}
2.2.3 PixelMap拖拽
import { common } from '@kit.AbilityKit'
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import fs from '@ohos.file.fs';
@Entry
@Component
struct PixelMapDrag {
@State targetImage: string | PixelMap | null = null;
private context = getContext(this) as common.UIAbilityContext;
filesDir = this.context.filesDir;
build() {
Column() {
// PixelMap拖出
Column() {
Image(this.context.resourceManager.getDrawableDescriptor($r('app.media.test').id).getPixelMap())
.objectFit(ImageFit.Contain).draggable(true)
.width('70%').height('70%')
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
// PixelMap拖入
Column() {
Image(this.targetImage)
.objectFit(ImageFit.Contain)
.width('70%').height('70%')
.allowDrop([uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP])
.onDrop(async (event: DragEvent, extraParams: string) => {
let data: UnifiedData = event.getData();
let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
if (records[0].getType() === uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP) {
let record: unifiedDataChannel.SystemDefinedPixelMap = (records[0]) as unifiedDataChannel.SystemDefinedPixelMap;
this.targetImage = await this.createPixelMap(record);
// 落盘到本地
const imagePackerApi = image.createImagePacker();
let packOpts : image.PackingOption = { format:"image/jpeg", quality:98 };
const path : string = this.context.cacheDir + "/pixel_map.jpg";
let file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
imagePackerApi.packToFile(this.targetImage, file.fd, packOpts).then(() => {
// 直接打包进文件
}).catch((error : BusinessError) => {
console.error('Failed to pack the image. And the error is: ' + error);
})
}
})
}
.width('70%').height('30%')
.border({ width: 2, color: Color.Gray, radius: 5, style: BorderStyle.Dotted })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}.width('100%').height('100%')
}
public async createPixelMap(pixelMap: unifiedDataChannel.SystemDefinedPixelMap): Promise<image.PixelMap | null> {
let mWidth: number = (pixelMap.details?.width ?? -1) as number;
let mHeight: number = (pixelMap.details?.height ?? -1) as number;
let mPixelFormat: image.PixelMapFormat =
(pixelMap.details?.['pixel-format'] ?? image.PixelMapFormat.UNKNOWN) as image.PixelMapFormat;
let mItemPixelMapData: Uint8Array = pixelMap.rawData;
const opts: image.InitializationOptions = {
editable: false, pixelFormat: mPixelFormat, size: {
height: mHeight,
width: mWidth
}
};
const buffer: ArrayBuffer = mItemPixelMapData.buffer.slice(mItemPixelMapData.byteOffset,
mItemPixelMapData.byteLength + mItemPixelMapData.byteOffset);
try {
let pixelMap: image.PixelMap = await image.createPixelMap(buffer, opts);
return pixelMap;
} catch (err) {
console.error('dragtest--> getPixelMap', err);
return null;
}
}
}
2.3 文件拖拽
文件拖拽,以Video为例:
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import { fileIo, fileUri } from '@kit.CoreFileKit';
@Component
export struct ResourceVideoDragDemo {
@State context: Context = getContext(this);
@State videoWidth: number = 200;
@State videoHeight: number = 200;
@State innerResource: Resource | string | null = null;
@State originalResource: string = '';
private controller: VideoController | undefined;
build() {
NavDestination() {
Column() {
Column() {
// 文件拖出
Column() {
Text('原视频').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
Video({ src: $rawfile('VID_1725089040_001.mp4'), controller: this.controller })
.width(200)
.height(200)
.autoPlay(true)
.loop(true)
.draggable(true)
.border({ radius: 8 })
// 长按之后,将视频文件拷贝到沙箱路径
.parallelGesture(
LongPressGesture({ repeat: false })
.onAction((event: GestureEvent) => {
this.context.resourceManager.getRawFd('VID_1725089040_001.mp4', (err, data) => {
let filePath = this.context.filesDir + '/VID_1725089040_001.mp4';
console.log('文件路径:' + filePath);
let dest = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
let bufsize = 4096;
let buf = new ArrayBuffer(bufsize);
let off = 0, len = 0, readedLen = 0;
// 通过buffer将rawfile文件内容copy到沙箱路径
while (len = fileIo.readSync(data.fd, buf, { offset: data.offset + off, length: bufsize })) {
readedLen += len;
fileIo.writeSync(dest.fd, buf, { offset: off, length: len });
off = off + len;
if ((data.length - readedLen) < bufsize) {
bufsize = data.length - readedLen;
}
}
fileIo.close(dest.fd);
this.context.resourceManager.closeRawFd('VID_1725089040_001.mp4');
this.originalResource = fileUri.getUriFromPath(filePath);
console.log('originalResource:' + this.originalResource);
})
})
)
.onDragStart((event: DragEvent) => {
let video = new unifiedDataChannel.Video();
video.videoUri = this.originalResource;
let unifiedData = new unifiedDataChannel.UnifiedData(video);
event.setData(unifiedData);
})
}
Column() {
Text('视频拖拽落入框').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
Video({ src: this.innerResource, controller: this.controller })
.width(200)
.height(200)
.border({ radius: 8 })
.width(this.videoWidth)
.height(this.videoWidth)
.visibility(this.innerResource === null ? Visibility.Hidden : Visibility.Visible)
.autoPlay(true)
.loop(true)
}
.allowDrop([uniformTypeDescriptor.UniformDataType.VIDEO])
// 文件拖入
.onDrop(async (event: DragEvent) => {
let dragData = event.getData();
if (dragData != undefined) {
let records = dragData.getRecords();
let type = records[0].getType();
switch (type) {
case uniformTypeDescriptor.UniformDataType.VIDEO:
let record: unifiedDataChannel.Video = (records[0]) as unifiedDataChannel.Video;
this.innerResource = record.videoUri;
break;
default:
return;
}
event.setResult(0)
}
})
.width(300)
.height(300)
.border({ width: 1, radius: 5, style: BorderStyle.Dashed })
}.justifyContent(FlexAlign.SpaceBetween)
.layoutWeight(1)
.margin({ bottom: 10 })
}.width('100%').height('100%')
}
.title('资源视频video拖拽示例')
}
}
2.4 图文拖拽
图文拖入时,需要将dragEvent中的数据的图文分开依次处理,可由如下示例代码实现
2.4.1 Text组件图文拖拽
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import common from '@ohos.app.ability.common';
import { ImageUtil } from '../utils/ImageUtil';
@Component
export struct TextImageDragDemo {
@State targetImage: string | PixelMap | null = null;
@State targetTextContent: string = "图文落入框";
@State imageWidth: number = 200;
@State imageHeight: number = 200;
@State isDraggable: boolean = true
@State opacityValue: number = 1;
private context = getContext(this) as common.UIAbilityContext
build() {
NavDestination() {
Column() {
Column() {
// 图文拖出
Column() {
Text('原图文').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
Text() {
Span('测试图文拖拽')
ImageSpan(this.context.resourceManager.getDrawableDescriptor($r('app.media.2238350494').id).getPixelMap())
.width(200)
.height(200)
.border({ radius: 8 })
}
.opacity(this.opacityValue)
.onDragLeave((_event: DragEvent) => {
console.info('dragtest onDragMove')
this.opacityValue = 0.4
})
.onDragMove((_event: DragEvent) => {
console.info('dragtest onDragMove')
this.opacityValue = 0.4
})
.copyOption(CopyOptions.InApp)
}
// 图文拖入
Column() {
Text('图文落入框').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
Text() {
Span(this.targetTextContent)
ImageSpan(this.targetImage)
.width(this.imageWidth).height(this.imageHeight)
.visibility(this.targetImage === null ? Visibility.Hidden : Visibility.Visible)
}
}
.allowDrop([uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP,
uniformTypeDescriptor.UniformDataType.IMAGE,
uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,
uniformTypeDescriptor.UniformDataType.TEXT])
.onDragMove((_event: DragEvent) => {
this.opacityValue = 0.4
})
.onDrop(async (event: DragEvent) => {
console.info('dragtest start onDrop')
let dragData = event.getData();
if (dragData != undefined) {
let records = dragData.getRecords();
for (let i = 0; i < records.length; i++) {
console.info('dragtest get records', JSON.stringify(i))
let type = records[i].getType();
switch (type) {
case uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP:
console.info('dragtest type is PIXEL_MAP')
let record: unifiedDataChannel.SystemDefinedPixelMap = (records[i]) as unifiedDataChannel.SystemDefinedPixelMap;
this.targetImage = await ImageUtil.createPixelMap(record);
break;
case uniformTypeDescriptor.UniformDataType.IMAGE:
console.info('dragtest type is IMAGE')
let image: unifiedDataChannel.Image = (records[i]) as unifiedDataChannel.Image;
this.targetImage = image.imageUri;
break;
case uniformTypeDescriptor.UniformDataType.PLAIN_TEXT:
console.info('dragtest type is PLAIN_TEXT')
let plainText: unifiedDataChannel.PlainText = records[i] as unifiedDataChannel.PlainText;
this.targetTextContent = plainText.textContent;
console.log('dragtest targetTextContent:' + this.targetTextContent);
break;
case uniformTypeDescriptor.UniformDataType.TEXT:
console.info('dragtest type is TEXT')
let currentText: unifiedDataChannel.Text = records[i] as unifiedDataChannel.Text;
let text: string = !!currentText.details ? currentText.details['value'] : '';
this.targetTextContent = this.targetTextContent.concat(text);
console.log('dragtest targetTextContent:' + this.targetTextContent);
break;
default:
console.info('dragtest type is error')
return;
}
}
event.setResult(0)
this.opacityValue = 1
}
})
.width(300)
.height(300)
.border({ width: 1, radius: 5, style: BorderStyle.Dashed })
}.justifyContent(FlexAlign.SpaceBetween)
.layoutWeight(1)
.margin({ bottom: 10 })
}.width("100%").height('100%')
}
.title('图文拖拽示例')
}
}
2.4.2 RichEditor组件图文拖拽
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { ImageUtil } from '../utils/ImageUtil';
@Component
export struct RichImageDragDemo {
@State targetImage: string | PixelMap | null = null;
@State targetTextContent: string = "";
@State imageWidth: number = 200;
@State imageHeight: number = 200;
@State isDraggable: boolean = true
@State opacityValue: number = 1;
sourceController: RichEditorController = new RichEditorController();
targetController: RichEditorController = new RichEditorController();
private context = getContext(this) as common.UIAbilityContext
build() {
NavDestination() {
Column() {
Column() {
// 图文拖出
Column() {
Text('富文本区域图片').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
RichEditor({ controller: this.sourceController })
.onReady(() => {
this.sourceController.addTextSpan("012345",
{
style:
{
fontColor: Color.Orange,
fontSize: 30
}
})
this.sourceController.addImageSpan(
this.context.resourceManager.getDrawableDescriptor($r('app.media.2238350494').id).getPixelMap(),
{
imageStyle:
{
size: ["157px", "157px"]
}
})
})
}
// 图文拖入
Column() {
Text('图文落入框').fontSize(12).fontColor(0xCCCCCC).width('100%').padding({ left: 20 })
RichEditor({ controller: this.targetController })
}
.allowDrop([uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP,
uniformTypeDescriptor.UniformDataType.IMAGE,
uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,
uniformTypeDescriptor.UniformDataType.TEXT])
.onDragMove((_event: DragEvent) => {
this.opacityValue = 0.4
})
.onDrop(async (event: DragEvent) => {
console.info('dragtest start onDrop')
let dragData = event.getData();
this.targetController.getCaretOffset();
if (dragData != undefined) {
let records = dragData.getRecords();
for (let i = 0; i < records.length; i++) {
console.info('dragtest get records', JSON.stringify(i))
let type = records[i].getType();
switch (type) {
case uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP:
console.info('dragtest type is PIXEL_MAP')
let record: unifiedDataChannel.SystemDefinedPixelMap = (records[i]) as unifiedDataChannel.SystemDefinedPixelMap;
this.targetImage = await ImageUtil.createPixelMap(record);
this.targetController.addImageSpan(this.targetImage,
{
imageStyle:
{
size: ["175px", "175px"]
},
offset: this.targetController.getCaretOffset()
})
break;
case uniformTypeDescriptor.UniformDataType.IMAGE:
console.info('dragtest type is IMAGE')
let image: unifiedDataChannel.Image = (records[i]) as unifiedDataChannel.Image;
this.targetImage = image.imageUri;
this.targetController.addImageSpan(this.targetImage,
{
imageStyle:
{
size: ["175px", "175px"]
},
offset: this.targetController.getCaretOffset()
})
break;
case uniformTypeDescriptor.UniformDataType.PLAIN_TEXT:
console.info('dragtest type is PLAIN_TEXT')
let plainText: unifiedDataChannel.PlainText = records[i] as unifiedDataChannel.PlainText;
this.targetTextContent = plainText.textContent;
console.log('dragtest targetTextContent:' + this.targetTextContent);
this.targetController.addTextSpan(this.targetTextContent,
{
style:
{
fontColor: Color.Orange,
fontSize: 30
},
offset: this.targetController.getCaretOffset()
})
break;
case uniformTypeDescriptor.UniformDataType.TEXT:
console.info('dragtest type is TEXT')
let currentText: unifiedDataChannel.Text = records[i] as unifiedDataChannel.Text;
let text: string = !!currentText.details ? currentText.details['value'] : '';
this.targetTextContent = this.targetTextContent.concat(text);
console.log('dragtest targetTextContent:' + this.targetTextContent);
this.targetController.addTextSpan(this.targetTextContent,
{
style:
{
fontColor: Color.Orange,
fontSize: 30
},
offset: this.targetController.getCaretOffset()
})
break;
default:
console.info('dragtest type is error')
return;
}
}
event.setResult(0)
this.opacityValue = 1
}
this.targetTextContent = "";
this.targetImage = null;
})
.width(300)
.height(300)
.border({ width: 1, radius: 5, style: BorderStyle.Dashed })
}.justifyContent(FlexAlign.SpaceBetween)
.layoutWeight(1)
.margin({ bottom: 10 })
}.width("100%").height('100%')
}
.title('富文本中图文拖拽示例')
}
}
2.4.3 多entry
import { uniformDataStruct, uniformTypeDescriptor, unifiedDataChannel } from '@kit.ArkData';
import { fileUri, fileIo as fs } from '@kit.CoreFileKit'
import { common } from '@kit.AbilityKit'
@Entry
@Component
struct Index {
@State ondrop_uri: string = '';
@State description:string = '';
uiContext = this.getUIContext();
udKey: string = '';
build() {
Column() {
Column() {
Text("拖出方")
Text("oriUri:" + 'file://data/image/1.png')
Text("description:" + 'This is the description of the hyperlink')
}
.width(300)
.height(200)
.borderWidth(2)
.margin(20)
.onDragStart((event)=>{
//创建FILE_URI类型数据
let fileUriDetails : Record<string, string> = {
'attr1': 'value1',
'attr2': 'value2',
}
let fileUri : uniformDataStruct.FileUri = {
uniformDataType : 'general.file-uri',
oriUri : 'file://data/image/1.png',
fileType : 'general.image',
details : fileUriDetails,
}
//创建HYPERLINK类型数据
let hyperlink : uniformDataStruct.Hyperlink = {
uniformDataType:'general.hyperlink',
url : 'file://data/image/1.png',
description : 'This is the description of the hyperlink',
}
//新建一个新的unifiedData
let unifiedData = new unifiedDataChannel.UnifiedData();
//新建一个UnifiedRecord 初始化塞入FORM类型的数据(可自定义)
let record = new unifiedDataChannel.UnifiedRecord(uniformTypeDescriptor.UniformDataType.HYPERLINK, hyperlink);
//addEntry 添加另一份FILE_URI数据到record里面
record.addEntry(uniformTypeDescriptor.UniformDataType.FILE_URI, fileUri);
//将record塞入unifiedData
unifiedData.addRecord(record);
//将unifiedData塞入dragEvent
event.setData(unifiedData)
})
Button("Reset")
.onClick(()=>{
this.ondrop_uri = ''
this.description = ''
})
Column() {
Text("落入方")
Text("oriUri:" + this.ondrop_uri)
Text("description:" + this.description)
}
.width(300)
.height(200)
.borderWidth(2)
.margin(20)
.onDrop((event)=>{
//配置异步拖拽参数
let context = this.uiContext.getHostContext() as common.UIAbilityContext;
let pathDir: string = context.distributedFilesDir;
let destUri = fileUri.getUriFromPath(pathDir);
//配置数据处理回调
let progressListener: unifiedDataChannel.DataProgressListener = (progress: unifiedDataChannel.ProgressInfo, dragData: UnifiedData|null) => {
if(dragData != null) {
//拿到records
let records:Array<unifiedDataChannel.UnifiedRecord> = dragData.getRecords();
//循环遍历查询
for (let i = 0; i < records.length; i++) {
let unifiedDataRecord = records[i] as unifiedDataChannel.UnifiedRecord;
//找到对应类型的数据 getEntry
let fileUriRead : uniformDataStruct.FileUri = unifiedDataRecord.getEntry(uniformTypeDescriptor.UniformDataType.FILE_URI) as uniformDataStruct.FileUri;
if (fileUriRead != undefined) {
//拿到数据类型中对应的字段
console.info(`oriUri: ${fileUriRead.oriUri}`);
this.ondrop_uri = fileUriRead.oriUri;
}
let hyperlink = unifiedDataRecord.getEntry(uniformTypeDescriptor.UniformDataType.HYPERLINK) as uniformDataStruct.Hyperlink;
if (hyperlink != undefined) {
console.info(`description: ${hyperlink.description}`);
this.description = hyperlink.description as string;
}
}
} else {
console.log('dragData is undefined');
}
console.log(`percentage: ${progress.progress}`);
};
//配置异步获取数据options
let options: DataSyncOptions = {
destUri: destUri,
fileConflictOptions: unifiedDataChannel.FileConflictOptions.OVERWRITE, //配置数据冲突处理方式
progressIndicator: unifiedDataChannel.ProgressIndicator.DEFAULT, //配置进度条,数据处理超过500ms生效
dataProgressListener: progressListener,
}
try {
this.udKey = (event as DragEvent).startDataLoading(options);
console.log('udKey: ', this.udKey);
} catch(e) {
console.log(`startDataLoading errorCode: ${e.code}, errorMessage: ${e.message}`);
}
}, {disableDataPrefetch: true}) //当使用startDataLoading获取数据时需设置该参数为true,防止拖拽提前获取数据。
Button('取消数据传输')
.onClick(() => {
try {
this.getUIContext().getDragController().cancelDataLoading(this.udKey);
} catch (e) {
console.log(`cancelDataLoading errorCode: ${e.code}, errorMessage: ${e.message}`);
}
})
.margin({top: 10})
}
.height('100%')
.width('100%')
}
}
3.共性问题解决方案
3.1 拖出的PixelMap落入到备忘录、中转站等时与拖出端存在色差
拖出PixelMap时,需要设置当前PixelMap的编码格式,否则落入方不能确定该PixelMap的编码方式,按照默认行为处理可能出现存在色差的情况。
PixelMap的默认编码格式是 RGBA,若拖出方未指定PixelMap的编码格式,则落入方按照RGBA格式解码即可。
若拖出方有指定了PixelMap的编码格式,则落入方通过systemDefinedPixelMap.details; details['pixel-format'] 获取拖出方的编码格式,再以该格式解码即可。
示例代码见图片拖拽下PixelMap拖拽。
PixelMap 使用指导。
3.2 Image组件是在线资源时拖出后不能正常落位
若Image组件拖出时,其资源文件是在线地址,则需要拖拽落入方实现对在线数据的下载处理逻辑
当前系统应用中 备忘录、中转站、小艺均已支持在线图片资源的落入处理
处理在线资源落入时,可通过 Http 模块提供的下载能力实现在线uri的处理,如下示例代码所示
http.createHttp().request('http://sns-img-hw.xhscdn.com/1040g008313f5sogu045g4a4pehn7be0k6obctpg?imageView2/2/w/1080/format/reif/q/90',
{
method: http.RequestMethod.GET,
connectTimeout: 60000,
readTimeout: 60000
},
(error: BusinessError, data: http.HttpResponse) => {
let imageData: ArrayBuffer = data?.result as ArrayBuffer
let imageSource: image.ImageSource = image.createImageSource(imageData)
imageSource.getImageInfo((err, data) => {
let opts: image.DecodingOptions = {
editable: true,
desiredSize: {
height: data.size.height,
width: data.size.width
}
}
imageSource.createPixelMap(opts, async (err, pixelMap) => {
if (err) return
const offScreenCanvas = new OffscreenCanvas(data.size.width, data.size.height, LengthMetricsUnit.PX)
const offScreenContext: OffscreenCanvasRenderingContext2D = offScreenCanvas.getContext('2d')
offScreenContext.drawImage(pixelMap, 0, 0, offScreenCanvas.width, offScreenCanvas.height)
offScreenContext.textAlign = 'right'
offScreenContext.fillStyle = 'rgba(255, 255, 255, 0.7)'
offScreenContext.font = vp2px(8) + 'px'
offScreenContext.shadowBlur = 2
offScreenContext.shadowColor = 'rgba(0, 0, 0, 0.3)'
offScreenContext.fillText(`小红书号`, offScreenCanvas.width - vp2px(10), offScreenCanvas.height - vp2px(11))
this.pixelMap = offScreenContext.getPixelMap(0, 0, offScreenCanvas.width, offScreenCanvas.height);
const imagePackerApi = image.createImagePacker();
const context = getContext(this)
let packOpts : image.PackingOption = { format:"image/png", quality:98 };
let pathDir = context.filesDir
let path = '/cache.png'
this.writeFileSync(pathDir, path)
let file = await fs.open(context.filesDir+path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
3.3 拖拽的触发手势长按浮起与长按手势之间存在冲突
3.3.1 长按手势与拖拽发起手势冲突
若组件既需要处理长按手势,又需要支持可拖拽能力(因拖拽行为的触发为长按500ms并移动10vp),则可通过 parallelGesture 实现多个手势事件的处理,而不产生冲突。
ParallelGesture使用参考:
// xxx.ets
@Entry
@Component
struct LongPressGestureExample {
@State count: number = 0
build() {
Column() {
Text('LongPress onAction:' + this.count).fontSize(28)
.ParallelGesture(
LongPressGesture({ repeat: true })
// 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
.onAction((event: GestureEvent) => {
if (event && event.repeat) {
this.count++
}
})
// 长按动作一结束触发
.onActionEnd((event: GestureEvent) => {
this.count = 0
})
)
.onDragStart((event:DragEvent) => {
// 设置拖拽数据和自定义拖拽背板
})
}
.height(200)
.width(300)
.padding(20)
.border({ width: 3 })
.margin(30)
}
}
3.3.2 长按弹窗、弹出子菜单与拖拽手势发起冲突的情况
// xxx.ets
@Entry
@Component
struct LongPressGestureExample {
@State count: number = 0;
build() {
Column() {
Text('LongPress onAction:' + this.count).fontSize(28)
.ParallelGesture(
LongPressGesture({ repeat: true })
// 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
.onAction((event: GestureEvent) => {
// 弹出子窗或子菜单
})
// 长按动作一结束触发
.onActionEnd((event: GestureEvent) => {
this.count = 0;
})
)
.onDragStart((event:DragEvent) => {
// 设置拖拽数据和自定义拖拽背板
})
.onDragMove((event:DragEvent) => {
// 隐藏子窗或子菜单,避免中断拖拽手势
})
.onDrop((event:DragEvent) => {
// 必须监听onDrop事件,否则onDragMove事件回调不会被触发
})
}
.height(200)
.width(300)
.padding(20)
.border({ width: 3 })
.margin(30)
}
}
3.4 图文混排数据拖拽落位后与原拖出方排版格式有差异
当前拖拽数据中图文混排数据可通过HTML实现,但拖拽落入方组件可能并非HTML格式,如RichEditor、RichText等。
当前系统能力对各类富文本数据的格式转换能力较弱,不能保证排版样式一致。
3.5文字无法长按选中后再起拖
文字若要长按选中后再起拖,必须要设置copyOptions属性,否则无法触发长按选中逻辑,示例如下所示
Text(this.message)
.copyOption(CopyOptions.InApp)
.draggable(true)
若要实现长按全选,则应监听长按手势,并结合selection接口实现
Text(this.message)
.copyOption(CopyOptions.InApp)
.draggable(true)
. parallelGesture(
LongPressGesture({ repeat: true })
// 由于repeat设置为true,长按动作存在时会连续触发,触发间隔为duration(默认值500ms)
.onAction((event: GestureEvent) => {
this.beginIndex = 0;
this.endIndex = endIndexOfTextContent;
})
.selection(this.beginIndex, this.endIndex)
3.6 拖拽落入到某个组件后不能触发其他组件的动作
eg:拖拽到某个IM应用的编辑框后,不能触发其发送按钮高亮标识可发送
本质上,该场景是应用自身应该实现的处理逻辑,可通过状态变量、Provider Consumer 注解等多种方式实现。
3.7 拖拽到文字某些"输入框"后不响应拖拽
问题原因,因ArkUI默认支持落入的组件包括"Search、TextInput、TextArea、Video",若应用实现时输入框使用的是Text组件实现,则默认不可响应拖拽事件.
修改方案
若要实现对拖拽事件的响应,则应主动调用allowDrop、onDrop逻辑实现落入。示例代码可如下所示:
content: string = "请输入"
Text(this.message)
.allowDrop([
uniformTypeDescriptor.UniformDataType.PLAIN_TEXT, uniformTypeDescriptor.UniformDataType.TEXT])
.onDrop((event:DragEvent) => {
let currentText: unifiedDataChannel.Text = records as unifiedDataChannel.Text;
let text: string = !!currentText.details ? currentText.details['value'] : '';
this.content= this.content.concat(text);
})
3.8 拖拽到小艺后显示不支持处理此类数据
问题原因
1、该问题通常是由于拖拽数据源设置不当导致的问题,如拖拽文件时文件的uri为非法参数,则在落位获取数据时,UDMF不能正常读取数据,导致落位失败
2、72及之前版本,小艺未对PixelMap 和 在线Image资源的落位做适配,导致落位失败,73版本已修复。
解决办法
在拖拽发起时候应正确设置拖拽数据,避免拖拽数据为空或拖拽数据非法,如无效uri等情况。
3.9 文件、图片等拖入到可落入组件(显示绿色+角标)时不能正常落位
问题原因
1、显示绿色角标是因为落入方组件在allowDrop列表中刚设置了当前拖拽数据类型,标识支持该类数据的落入
2、但是具体是否能够成功落位取决于落入组件在onDrop逻辑中是否正确处理了拖拽数据,如未对数据做处理并刷新UI、读取数据失败等情况都有可能导致落位失败
解决办法
1、落入方应用需明确在onDrop逻辑中是否有对当前拖拽数据做处理并刷新UI
2、排查当前拖拽数据是否非法,如 无效uri、无效在线地址等情况。
3.10 问题定位定界关键日志
判定当前拖拽的Image是不是在线资源
在起拖端搜索如下日志,有则说明是在线资源
hilog | grep "Get uri authority empty or uri scheme not equals to file."
# 若有上述日志,标识当前拖拽的是在线资源
判定当前拖拽数据类型及Summary
在起拖端,日志中搜索AceDrag.*summary,在起拖端即可看到当前拖拽数据的summary
hilog | grep -E "AceDrag.*summary"
更多推荐
所有评论(0)