HarmonyOS:统一拖拽
一、概述
拖拽操作是一种直观且高效的数据传输方式,它允许用户通过标准手势(包括用手指、鼠标或触控笔按住并移动)在应用程序之间及内部进行数据传输。
拖拽功能不仅操作便捷,还能与多种系统能力深度融合,拓展出更为广泛的应用场景。例如,跨设备拖拽让用户能在不同设备间无缝传输数据,跨窗口拖拽提升了多任务处理的灵活性。此外,基于拖拽操作还可以开发出更多创新性的应用场景,如AI智能识别、水印添加等,这些创新性的功能接入统称为“统一拖拽”。
下面将介绍几种典型拖拽场景及其具体实现方案,帮助开发者更好地理解和应用拖拽技术。
通过设置组件的拖拽响应,可以自定义拖出数据、拖入数据和拖拽背板图,实现如下场景:
- 拖拽图像增加水印:为拖拽的图像添加水印,水印内容为图像的拖拽时间。开发者可以在应用时根据需求自定义水印内容,
如标记拖拽图片的来源信息,为图像管理与溯源提供便利。- 自定义拖拽背板图:将拖拽中的背板图设置为自定义数据内容。开发者可根据个性化需求打造独特的拖拽视觉效果。
- AI识别拖拽内容:通过在接收拖拽内容时增加AI识别功能,使得只能显示文字的组件可以接收图片拖拽并显示图片中的文字信息。开发者可以将此能力应用于拖拽识图搜索。
将拖拽框架与系统的分屏能力、键鼠穿越能力、小艺及中转站结合,可以实现如下场景:
- 分屏拖拽:演示了分屏拖拽的功能,可以在分屏中打开两个不同的应用,实现跨应用拖拽。
- 跨设备拖拽:演示了基于键鼠穿越能力的跨设备拖拽,可以在平板和PC/2in1设备中使用此功能以直观便捷地交换数据。
- 拖入小艺和中转站:演示了小艺和中转站与拖拽框架结合的能力,可以利用中转站暂存拖拽内容或进行跨设备拖拽,也可以利用小艺的AI对话式分析能力处理拖拽内容。
二、实现原理
拖拽流程可以分为三部分:发起拖拽、拖拽中和释放拖拽。其中,拖出方通过 draggable() 和 onDragStart() 等接口处理拖出数据,拖入方通过allowDrop()和onDrop()等接口处理拖入数据,拖拽数据使用UDMF统一数据对象UnifiedData 进行封装。下面,将按照这三个部分依次介绍拖拽的基础实现。
拖拽流程展示图如下:
1. 发起拖拽
2. 拖拽中
3. 释放拖拽
拖拽图片示例代码
import { SubHeader, TextModifier } from '@kit.ArkUI';
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';
@Component
struct TestDragImage {
@State uri: string = '';
@State a_blockArr: string[] = [];
@State targetImage: string = '';
@State a_visible: Visibility = Visibility.Visible;
@State dragSuccess: Boolean = false;
@State primaryModifier: TextModifier =
new TextModifier().fontColor(Color.Gray).fontSize(18).fontWeight(FontWeight.Medium);
build() {
Column() {
SubHeader({
// 子标题,用于列表项顶部,将该组列表划分为一个区块
primaryTitle: '拖拽图片',
primaryTitleModifier: this.primaryModifier
})
Image($r('app.media.mount'))
.width('342vp')//图片的显示尺寸是否跟随图源尺寸。
// 默认值:false
// 说明:
// 当不设置fitOriginalSize或者设置fitOriginalSize为false时,组件显示大小不跟随图源大小。
// 当设置fitOriginalSize为true时,组件显示大小跟随图源大小。
.fitOriginalSize(true)
.borderRadius(8)
.visibility(this.a_visible)
.draggable(true)
.onDragStart((event: DragEvent) => { // 拖拽中
console.log("开始拖拽图片了 onDragStart");
})
.onDragEnd((event: DragEvent) => {
console.log("开始拖拽图片了 onDragEnd");
let ret = event.getResult();
if (ret === 0) {
console.log("拖拽图片 enter ret === 0");
} else {
console.log("拖拽图片 enter ret != 0");
this.a_visible = Visibility.Visible;
}
})
SubHeader({
// 子标题,用于列表项顶部,将该组列表划分为一个区块
primaryTitle: '允许图片释放区',
primaryTitleModifier: this.primaryModifier
})
Column() {
Image(this.targetImage)
.height('193vp')
.fitOriginalSize(true)
.constraintSize({ maxWidth: '100%' })
.borderRadius(8)
}
.height('193vp')
.width('342vp')
.constraintSize({ maxWidth: '100%' })
.backgroundColor(Color.Gray)
.borderRadius(8)
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop((event?: DragEvent) => {
let dragData: UnifiedData = (event as DragEvent).getData() as UnifiedData;
if (dragData !== undefined) {
let arr: Array<unifiedDataChannel.UnifiedRecord> = dragData.getRecords();
if (arr.length > 0) {
console.log("拖拽图片 dragData 不为空");
this.targetImage = (arr[0] as unifiedDataChannel.Image).imageUri
} else {
console.log("拖拽图片 dragData arr is null ");
}
} else {
console.log("拖拽图片 dragData is undefined");
}
this.dragSuccess = true;
})
}
}
}
@Entry
@Component
struct TestDrag {
@State message: string = '统一拖拽';
build() {
Scroll() {
Column({ space: 10 }) {
Text(this.message)
.id('TestDragHelloWorld')
.fontSize(20)
.fontWeight(FontWeight.Medium)
TestDragImage()
}
}
.height('100%')
.width('100%')
}
}
三、拖拽图像增加水印
在拖拽过程中,可以自定义拖出响应,为拖拽图像增加水印,以标识图像的相关信息。下面以在图像中增加拖拽时间水印为例,介绍实现原理。
3.1 实现原理
在拖出对象的onDragStart()接口中获取图像信息,调用系统绘制能力drawing在图像上绘制水印,通过DragEvent的setData()接口将水印图像设置为拖拽数据。
3.2 开发步骤
- 将Image的draggable()属性设置为true。
- 在拖出对象的onDragStart()接口中,获取图像信息并将其转换成PixelMap。
- 将图片绘制到Canvas画布上,并获取拖拽时间作为水印绘制到画布上的指定位置,得到添加水印的图像。
- 将图像打包保存在文件中,调用DragEvent的setData()接口将水印图像设置为拖拽数据。
3.3 效果图
拖拽中
释放拖拽
拖拽图像增加水印示例代码
import { display, promptAction, SubHeader, TextModifier } from '@kit.ArkUI';
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'
import { image } from '@kit.ImageKit'
import { drawing } from '@kit.ArkGraphics2D'
import { fileIo as fs, fileUri } from '@kit.CoreFileKit'
import { resourceManager } from '@kit.LocalizationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError, systemDateTime } from '@kit.BasicServicesKit';
const TAG = '[统一拖拽]';
@Component
struct TestDragImageWatermarkDrag {
@State targetImage: string = '';
@State primaryModifier: TextModifier =
new TextModifier().fontColor(Color.Gray).fontSize(18).fontWeight(FontWeight.Medium);
context: Context = getContext(this);
time: string = '0';
getTimeWatermark(str: number): string {
let time: string = '';
let date = new Date(str);
try {
let year = date.getFullYear();
let month = (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1);
let day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
let hour = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
let min = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
let second = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
time = year + '-' + month + '-' + day + ' ' + hour + ':' + min + ':' + second;
hilog.info(0x0000, TAG, `${date}transform===>${time}`);
} catch (error) {
hilog.error(0x0000, TAG,
`Failed to get currentTime, code = ${(error as BusinessError).code}, message = ${(error as BusinessError).message}`);
}
return time;
}
getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) {
try {
let data: UnifiedData = event.getData();
if (!data) {
return false;
}
let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
if (!records || records.length <= 0) {
return false;
}
callback(event);
return true;
} catch (error) {
hilog.error(0x0000, TAG,
`getData failed, code = ${(error as BusinessError).code}, message = ${(error as BusinessError).message}`);
return false;
}
}
getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) {
if (this.getDataFromUdmfRetry(event, callback)) {
return;
}
setTimeout(() => {
this.getDataFromUdmfRetry(event, callback);
}, 1500);
}
addWaterMark(watermark: string, pixelMap: image.PixelMap) {
if (canIUse('SystemCapability.Graphics.Drawing')) {
watermark = this.context.resourceManager.getStringSync($r('app.string.drag_time')) + watermark;
let imageWidth = pixelMap.getImageInfoSync().size.width;
let imageHeight = pixelMap.getImageInfoSync().size.height;
let imageScale = imageWidth / display.getDefaultDisplaySync().width;
const canvas = new drawing.Canvas(pixelMap);
const pen = new drawing.Pen();
const brush = new drawing.Brush();
pen.setColor({
alpha: 102,
red: 255,
green: 255,
blue: 255
})
brush.setColor({
alpha: 102,
red: 255,
green: 255,
blue: 255
})
const font = new drawing.Font();
font.setSize(48 * imageScale);
let textWidth = font.measureText(watermark, drawing.TextEncoding.TEXT_ENCODING_UTF8);
const textBlob = drawing.TextBlob.makeFromString(watermark, font, drawing.TextEncoding.TEXT_ENCODING_UTF8);
canvas.attachBrush(brush);
canvas.attachPen(pen);
canvas.drawTextBlob(textBlob, imageWidth - 24 * imageScale - textWidth, imageHeight - 32 * imageScale);
canvas.detachBrush();
canvas.detachPen();
} else {
hilog.info(0x0000, TAG, 'watermark is not supported');
}
return pixelMap;
}
build() {
Column({ space: 10 }) {
SubHeader({
primaryTitle: '拖拽图像增加水印',
primaryTitleModifier: this.primaryModifier
})
Image($rawfile('river.png'))
.width('342vp')
.fitOriginalSize(true)
.borderRadius(8)
.draggable(true)
.onDragStart((event: DragEvent) => {
const resourceMgr: resourceManager.ResourceManager = this.context.resourceManager;
let rawFileDescriptor = resourceMgr.getRawFdSync('river.png');
const imageSourceApi: image.ImageSource = image.createImageSource(rawFileDescriptor);
let pixelMap = imageSourceApi.createPixelMapSync();
this.time = this.getTimeWatermark(systemDateTime.getTime(false));
let markPixelMap = this.addWaterMark(this.time, pixelMap);
let packOpts: image.PackingOption = { format: 'image/png', quality: 20 };
let file =
fs.openSync(`${this.context.filesDir}/watermark.png`, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
const imagePackerApi: image.ImagePacker = image.createImagePacker();
imagePackerApi.packToFile(markPixelMap, file.fd, packOpts);
let img: unifiedDataChannel.Image = new unifiedDataChannel.Image();
img.imageUri = fileUri.getUriFromPath(`${this.context.filesDir}/watermark.png`);
let data: unifiedDataChannel.UnifiedData = new unifiedDataChannel.UnifiedData(img);
(event as DragEvent).setData(data);
fs.closeSync(file.fd);
})
.onDragEnd((event) => {
if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
promptAction.showToast({
duration: 100,
bottom: '80vp',
message: $r('app.string.drag_successfully')
});
} else if (event.getResult() === DragResult.DRAG_FAILED) {
promptAction.showToast({ duration: 100, bottom: '80vp', message: $r('app.string.drag_failed') });
}
})
SubHeader({
primaryTitle: $r('app.string.area_can_drag'),
primaryTitleModifier: this.primaryModifier
})
Column() {
Image(this.targetImage)
.width('342vp')
.fitOriginalSize(true)
.borderRadius(8)
.draggable(true)
}
.alignItems(HorizontalAlign.Center)
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop((dragEvent?: DragEvent) => {
this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {
let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords();
this.targetImage = (records[0] as unifiedDataChannel.Image).imageUri;
event.useCustomDropAnimation = false;
event.setResult(DragResult.DRAG_SUCCESSFUL);
})
})
.backgroundColor(Color.Gray)
.width('342vp')
.constraintSize({ maxWidth: '100%' })
.height('193vp')
.borderRadius(8)
.margin({
left: '16vp',
right: '16vp'
})
}
.height('100%')
}
}
@Entry
@Component
struct TestDrag {
@State message: string = '统一拖拽';
build() {
Scroll() {
Column({ space: 10 }) {
Text(this.message)
.id('TestDragHelloWorld')
.fontSize(20)
.fontWeight(FontWeight.Medium)
// 拖拽图片
// TestDragImage()
//拖拽图片添加水印
TestDragImageWatermarkDrag()
}
}
.height('100%')
.width('100%')
}
}
四、自定义拖拽背板图
在拖拽过程中,可以自定义拖拽背板图,展示拖拽数据的相关信息。
4.1 实现原理
在拖出对象的onDragStart()接口中,回调自定义的PixelMap作为拖拽中的背板图。
4.2 开发步骤
- 创建自定义组件。
- 将自定义组件转换成PixelMap,作为拖拽过程中显示的图片。
说明
由于CustomBuilder需要离线渲染之后才能使用,存在一定的性能开销和时延,因此推荐开发者优先使用DragItemInfo中的PixelMap方式返回背板图。- 在拖出对象的onDragStart()接口中,将回调的PixelMap作为拖拽中的背板图。
4.3 效果图
拖拽中
释放拖拽,拖拽成功
自定义拖拽背板图示例代码
import { display, promptAction, SubHeader, TextModifier } from '@kit.ArkUI';
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'
import { image } from '@kit.ImageKit'
import { drawing } from '@kit.ArkGraphics2D'
import { fileIo as fs, fileUri } from '@kit.CoreFileKit'
import { resourceManager } from '@kit.LocalizationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError, systemDateTime } from '@kit.BasicServicesKit';
const TAG = '[统一拖拽]';
@Component
struct TestBackgroundDrag {
@State targetImage: string = '';
@State primaryModifier: TextModifier =
new TextModifier().fontColor(Color.Gray).fontSize(18).fontWeight(FontWeight.Medium);
@State pixelMap: image.PixelMap | undefined = undefined;
@Builder
pixelMapBuilder() {
Column() {
Text($r('app.string.background_content'))
.fontSize('16fp')
.fontColor(Color.Black)
.margin({
left: '16vp',
right: '16vp',
top: '8vp',
bottom: '8vp'
})
}
.backgroundColor(Color.White)
.borderRadius(18)
}
private getComponentSnapshot(): void {
this.getUIContext().getComponentSnapshot().createFromBuilder(() => {
this.pixelMapBuilder()
},
(error: Error, pixmap: image.PixelMap) => {
if (error) {
hilog.error(0x0000, TAG, JSON.stringify(error));
return;
}
this.pixelMap = pixmap;
})
}
private PreDragChange(preDragStatus: PreDragStatus): void {
if (preDragStatus == PreDragStatus.ACTION_DETECTING_STATUS) {
this.getComponentSnapshot();
}
}
build() {
Column({ space: 10 }) {
SubHeader({
primaryTitle: '自定义拖拽背板图',
primaryTitleModifier: this.primaryModifier
})
Image($r('app.media.mount'))
.width('342vp')
.fitOriginalSize(true)
.borderRadius(8)
.draggable(true)
.onPreDrag((status: PreDragStatus) => {
this.PreDragChange(status);
})
.onDragStart(() => {
let dragItemInfo: DragItemInfo = {
pixelMap: this.pixelMap,
builder: () => {
this.pixelMapBuilder()
},
extraInfo: "this is extraInfo"
};
return dragItemInfo;
})
.onDragEnd((event) => {
if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
promptAction.showToast({
duration: 100,
bottom: '80vp',
message: $r('app.string.drag_successfully')
});
} else if (event.getResult() === DragResult.DRAG_FAILED) {
promptAction.showToast({ duration: 100, bottom: '80vp', message: $r('app.string.drag_failed') });
}
})
SubHeader({
primaryTitle: $r('app.string.area_can_drag'),
primaryTitleModifier: this.primaryModifier
})
Column() {
Image(this.targetImage)
.height('193vp')
.fitOriginalSize(true)
.constraintSize({ maxWidth: '100%' })
.borderRadius(8)
}
.backgroundColor(Color.Grey)
.height('193vp')
.width('342vp')
.constraintSize({ maxWidth: '100%' })
.borderRadius(8)
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop((event?: DragEvent) => {
let dragData: UnifiedData = (event as DragEvent).getData() as UnifiedData;
if (dragData !== undefined) {
let arr: Array<unifiedDataChannel.UnifiedRecord> = dragData.getRecords();
if (arr.length > 0) {
this.targetImage = (arr[0] as unifiedDataChannel.Image).imageUri;
} else {
hilog.info(0x0000, TAG, 'dragData arr is null');
}
} else {
hilog.info(0x0000, TAG, 'dragData is undefined');
}
event?.setResult(DragResult.DRAG_SUCCESSFUL);
})
.margin({
left: '16vp',
right: '16vp'
})
}
.height('100%')
}
}
@Entry
@Component
struct TestDrag {
@State message: string = '统一拖拽';
build() {
Scroll() {
Column({ space: 10 }) {
Text(this.message)
.id('TestDragHelloWorld')
.fontSize(20)
.fontWeight(FontWeight.Medium)
//拖拽图片
// TestDragImage()
// 拖拽图片添加水印
// TestDragImageWatermarkDrag()
// 自定义拖拽背板图
TestBackgroundDrag()
}
}
.height('100%')
.width('100%')
}
}
五、AI识别拖拽内容
在拖拽过程中,可以自定义拖入响应,以识别拖拽内容并将其输出在释放区内。下面以通过AI识别拖拽图像中的文字为例,介绍实现原理。
5.1 实现原理
在拖入对象的onDrop()接口中,通过DragEvent的getData()接口获取拖拽数据后,调用系统文字识别能力textRecognition得到图像中的文字信息。
5.2 开发步骤
- 在拖拽释放区域的allowDrop()接口中设置允许拖入的数据类型为uniformTypeDescriptor.UniformDataType.IMAGE。
- 在拖入对象的onDrop()接口中,调用DragEvent的getData()接口获取拖拽数据。
- 将拖拽数据转换成颜色数据格式为RGBA_8888的PixelMap类型的视觉信息。
- 调用系统文字识别能力textRecognition获取拖拽数据中的文字信息。
5.3 效果图
拖拽中
释放拖拽,识别成功
AI识别拖拽内容示例代码
import { display, promptAction, SubHeader, TextModifier } from '@kit.ArkUI';
import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'
import { image } from '@kit.ImageKit'
import { drawing } from '@kit.ArkGraphics2D'
import { fileIo as fs, fileUri } from '@kit.CoreFileKit'
import { resourceManager } from '@kit.LocalizationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError, systemDateTime } from '@kit.BasicServicesKit';
import { textRecognition } from '@kit.CoreVisionKit';
const TAG = '[统一拖拽]';
@Component
struct TestAIRecognitionDrag {
@State textContent: string = '';
@State primaryModifier: TextModifier =
new TextModifier().fontColor(Color.Gray).fontSize(18).fontWeight(FontWeight.Medium);
context: Context = getContext(this);
build() {
Column({ space: 10 }) {
SubHeader({
primaryTitle: 'AI识别拖拽内容',
primaryTitleModifier: this.primaryModifier
})
Image($r('app.media.architecture'))
.width('355vp')
.fitOriginalSize(true)
.borderRadius(8)
.draggable(true)
.onDragEnd((event) => {
if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
promptAction.showToast({
duration: 100,
bottom: '80vp',
message: $r('app.string.drag_successfully')
});
} else if (event.getResult() === DragResult.DRAG_FAILED) {
promptAction.showToast({ duration: 100, bottom: '80vp', message: $r('app.string.drag_failed') });
}
})
SubHeader({
primaryTitle: $r('app.string.area_can_drag'),
primaryTitleModifier: this.primaryModifier
})
Column() {
Text(this.textContent)
.width('100%')
.height('175vp')
.lineHeight(22)
.borderRadius(8)
.align(Alignment.TopStart)
}
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop(async (event?: DragEvent) => {
let dragData: UnifiedData = (event as DragEvent).getData() as UnifiedData;
if (dragData === undefined) {
hilog.info(0x0000, TAG, 'ondrop undefined data');
return;
}
let record: Array<unifiedDataChannel.UnifiedRecord> = dragData.getRecords();
if (record.length <= 0) {
hilog.info(0x0000, TAG, 'dragData arr is null');
return;
}
let imageSource = record[0] as unifiedDataChannel.Image;
const resourceReg = new RegExp('resource');
if (resourceReg.test(imageSource.uri)) {
const numberReg = new RegExp('[0-9]+');
let idArray = imageSource.uri.match(numberReg);
if (idArray !== null) {
let id = idArray[0];
let drawableDescriptor = this.context.resourceManager.getDrawableDescriptor(Number(id), 0, 1);
let pixelMapInit = drawableDescriptor.getPixelMap() as image.PixelMap;
let imageHeight = pixelMapInit.getImageInfoSync().size.height;
let imageWidth = pixelMapInit.getImageInfoSync().size.width;
const readBuffer: ArrayBuffer = new ArrayBuffer(imageHeight * imageWidth * 4);
pixelMapInit.readPixelsToBufferSync(readBuffer);
let opts: image.InitializationOptions = {
editable: true,
size: { height: imageHeight, width: imageWidth },
srcPixelFormat: pixelMapInit.getImageInfoSync().pixelFormat,
pixelFormat: 3,
alphaType: pixelMapInit.getImageInfoSync().alphaType,
scaleMode: 0
};
let pixelMap: image.PixelMap = image.createPixelMapSync(readBuffer, opts);
let visionInfo: textRecognition.VisionInfo = {
pixelMap: pixelMap
};
let data = await textRecognition.recognizeText(visionInfo);
let recognitionString = data.value;
this.textContent = recognitionString;
}
}
})
.backgroundColor(Color.Grey)
.borderRadius(8)
.padding({
left: '16vp',
right: '16vp',
top: '8vp',
bottom: '8vp'
})
.margin({
left: '16vp',
right: '16vp'
})
}
.height('100%')
}
}
@Entry
@Component
struct TestDrag {
@State message: string = '统一拖拽';
build() {
Scroll() {
Column({ space: 10 }) {
Text(this.message)
.id('TestDragHelloWorld')
.fontSize(20)
.fontWeight(FontWeight.Medium)
//拖拽图片
// TestDragImage()
// 拖拽图片添加水印
// TestDragImageWatermarkDrag()
// 自定义拖拽背板图
// TestBackgroundDrag()
//AI识别拖拽内容
TestAIRecognitionDrag()
}
}
.height('100%')
.width('100%')
}
}