本文将深入解析 HarmonyOS NEXT 中 DragDrop 拖拽放置布局的实现原理,涵盖 24 个核心 API,配合完整的实战代码,帮助开发者从零掌握拖拽交互的开发技巧。


项目演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

目录

  1. DragDrop 技术概述
  2. 开发环境准备
  3. 核心装饰器 API 详解
  4. UI 布局组件 API
  5. 拖拽事件系统 API
  6. 数据交互 API
  7. 样式与资源 API
  8. 完整实战项目
  9. 双端适配策略
  10. 进阶技巧与性能优化
  11. 常见问题解答
  12. 总结与展望

1. DragDrop 技术概述

1.1 什么是 DragDrop?

DragDrop(拖拽放置)是现代移动应用和 Web 应用中广泛使用的交互模式。它允许用户通过长按或拖拽手势,将一个元素从源位置移动到目标位置,并触发相应的业务逻辑。

在 HarmonyOS NEXT 中,DragDrop 提供了一套完整的 API 体系,支持跨设备的统一交互体验。

1.2 应用场景

场景 说明
应用分类整理 将应用图标拖拽到文件夹中
购物车操作 从商品列表拖拽添加到购物车
任务管理 在看板视图中拖拽切换任务状态
图片排序 拖拽调整图片展示顺序
文件管理 拖拽文件到目标文件夹

1.3 技术架构

┌─────────────────────────────────────────────────┐
│                  用户手势层                       │
│  长按触发 → 拖拽移动 → 释放                      │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│              DragDrop API 层                     │
│  .draggable()  .onDragStart()  .onDragEnd()     │
│  .onDragEnter() .onDragMove()  .onDragLeave()   │
│  .onDrop()                                       │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│              数据传输层                          │
│  UnifiedData → UnifiedRecord → PlainText        │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│              UI 更新层                           │
│  @State 响应式驱动 UI 自动刷新                   │
└─────────────────────────────────────────────────┘

1.4 核心概念

拖拽源(Drag Source):用户长按启动拖拽的组件。

放置目标(Drop Target):接收拖拽数据的目标组件区域。

拖拽预览(Drag Preview):跟随用户手指移动的视觉反馈组件。

拖拽数据(Drag Data):通过 UnifiedData 在源和目标之间传递的数据对象。


2. 开发环境准备

2.1 前置条件

项目 要求
操作系统 Windows 10/11 或 macOS 12+
开发工具 DevEco Studio 4.0 Release+
HarmonyOS SDK API 12+
设备 真机或模拟器(推荐触屏设备)

2.2 创建项目

打开 DevEco Studio,选择「Create Project」 → 「Application」 → 「Empty Ability」。

2.3 项目结构

MyApplication71/
├── entry/
│   ├── src/
│   │   └── main/
│   │       ├── ets/
│   │       │   ├── entryability/
│   │       │   │   └── EntryAbility.ets
│   │       │   └── pages/
│   │       │       └── Index.ets          # 主页面
│   │       └── resources/
│   │           ├── base/
│   │           │   ├── element/
│   │           │   │   ├── color.json      # 颜色资源
│   │           │   │   └── string.json     # 字符串资源
│   │           │   └── media/
│   │           │       └── icon.png
│   │           └── dark/                   # 深色主题
│   │               └── element/
│   │                   └── color.json
│   └── module.json5                        # 模块配置
└── build-profile.json5

3. 核心装饰器 API 详解

装饰器是 ArkTS 的核心特性,用于声明组件的类型和行为。本节将解析 DragDrop 开发中必须掌握的 8 个装饰器 API

3.1 @Entry — 入口组件装饰器

功能说明:标记应用的入口页面组件,每个 Ability 必须包含且仅包含一个 @Entry 装饰的组件。

语法格式

@Entry
@Component
struct Index {
  // 组件实现
}

使用场景

  • 应用主页面
  • 独立功能页面
  • 需要路由跳转的目标页面

注意事项

  • @Entry 必须与 @Component 配合使用
  • 组件内必须实现 build() 方法
  • 组件内可以使用 @State 声明状态变量

3.2 @Component — 组件装饰器

功能说明:定义一个可复用的 UI 组件,组件可以拥有自己的状态和生命周期。

语法格式

@Component
struct MyComponent {
  @State count: number = 0;

  build() {
    Column() {
      Text(`计数: ${this.count}`)
      Button('增加')
        .onClick(() => {
          this.count++;
        })
    }
  }
}

组件的特性

  • 封装性:组件内部状态对外不可见
  • 复用性:可在多个页面中引用
  • 独立性:拥有独立的生命周期

3.3 @State — 状态管理装饰器

功能说明:声明组件内部的响应式状态变量,当状态改变时,ArkUI 会自动重新渲染关联的组件。

语法格式

@State variableName: Type = initialValue;

在 DragDrop 中的应用

@Component
struct DragDropExample {
  // 拖拽状态:空闲/拖拽中/完成
  @State dragStatus: string = 'idle';
  
  // 目标区域背景色(响应式改变)
  @State targetBgColor: string = '#FFF3E0';
  
  // 已放置物品列表
  @State droppedItems: string[] = [];
  
  // 当前选中/拖拽的物品
  @State currentItem: DragItemData | null = null;
}

状态更新触发 UI 刷新的流程

状态改变 → ArkUI 检测 → 标记脏组件 → 下一帧重新渲染

最佳实践

  • 状态变量初始值必须指定
  • 避免在 build() 方法中直接修改状态
  • 复杂对象建议使用 @Observed + @ObjectLink

3.4 @Builder — UI 构建器装饰器

功能说明:定义可复用的 UI 片段,支持参数传递,类似于函数式组件。

语法格式

@Builder
BuilderName(params: ParamType) {
  // UI 描述
}

在 DragDrop 中的应用场景

// 拖拽预览构建器
@Builder
DragPreviewBuilder(item: DragItemData) {
  Row() {
    Text(item.displayName)
      .fontSize(18)
      .fontColor(Color.White)
      .fontWeight(FontWeight.Bold)
  }
  .width(120)
  .height(60)
  .justifyContent(FlexAlign.Center)
  .backgroundColor($r('app.color.title_background'))
  .borderRadius(12)
  .shadow({
    radius: 8,
    color: '#40000000',
    offsetX: 0,
    offsetY: 4
  })
}

// 空目标区域构建器
@Builder
EmptyTargetBuilder() {
  Column() {
    Text($r('app.string.drop_hint'))
      .fontSize(16)
      .fontColor($r('app.color.hint_text'))
      .textAlign(TextAlign.Center)
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}

调用方式

// 在其他 Builder 中调用
@Builder
MainBuilder() {
  Column() {
    // 调用带参数的 Builder
    this.DragPreviewBuilder(currentItem);
    
    // 调用无参数的 Builder
    if (droppedItems.length === 0) {
      this.EmptyTargetBuilder();
    }
  }
}

注意事项

  • Builder 内不能使用 return 语句
  • Builder 内不能使用箭头函数
  • 参数传递遵循值语义

3.5 struct — 结构体定义

功能说明:定义自定义类型,用于组织和封装相关数据。在 DragDrop 中常用于定义数据模型。

语法格式

interface DragItemData {
  id: string;
  name: Resource;
  displayName: string;
  icon?: Resource;
}

完整示例

// 拖拽物品数据结构
interface DragItemData {
  id: string;
  name: Resource;
  displayName: string;
  category: string;
  sortOrder: number;
}

// 拖拽事件处理返回类型
interface DropResult {
  success: boolean;
  message: string;
  droppedItem?: DragItemData;
}

3.6 import — 模块导入

功能说明:导入其他模块的导出内容,包括类型定义、工具类、常量等。

在 DragDrop 开发中常用的导入

// 数据交互相关
import { unifiedDataChannel, uniformDataStruct, uniformTypeDescriptor } from '@kit.ArkData';

// UI 提示组件
import { promptAction } from '@kit.ArkUI';

// 错误处理
import { BusinessError } from '@kit.BasicServicesKit';

导入说明

  • @kit.ArkData:提供统一数据通道 API
  • @kit.ArkUI:提供 UI 组件和交互 API
  • @kit.BasicServicesKit:提供基础服务 API

3.7 私有成员修饰符

功能说明:控制类/结构体成员的访问权限。

@Component
struct DragDropExample {
  // 公开成员:外部可访问
  public componentTitle: string = '拖拽示例';
  
  // 私有成员:仅内部可访问
  private dragItems: DragItemData[] = [
    { id: '1', name: $r('app.string.item_1'), displayName: '物品一' }
  ];
  
  // 受保护成员:子类可访问
  protected defaultItem: DragItemData = {
    id: 'default', name: $r('app.string.default'), displayName: '默认'
  };
}

3.8 @ObjectLink — 对象状态同步

功能说明:在嵌套组件间同步对象状态,用于复杂数据场景。

// 定义可观察对象
@Observed
class DragModel {
  items: DragItemData[] = [];
  isDragging: boolean = false;
}

// 使用 @ObjectLink 同步
@Component
struct ChildComponent {
  @ObjectLink model: DragModel;
  
  build() {
    Text(`拖拽状态: ${this.model.isDragging}`)
  }
}

4. UI 布局组件 API

DragDrop 的 UI 结构通常包含源区域目标区域,本节介绍构建这种布局所需的核心组件 API。

4.1 Column — 垂直布局容器

功能说明:将子组件按垂直方向依次排列。

基本用法

Column() {
  Text('第一行')
  Text('第二行')
  Text('第三行')
}
.width('100%')
.height(300)

在 DragDrop 中的应用

// 源区域容器
Column({ space: 12 }) {
  // 标题
  Text('可拖拽物品')
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
    .margin({ bottom: 8 })
  
  // 物品列表
  ForEach(dragItems, (item: DragItemData) => {
    this.DragItemRow(item);
  }, (item: DragItemData) => item.id)
}
.width('45%')
.padding(16)
.backgroundColor($r('app.color.source_background'))
.borderRadius(12)

常用属性

  • justifyContent:主轴对齐方式
  • alignItems:交叉轴对齐方式
  • space:子组件间距

4.2 Row — 水平布局容器

功能说明:将子组件按水平方向依次排列。

基本用法

Row() {
  Button('左')
  Button('中')
  Button('右')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)

在 DragDrop 中的应用

// 主布局:左右分栏
Row() {
  // 左侧源区域
  Column() { /* ... */ }
  .width('45%')
  
  // 右侧目标区域
  Column() { /* ... */ }
  .layoutWeight(1)  // 占剩余空间
}
.width('100%')
.height('100%')
.padding(16)

常用属性

  • width:宽度(支持百分比)
  • height:高度(支持百分比)
  • layoutWeight:权重,用于弹性布局
  • padding:内边距
  • margin:外边距

4.3 Text — 文本组件

功能说明:展示纯文本内容,支持资源引用。

基本用法

Text('Hello World')            // 硬编码字符串
Text($r('app.string.title'))   // 资源引用
Text('动态内容: ' + count)      // 字符串拼接

常用属性

Text('示例文本')
  .fontSize(16)                          // 字体大小
  .fontColor($r('app.color.text'))      // 字体颜色
  .fontWeight(FontWeight.Bold)          // 字体粗细
  .fontFamily('HarmonyOS Sans')          // 字体族
  .fontStyle(FontStyle.Italic)           // 字体样式
  .lineHeight(24)                        // 行高
  .maxLines(2)                           // 最大行数
  .textOverflow({ overflow: TextOverflow.Ellipsis })  // 溢出处理
  .textAlign(TextAlign.Center)           // 对齐方式

在 DragDrop 中的应用

// 状态提示文本
Text(dragStatusText)
  .fontSize(14)
  .fontColor(getStatusColor())
  .fontWeight(FontWeight.Medium)
  .padding({ left: 16, right: 16, top: 8, bottom: 8 })
  .backgroundColor(getStatusBgColor())
  .borderRadius(20)

// 拖拽物品名称
Text(item.displayName)
  .fontSize(16)
  .fontColor($r('app.color.item_text'))
  .fontWeight(FontWeight.Medium)

4.4 Button — 按钮组件

功能说明:可交互的按钮,支持点击事件。

基本用法

Button('点击我')
  .onClick(() => {
    console.log('Button clicked');
  })

// 使用资源引用
Button($r('app.string.clear_button'))

在 DragDrop 中的应用

// 清空按钮(仅在有已放置物品时显示)
if (droppedItems.length > 0) {
  Button($r('app.string.clear_button'))
    .width('60%')
    .height(48)
    .fontSize(16)
    .fontColor($r('app.color.button_text'))
    .backgroundColor($r('app.color.button_bg'))
    .borderRadius(24)
    .margin({ top: 16, bottom: 24 })
    .onClick(() => {
      this.clearDroppedItems();
    })
}

4.5 ForEach — 列表渲染

功能说明:遍历数组并渲染列表组件,自动处理更新和复用。

语法格式

ForEach(
  array,              // 数据源数组
  (item, index) => {  // 渲染函数
    // 返回组件
  },
  (item, index) => {  // 键生成函数(可选,推荐)
    // 返回唯一标识
  }
)

在 DragDrop 中的完整用法

ForEach(this.dragItems, (item: DragItemData) => {
  Row() {
    Text(item.name)
      .fontSize(18)
      .fontColor($r('app.color.item_text'))
    
    if (this.selectedItem !== null && this.selectedItem.id === item.id) {
      Text(' ✓')
        .fontSize(16)
        .fontColor($r('app.color.success'))
    }
  }
  .width('100%')
  .height(50)
  .justifyContent(FlexAlign.Center)
  .backgroundColor(this.getItemBgColor(item))
  .borderRadius(8)
  .draggable(true)
  .onDragStart((event: DragEvent) => {
    return this.handleDragStart(item, event);
  })
  .onDragEnd((event: DragEvent) => {
    this.handleDragEnd(event);
  })
  .onClick(() => {
    this.selectItem(item);
  })
}, (item: DragItemData) => item.id)  // 唯一键

注意事项

  • 键生成函数必须返回唯一标识
  • 避免使用数组索引作为键(会导致复用问题)
  • 数组变化时自动刷新 UI

4.6 Flex — 弹性布局

功能说明:类似于 CSS Flexbox 的弹性布局容器。

基本用法

Flex({ wrap: FlexWrap.Wrap }) {
  // 子组件自动换行
  Text('A')
  Text('B')
  Text('C')
}
.width('100%')

在 DragDrop 中的应用

// 已放置物品标签展示
Flex({ wrap: FlexWrap.Wrap }) {
  ForEach(this.droppedItems, (item: string, index: number) => {
    Text(item)
      .fontSize(14)
      .fontColor($r('app.color.item_text'))
      .padding({ left: 12, right: 12, top: 6, bottom: 6 })
      .margin({ right: 8, bottom: 8 })
      .backgroundColor($r('app.color.tag_bg'))
      .borderRadius(16)
  }, (item: string, index: number) => index.toString())
}
.width('100%')

4.7 layoutWeight — 布局权重

功能说明:在父容器剩余空间中按比例分配空间。

用法

Row() {
  Column() { /* 内容 */ }
    .width('40%')      // 固定 40%
  
  Column() { /* 内容 */ }
    .layoutWeight(1)   // 占剩余 60%
  
  Column() { /* 内容 */ }
    .layoutWeight(2)   // 如果有剩余,再按 2:1 分配
}
.width('100%')

在 DragDrop 中的应用

Row() {
  // 左侧源区域:固定 45%
  Column() { /* 物品列表 */ }
    .width('45%')
  
  // 右侧目标区域:剩余空间
  Column() { /* 放置区域 */ }
    .layoutWeight(1)
}
.width('100%')
.height('100%')

4.8 padding / margin — 内边距与外边距

功能说明:控制组件内部和外部的间距。

基本用法

.padding(16)                    // 四周统一 16
.padding({ left: 16, right: 16 })  // 左右各 16
.padding({ top: 8, bottom: 16 })   // 上下不同
.padding({ left: 8, right: 8, top: 16, bottom: 16 })

.margin(16)                      // 四周统一
.margin({ top: 8 })              // 仅上方

5. 拖拽事件系统 API

这是 DragDrop 的核心 API 集合,共 8 个事件和方法

5.1 .draggable() — 启用拖拽能力

功能说明:标记组件为可拖拽元素,启用后用户长按可启动拖拽。

语法格式

.draggable(enabled: boolean)

基本用法

Row() {
  Text('拖拽我')
}
.draggable(true)   // 启用拖拽

完整示例

Row() {
  Text(item.displayName)
    .fontSize(18)
    .fontColor($r('app.color.item_text'))
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.backgroundColor($r('app.color.item_bg'))
.borderRadius(8)

// ===== 核心:启用拖拽 =====
.draggable(true)

// 配合事件监听
.onDragStart((event: DragEvent) => {
  return this.handleDragStart(item, event);
})
.onDragEnd((event: DragEvent) => {
  this.handleDragEnd(event);
})

注意事项

  • true 启用拖拽,false 禁用
  • 可在运行时动态切换
  • 必须配合 onDragStart 使用才有完整功能

5.2 .onDragStart() — 拖拽开始事件

功能说明:用户长按启动拖拽时触发,用于初始化拖拽状态、设置数据、返回预览。

语法格式

.onDragStart(callback: (event: DragEvent) => DragItemInfo)

参数说明

  • event:拖拽事件对象,包含坐标、时间戳等信息
  • 返回值 DragItemInfo:包含预览组件和额外信息

完整实现

handleDragStart(item: DragItemData, event: DragEvent): DragItemInfo {
  // 1. 更新状态
  this.currentDragItem = item;
  this.dragStatus = $r('app.string.dragging');
  
  // 2. 设置拖拽数据(跨组件传递)
  try {
    // 创建数据对象
    let plainText: uniformDataStruct.PlainText = {
      uniformDataType: 'general.plain-text',
      textContent: item.displayName
    };
    
    // 创建记录
    let record = new unifiedDataChannel.UnifiedRecord(
      uniformTypeDescriptor.UnifiedDataType.PLAIN_TEXT,
      plainText
    );
    
    // 创建统一数据
    let unifiedData = new unifiedDataChannel.UnifiedData(record);
    
    // 设置到事件中
    event.setData(unifiedData);
    
  } catch (e) {
    let error: BusinessError = e as BusinessError;
    console.error('setData failed: ' + error.message);
  }
  
  // 3. 返回预览组件和额外信息
  return {
    builder: this.DragPreviewBuilder(item),  // 预览 UI
    extraInfo: item.id                        // 额外数据
  };
}

返回值类型 DragItemInfo

interface DragItemInfo {
  builder: CustomBuilder;      // 预览组件构建器
  extraInfo?: string;          // 额外信息字符串
}

5.3 .onDragEnd() — 拖拽结束事件

功能说明:用户释放手指/鼠标时触发,无论成功放置还是取消都会触发。

语法格式

.onDragEnd(callback: (event: DragEvent) => void)

完整实现

handleDragEnd(event: DragEvent): void {
  // 获取拖拽结果
  let result = event.getResult();
  
  // 根据结果处理
  if (result === DragResult.DRAG_SUCCESSFUL) {
    // 成功放置
    this.dragStatus = $r('app.string.drop_success');
    promptAction.showToast({ message: '放置成功!' });
    
  } else if (result === DragResult.DRAG_FAILED) {
    // 放置失败
    this.dragStatus = $r('app.string.drop_failed');
    promptAction.showToast({ message: '放置失败' });
    
  } else {
    // 用户取消(拖拽到有效区域外释放)
    this.dragStatus = $r('app.string.idle');
  }
  
  // 清理状态
  this.currentDragItem = null;
  this.isInTargetArea = false;
  
  // 恢复目标区域样式
  this.targetBgColor = $r('app.color.target_bg');
  this.targetBorderColor = $r('app.color.border');
}

DragResult 枚举值

说明
DRAG_SUCCESSFUL 成功放置到有效目标区域
DRAG_FAILED 放置到无效区域或出错
其他 用户取消操作

5.4 .onDragEnter() — 进入目标区域事件

功能说明:拖拽元素首次进入目标区域时触发,用于视觉反馈。

语法格式

.onDragEnter(callback: (event: DragEvent) => void)

完整实现

handleDragEnter(event: DragEvent): void {
  // 更新进入状态
  this.isInTargetArea = true;
  this.dragStatus = $r('app.string.enter_target');
  
  // 视觉反馈:高亮目标区域
  this.targetBgColor = $r('app.color.target_hover');
  this.targetBorderColor = $r('app.color.border_active');
  
  // 震动反馈(可选)
  // vibrator.vibrate(VibratorEffect.SHORT);
}

视觉反馈效果

[正常状态]          [进入状态]
┌──────────┐        ╔══════════╗
│          │   →    ║ ▓▓▓▓▓▓▓▓ ║
│ 放置区域 │        ║ ▓ 高亮 ▓ ║
│          │        ║ ▓▓▓▓▓▓▓▓ ║
└──────────┘        ╚══════════╝
灰色边框             橙色边框+深色背景

5.5 .onDragLeave() — 离开目标区域事件

功能说明:拖拽元素离开目标区域时触发,用于恢复视觉状态。

语法格式

.onDragLeave(callback: (event: DragEvent) => void)

完整实现

handleDragLeave(event: DragEvent): void {
  // 更新离开状态
  this.isInTargetArea = false;
  this.dragStatus = $r('app.string.leave_target');
  
  // 恢复目标区域样式
  this.targetBgColor = $r('app.color.target_bg');
  this.targetBorderColor = $r('app.color.border');
}

5.6 .onDragMove() — 在目标区域内移动

功能说明:拖拽过程中在目标区域内移动时持续触发,用于实时更新。

语法格式

.onDragMove(callback: (event: DragEvent) => void)

基本实现

handleDragMove(event: DragEvent): void {
  // 允许放置
  event.setResult(DragResult.DROP_ENABLED);
  
  // 可选:获取当前坐标
  let x = event.getX();
  let y = event.getY();
  console.log(`Drag position: (${x}, ${y})`);
}

高级用法 — 动态调整放置区域

handleDragMove(event: DragEvent): void {
  event.setResult(DragResult.DROP_ENABLED);
  
  // 根据拖拽位置调整 UI
  let y = event.getY();
  
  if (y < 100) {
    // 上半区域:显示 "置顶" 提示
    this.hintText = '放置到顶部';
  } else {
    // 下半区域:显示 "到底部" 提示
    this.hintText = '放置到末尾';
  }
}

5.7 .onDrop() — 释放到目标区域(核心)

功能说明:用户在目标区域释放时触发,执行实际的数据处理逻辑。

语法格式

.onDrop(callback: (event: DragEvent) => void)

完整实现

handleDrop(event: DragEvent): void {
  try {
    // 1. 获取拖拽数据
    let data = event.getData();
    if (!data) {
      console.error('No drag data available');
      event.setResult(DragResult.DRAG_FAILED);
      return;
    }
    
    // 2. 解析数据
    let records = data.getRecords();
    if (!records || records.length === 0) {
      console.error('No records in data');
      event.setResult(DragResult.DRAG_FAILED);
      return;
    }
    
    // 3. 获取当前拖拽的物品
    let itemName: string = '';
    if (this.currentDragItem !== null) {
      itemName = this.currentDragItem.displayName;
    }
    
    // 4. 执行业务逻辑:添加到已放置列表
    if (itemName !== '') {
      this.droppedItems.push(itemName);
      
      // 5. 设置拖拽结果
      event.setResult(DragResult.DRAG_SUCCESSFUL);
      
      // 6. 反馈提示
      promptAction.showToast({
        message: '已添加: ' + itemName,
        duration: 1500
      });
      
    } else {
      event.setResult(DragResult.DRAG_FAILED);
    }
    
  } catch (e) {
    let error: BusinessError = e as BusinessError;
    console.error('Drop error: code=' + error.code + ', msg=' + error.message);
    event.setResult(DragResult.DRAG_FAILED);
  }
  
  // 7. 清理状态
  this.isInTargetArea = false;
  this.targetBgColor = $r('app.color.target_bg');
  this.targetBorderColor = $r('app.color.border');
}

数据解析流程

UnifiedData → getRecords() → UnifiedRecord[]
  ↓
UnifiedRecord → 按类型获取内容
  ↓
PlainText → textContent(实际数据)

5.8 .onClick() — 点击事件(备选方案)

功能说明:处理点击事件,作为电脑端无法长按拖拽时的备选交互方式。

语法格式

.onClick(callback: () => void)

在 DragDrop 备选方案中的应用

// 点击源物品 → 选中状态
.onClick(() => {
  if (this.selectedItem !== null && this.selectedItem.id === item.id) {
    // 取消选中
    this.selectedItem = null;
    this.isSelectMode = false;
    this.dragStatus = $r('app.string.idle');
    this.targetBgColor = $r('app.color.target_bg');
  } else {
    // 选中物品
    this.selectedItem = item;
    this.isSelectMode = true;
    this.dragStatus = $r('app.string.selected');
    this.targetBgColor = $r('app.color.target_hover');
    
    promptAction.showToast({
      message: '已选中: ' + item.displayName,
      duration: 1500
    });
  }
})

// 点击目标区域 → 执行放置
.onClick(() => {
  if (this.selectedItem !== null) {
    this.droppedItems.push(this.selectedItem.displayName);
    this.dragStatus = $r('app.string.drop_success');
    
    promptAction.showToast({
      message: '已添加: ' + this.selectedItem.displayName,
      duration: 1500
    });
    
    // 重置状态
    this.selectedItem = null;
    this.isSelectMode = false;
    this.targetBgColor = $r('app.color.target_bg');
  }
})

6. 数据交互 API

6.1 UnifiedData — 统一数据对象

功能说明:跨组件拖拽时传递数据的容器类。

创建方式

// 方式一:从 Record 创建
let record = new unifiedDataChannel.UnifiedRecord(
  uniformTypeDescriptor.UnifiedDataType.PLAIN_TEXT,
  plainTextContent
);
let unifiedData = new unifiedDataChannel.UnifiedData(record);

// 方式二:从多个 Record 创建
let records = [
  new unifiedDataChannel.UnifiedRecord(
    uniformTypeDescriptor.UnifiedDataType.PLAIN_TEXT,
    textContent
  ),
  new unifiedDataChannel.UnifiedRecord(
    uniformTypeDescriptor.UnifiedDataType.IMAGE,
    imageContent
  )
];
let unifiedData = new unifiedDataChannel.UnifiedData(records);

6.2 UnifiedRecord — 数据记录

功能说明:单一数据项的封装,包含类型和内容。

支持的数据类型

类型 常量值 内容格式
纯文本 PLAIN_TEXT PlainText 对象
HTML HTML 字符串
URI URI 字符串数组
图片 IMAGE 文件描述符
文件 FILE 文件描述符

代码示例

// 创建纯文本记录
let plainText: uniformDataStruct.PlainText = {
  uniformDataType: 'general.plain-text',
  textContent: 'Hello DragDrop'
};

let record = new unifiedDataChannel.UnifiedRecord(
  uniformTypeDescriptor.UnifiedDataType.PLAIN_TEXT,
  plainText
);

// 获取记录内容
let retrievedRecord = unifiedData.getRecords()[0];
let content = retrievedRecord.getPlainText();

6.3 event.setData() — 设置拖拽数据

功能说明:在 onDragStart 中将数据绑定到拖拽事件。

语法格式

event.setData(data: UnifiedData): void

完整流程

.onDragStart((event: DragEvent) => {
  // 1. 创建数据
  let plainText: uniformDataStruct.PlainText = {
    uniformDataType: 'general.plain-text',
    textContent: item.displayName
  };
  let record = new unifiedDataChannel.UnifiedRecord(
    uniformTypeDescriptor.UnifiedDataType.PLAIN_TEXT,
    plainText
  );
  let unifiedData = new unifiedDataChannel.UnifiedData(record);
  
  // 2. 绑定到事件
  event.setData(unifiedData);
  
  // 3. 返回预览
  return {
    builder: this.DragPreviewBuilder(item)
  };
})

6.4 event.getData() — 获取拖拽数据

功能说明:在 onDrop 中获取拖拽传递过来的数据。

语法格式

event.getData(): UnifiedData

完整流程

.onDrop((event: DragEvent) => {
  // 1. 获取数据
  let data = event.getData();
  
  if (data) {
    // 2. 解析记录
    let records = data.getRecords();
    
    // 3. 读取内容
    for (let i = 0; i < records.length; i++) {
      let content = records[i].getPlainText();
      console.log(`Record ${i}: ${content}`);
    }
    
    // 4. 处理业务
    // ...
  }
})

7. 样式与资源 API

7.1 $r() — 资源引用

功能说明:引用 resources 目录下的字符串、颜色、图片等资源。

语法格式

$r('app.type.name')

常用资源类型

// 字符串资源
$r('app.string.title')

// 颜色资源
$r('app.color.background')

// 图片资源
$r('app.media.icon')

在 DragDrop 中的应用

// 引用字符串
Text($r('app.string.drag_title'))
  .fontSize(24)

// 引用颜色
.backgroundColor($r('app.color.source_background'))

// 动态资源(根据状态切换)
.backgroundColor(
  isDragging ? 
    $r('app.color.dragging_bg') : 
    $r('app.color.normal_bg')
)

7.2 promptAction.showToast() — 提示反馈

功能说明:显示轻量级提示消息,操作成功/失败时使用。

语法格式

promptAction.showToast(options: PromptOptions)

基本用法

promptAction.showToast({
  message: '操作成功',
  duration: 1500  // 显示时长(毫秒)
})

在 DragDrop 中的应用

// 成功放置
promptAction.showToast({
  message: '已添加: ' + itemName,
  duration: 1500
})

// 选中提示
promptAction.showToast({
  message: '已选中: ' + item.displayName,
  duration: 1500
})

// 清空提示
promptAction.showToast({
  message: '已清空所有物品',
  duration: 1500
})

7.3 .shadow() — 阴影效果

功能说明:为组件添加阴影,增强视觉层次。

语法格式

.shadow(options: ShadowOptions)

参数说明

{
  radius: number,      // 阴影模糊半径
  color: string,       // 阴影颜色
  offsetX: number,     // 水平偏移
  offsetY: number      // 垂直偏移
}

在 DragDrop 中的应用

// 拖拽预览阴影
Row() { /* ... */ }
.shadow({
  radius: 12,
  color: '#40000000',  // 半透明黑色
  offsetX: 0,
  offsetY: 6
})

// 卡片阴影
Column() { /* ... */ }
.shadow({
  radius: 8,
  color: '#20000000',
  offsetX: 2,
  offsetY: 2
})

7.4 .border() — 边框样式

功能说明:设置组件边框的颜色、宽度、样式。

语法格式

.border(options: BorderOptions)

完整示例

// 基础边框
.border({
  width: 1,
  color: $r('app.color.border')
})

// 虚线边框(目标区域)
.border({
  width: 2,
  color: isActive ? $r('app.color.border_active') : $r('app.color.border'),
  style: BorderStyle.Dashed
})

// 动态边框(选中物品)
.border({
  width: selectedItem ? 2 : 1,
  color: selectedItem ? $r('app.color.border_active') : $r('app.color.border')
})

8. 完整实战项目

8.1 项目概述

功能:水果分类拖拽应用,支持将水果从列表拖拽到购物篮。

双端适配

  • 手机/平板:长按拖拽
  • 电脑预览:点击选中 → 点击放置

8.2 资源文件

color.json
{
  "color": [
    { "name": "page_background", "value": "#F5F5F5" },
    { "name": "source_background", "value": "#E3F2FD" },
    { "name": "target_background", "value": "#FFF3E0" },
    { "name": "target_hover", "value": "#FFCC80" },
    { "name": "item_background", "value": "#FFFFFF" },
    { "name": "item_text", "value": "#333333" },
    { "name": "title_text", "value": "#1976D2" },
    { "name": "status_text", "value": "#666666" },
    { "name": "status_success", "value": "#4CAF50" },
    { "name": "status_error", "value": "#F44336" },
    { "name": "border", "value": "#BDBDBD" },
    { "name": "border_active", "value": "#FF9800" },
    { "name": "button_background", "value": "#2196F3" },
    { "name": "button_text", "value": "#FFFFFF" }
  ]
}
string.json
{
  "string": [
    { "name": "app_name", "value": "购物篮" },
    { "name": "drag_title", "value": "水果分类" },
    { "name": "drag_hint", "value": "长按拖拽或点击选中后放置" },
    { "name": "source_title", "value": "可选水果" },
    { "name": "target_title", "value": "我的购物篮" },
    { "name": "drop_hint", "value": "将水果拖到这里\n或点击选择后再点击这里" },
    { "name": "status_idle", "value": "等待操作..." },
    { "name": "status_dragging", "value": "拖拽中..." },
    { "name": "status_enter", "value": "进入放置区域" },
    { "name": "status_leave", "value": "离开放置区域" },
    { "name": "status_drop", "value": "添加成功!" },
    { "name": "status_cancel", "value": "操作取消" },
    { "name": "status_selected", "value": "已选中,点击购物篮放置" },
    { "name": "item_apple", "value": "苹果 🍎" },
    { "name": "item_banana", "value": "香蕉 🍌" },
    { "name": "item_grape", "value": "葡萄 🍇" },
    { "name": "item_orange", "value": "橙子 🍊" },
    { "name": "item_watermelon", "value": "西瓜 🍉" },
    { "name": "item_strawberry", "value": "草莓 🍓" },
    { "name": "item_mango", "value": "芒果 🥭" },
    { "name": "clear_button", "value": "清空购物篮" }
  ]
}

8.3 主页面代码

// ============================================
// HarmonyOS DragDrop 完整实战
// ============================================
import { unifiedDataChannel, uniformDataStruct, uniformTypeDescriptor } from '@kit.ArkData';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// 数据模型
interface DragItemData {
  id: string;
  name: Resource;
  displayName: string;
}

@Entry
@Component
struct Index {
  // ===== 状态变量 =====
  @State dragStatus: Resource = $r('app.string.status_idle');
  @State targetBgColor: Resource = $r('app.color.target_background');
  @State targetBorderColor: Resource = $r('app.color.border');
  @State isInTargetArea: boolean = false;
  @State droppedItems: string[] = [];
  @State currentDragItem: DragItemData | null = null;
  @State selectedItem: DragItemData | null = null;
  @State isSelectMode: boolean = false;
  
  // ===== 数据源 =====
  private dragItems: DragItemData[] = [
    { id: 'apple', name: $r('app.string.item_apple'), displayName: '苹果 🍎' },
    { id: 'banana', name: $r('app.string.item_banana'), displayName: '香蕉 🍌' },
    { id: 'grape', name: $r('app.string.item_grape'), displayName: '葡萄 🍇' },
    { id: 'orange', name: $r('app.string.item_orange'), displayName: '橙子 🍊' },
    { id: 'watermelon', name: $r('app.string.item_watermelon'), displayName: '西瓜 🍉' },
    { id: 'strawberry', name: $r('app.string.item_strawberry'), displayName: '草莓 🍓' },
    { id: 'mango', name: $r('app.string.item_mango'), displayName: '芒果 🥭' }
  ];
  
  // ===== 构建器 =====
  
  // 拖拽预览 UI
  @Builder
  DragPreviewBuilder(item: DragItemData) {
    Row() {
      Text(item.name)
        .fontSize(18)
        .fontColor(Color.White)
        .fontWeight(FontWeight.Bold)
    }
    .width(110)
    .height(56)
    .justifyContent(FlexAlign.Center)
    .backgroundColor($r('app.color.title_text'))
    .borderRadius(12)
    .shadow({ radius: 8, color: '#40000000', offsetX: 0, offsetY: 4 })
  }
  
  // 空购物篮提示
  @Builder
  EmptyTargetBuilder() {
    Column() {
      Text($r('app.string.drop_hint'))
        .fontSize(15)
        .fontColor($r('app.color.status_text'))
        .textAlign(TextAlign.Center)
        .lineHeight(24)
      
      if (this.selectedItem !== null) {
        Text('✓ 已选中水果')
          .fontSize(14)
          .fontColor($r('app.color.status_success'))
          .margin({ top: 12 })
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
  
  // 已放物品列表
  @Builder
  DroppedItemsBuilder() {
    Column() {
      Text(`购物篮 (${this.droppedItems.length})`)
        .fontSize(16)
        .fontColor($r('app.color.title_text'))
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 12 })
      
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.droppedItems, (item: string, index: number) => {
          Text(item)
            .fontSize(14)
            .fontColor($r('app.color.item_text'))
            .padding({ left: 14, right: 14, top: 8, bottom: 8 })
            .margin({ right: 8, bottom: 8 })
            .backgroundColor($r('app.color.source_background'))
            .borderRadius(20)
        }, (item: string, index: number) => index.toString())
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
  }
  
  // ===== 拖拽事件处理 =====
  
  handleDragStart(item: DragItemData, event: DragEvent): DragItemInfo {
    this.currentDragItem = item;
    this.dragStatus = $r('app.string.status_dragging');
    this.selectedItem = null;
    
    try {
      let plainText: uniformDataStruct.PlainText = {
        uniformDataType: 'general.plain-text',
        textContent: item.displayName
      };
      let record = new unifiedDataChannel.UnifiedRecord(
        uniformTypeDescriptor.UnifiedDataType.PLAIN_TEXT,
        plainText
      );
      let unifiedData = new unifiedDataChannel.UnifiedData(record);
      event.setData(unifiedData);
    } catch (e) {
      console.error('setData error: ' + (e as BusinessError).message);
    }
    
    return {
      builder: this.DragPreviewBuilder(item),
      extraInfo: item.id
    };
  }
  
  handleDragEnd(event: DragEvent): void {
    let result = event.getResult();
    if (result === DragResult.DRAG_SUCCESSFUL) {
      this.dragStatus = $r('app.string.status_drop');
    } else if (result === DragResult.DRAG_FAILED) {
      this.dragStatus = $r('app.string.status_cancel');
    } else {
      this.dragStatus = $r('app.string.status_idle');
    }
    this.currentDragItem = null;
    this.isInTargetArea = false;
    this.targetBgColor = $r('app.color.target_background');
    this.targetBorderColor = $r('app.color.border');
  }
  
  handleDragEnter(event: DragEvent): void {
    this.isInTargetArea = true;
    this.dragStatus = $r('app.string.status_enter');
    this.targetBgColor = $r('app.color.target_hover');
    this.targetBorderColor = $r('app.color.border_active');
  }
  
  handleDragLeave(event: DragEvent): void {
    this.isInTargetArea = false;
    this.dragStatus = $r('app.string.status_leave');
    this.targetBgColor = $r('app.color.target_background');
    this.targetBorderColor = $r('app.color.border');
  }
  
  handleDragMove(event: DragEvent): void {
    event.setResult(DragResult.DROP_ENABLED);
  }
  
  handleDrop(event: DragEvent): void {
    try {
      let data = event.getData();
      if (data) {
        let records = data.getRecords();
        if (records && records.length > 0) {
          let itemName: string = '';
          if (this.currentDragItem !== null) {
            itemName = this.currentDragItem.displayName;
          }
          
          if (itemName !== '') {
            this.droppedItems.push(itemName);
            event.setResult(DragResult.DRAG_SUCCESSFUL);
            promptAction.showToast({ message: '添加: ' + itemName });
          } else {
            event.setResult(DragResult.DRAG_FAILED);
          }
        }
      }
    } catch (e) {
      console.error('Drop error: ' + (e as BusinessError).message);
      event.setResult(DragResult.DRAG_FAILED);
    }
    
    this.isInTargetArea = false;
    this.targetBgColor = $r('app.color.target_background');
    this.targetBorderColor = $r('app.color.border');
  }
  
  // ===== 点击模式处理 =====
  
  selectItem(item: DragItemData): void {
    if (this.selectedItem !== null && this.selectedItem.id === item.id) {
      this.selectedItem = null;
      this.isSelectMode = false;
      this.dragStatus = $r('app.string.status_idle');
      this.targetBgColor = $r('app.color.target_background');
      this.targetBorderColor = $r('app.color.border');
      return;
    }
    
    this.selectedItem = item;
    this.isSelectMode = true;
    this.dragStatus = $r('app.string.status_selected');
    this.targetBgColor = $r('app.color.target_hover');
    this.targetBorderColor = $r('app.color.border_active');
    promptAction.showToast({ message: '选中: ' + item.displayName });
  }
  
  placeSelectedItem(): void {
    if (this.selectedItem !== null) {
      this.droppedItems.push(this.selectedItem.displayName);
      this.dragStatus = $r('app.string.status_drop');
      promptAction.showToast({ message: '添加: ' + this.selectedItem.displayName });
      this.selectedItem = null;
      this.isSelectMode = false;
      this.targetBgColor = $r('app.color.target_background');
      this.targetBorderColor = $r('app.color.border');
    }
  }
  
  clearDroppedItems(): void {
    this.droppedItems = [];
    this.dragStatus = $r('app.string.status_idle');
    this.selectedItem = null;
    this.isSelectMode = false;
    promptAction.showToast({ message: '已清空购物篮' });
  }
  
  // ===== 辅助方法 =====
  
  getItemBgColor(item: DragItemData): Resource {
    if (this.selectedItem !== null && this.selectedItem.id === item.id) {
      return $r('app.color.target_hover');
    }
    return $r('app.color.item_background');
  }
  
  getStatusColor(): Resource {
    if (this.dragStatus === $r('app.string.status_drop')) {
      return $r('app.color.status_success');
    }
    return $r('app.color.status_text');
  }
  
  // ===== UI 构建 =====
  
  build() {
    Column() {
      // 标题区域
      Column() {
        Text($r('app.string.drag_title'))
          .fontSize(26)
          .fontColor($r('app.color.title_text'))
          .fontWeight(FontWeight.Bold)
        
        Text($r('app.string.drag_hint'))
          .fontSize(13)
          .fontColor($r('app.color.status_text'))
          .margin({ top: 6 })
      }
      .width('100%')
      .padding({ top: 32, bottom: 20 })
      
      // 状态指示
      Row() {
        Text(this.dragStatus)
          .fontSize(14)
          .fontColor(this.getStatusColor())
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .backgroundColor('#10000000')
          .borderRadius(20)
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({ bottom: 16 })
      
      // 主区域
      Row() {
        // 左侧:可选水果列表
        Column() {
          Text($r('app.string.source_title'))
            .fontSize(17)
            .fontColor($r('app.color.title_text'))
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })
          
          Scroll() {
            Column({ space: 10 }) {
              ForEach(this.dragItems, (item: DragItemData) => {
                Row() {
                  Text(item.name)
                    .fontSize(18)
                    .fontColor($r('app.color.item_text'))
                  
                  if (this.selectedItem !== null && this.selectedItem.id === item.id) {
                    Text(' ✓')
                      .fontSize(16)
                      .fontColor($r('app.color.status_success'))
                      .fontWeight(FontWeight.Bold)
                  }
                }
                .width('100%')
                .height(52)
                .justifyContent(FlexAlign.Center)
                .backgroundColor(this.getItemBgColor(item))
                .borderRadius(10)
                .border({
                  width: this.selectedItem !== null && this.selectedItem.id === item.id ? 2 : 1,
                  color: this.selectedItem !== null && this.selectedItem.id === item.id ? 
                    $r('app.color.border_active') : $r('app.color.border')
                })
                // 手机端:长按拖拽
                .draggable(true)
                .onDragStart((event: DragEvent) => {
                  return this.handleDragStart(item, event);
                })
                .onDragEnd((event: DragEvent) => {
                  this.handleDragEnd(event);
                })
                // 电脑端:点击选中
                .onClick(() => {
                  this.selectItem(item);
                })
              }, (item: DragItemData) => item.id)
            }
          }
          .width('100%')
          .scrollBar(BarState.Off)
        }
        .width('45%')
        .padding(14)
        .backgroundColor($r('app.color.source_background'))
        .borderRadius(12)
        
        // 右侧:购物篮
        Column() {
          Text($r('app.string.target_title'))
            .fontSize(17)
            .fontColor($r('app.color.title_text'))
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })
          
          Column() {
            if (this.droppedItems.length === 0) {
              this.EmptyTargetBuilder();
            } else {
              this.DroppedItemsBuilder();
            }
          }
          .width('100%')
          .layoutWeight(1)
          .backgroundColor(this.targetBgColor)
          .borderRadius(10)
          .border({
            width: 2,
            color: this.targetBorderColor,
            style: BorderStyle.Dashed
          })
          // 电脑端:点击放置
          .onClick(() => {
            this.placeSelectedItem();
          })
        }
        .layoutWeight(1)
        .padding(14)
        .backgroundColor($r('app.color.target_background'))
        .borderRadius(12)
        // 手机端:接收拖拽
        .onDragEnter((event: DragEvent) => {
          this.handleDragEnter(event);
        })
        .onDragLeave((event: DragEvent) => {
          this.handleDragLeave(event);
        })
        .onDragMove((event: DragEvent) => {
          this.handleDragMove(event);
        })
        .onDrop((event: DragEvent) => {
          this.handleDrop(event);
        })
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16 })
      
      // 清空按钮
      if (this.droppedItems.length > 0) {
        Button($r('app.string.clear_button'))
          .width('60%')
          .height(48)
          .fontSize(16)
          .fontColor($r('app.color.button_text'))
          .backgroundColor($r('app.color.button_background'))
          .borderRadius(24)
          .margin({ top: 16, bottom: 24 })
          .onClick(() => {
            this.clearDroppedItems();
          })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.page_background'))
  }
}

9. 双端适配策略

9.1 问题背景

在开发过程中发现:

  • 手机/平板:支持长按拖拽(原生 DragDrop 体验)
  • 电脑预览器:不支持长按,拖拽功能不可用

9.2 解决方案

采用双模式并存策略:

设备 交互方式 实现原理
手机 长按 → 拖拽 → 释放 .draggable() + .onDragStart/End
电脑 点击选中 → 点击放置 .onClick() 状态管理

9.3 状态管理

// 拖拽模式状态
@State currentDragItem: DragItemData | null = null;

// 点击模式状态
@State selectedItem: DragItemData | null = null;
@State isSelectMode: boolean = false;

9.4 模式切换逻辑

// onDragStart 时清除点击状态
handleDragStart(item: DragItemData, event: DragEvent): DragItemInfo {
  this.currentDragItem = item;
  this.selectedItem = null;  // ← 清除点击模式
  // ...
}

// onClick 时清除拖拽状态
selectItem(item: DragItemData): void {
  this.selectedItem = item;
  this.isSelectMode = true;
  this.currentDragItem = null;  // ← 清除拖拽模式
  // ...
}

10. 进阶技巧与性能优化

10.1 多个目标区域

Row() {
  // 目标区域 A
  Column() { /* ... */ }
    .onDrop((event: DragEvent) => {
      this.handleDropToA(event);
    })
  
  // 目标区域 B
  Column() { /* ... */ }
    .onDrop((event: DragEvent) => {
      this.handleDropToB(event);
    })
}

10.2 拖拽排序

handleDrop(event: DragEvent, targetIndex: number): void {
  // 从源区域获取被拖元素
  let draggedItem = this.getDraggedItem(event);
  
  // 重新插入到目标位置
  this.items.splice(targetIndex, 0, draggedItem);
}

10.3 性能优化建议

优化项 建议
减少状态更新 批量更新多个状态,避免逐次更新
使用条件渲染 仅在必要时渲染复杂组件
列表虚拟化 长列表使用 LazyForEach
Builder 复用 相同 UI 使用同一 Builder

10.4 错误处理模式

handleDrop(event: DragEvent): void {
  try {
    // 业务逻辑
  } catch (e) {
    // 统一错误处理
    let error = e as BusinessError;
    console.error('Drop failed: ' + error.message);
    promptAction.showToast({ message: '操作失败,请重试' });
    event.setResult(DragResult.DRAG_FAILED);
  } finally {
    // 清理状态
    this.resetDragState();
  }
}

11. 常见问题解答

Q1: .draggable(true) 不生效怎么办?

A: 请检查以下几点:

  1. 确认组件是可交互的(非纯展示组件)
  2. 同时实现 .onDragStart 回调
  3. 不要在 Builder 返回值上使用,直接在组件上链式调用
// ✅ 正确
Row() { /* ... */ }
  .draggable(true)
  .onDragStart(handler)

// ❌ 错误
MyBuilder()  // 假设这是个 Builder
  .draggable(true)

Q2: 电脑端无法拖拽?

A: 电脑预览器通常不支持长按手势,建议实现点击模式作为备选方案(详见第 9 节)。

Q3: setData 失败怎么办?

A:

  1. 检查导入是否完整:import { unifiedDataChannel, uniformDataStruct, uniformTypeDescriptor } from '@kit.ArkData'
  2. 确认数据类型正确(推荐从 PLAIN_TEXT 开始)
  3. 使用 try-catch 包裹并打印错误信息

Q4: 如何传递自定义对象?

A: 将对象序列化为 JSON 字符串:

let plainText: uniformDataStruct.PlainText = {
  uniformDataType: 'general.plain-text',
  textContent: JSON.stringify(customObject)
};

Q5: 拖拽预览如何自定义?

A: 通过 onDragStart 返回的 Builder 完全自定义,支持任意 UI 组件:

return {
  builder: this.CustomPreviewBuilder(item)
};

@Builder
CustomPreviewBuilder(item: DragItemData) {
  Stack() {
    Image(item.icon).width(60).height(60)
    Text(item.name).fontSize(12)
  }
  .width(80)
  .height(80)
  .backgroundColor(Color.White)
  .borderRadius(8)
  .shadow({ radius: 8, color: '#30000000', offsetY: 4 })
}

12. 总结与展望

12.1 核心知识点回顾

模块 关键 API 数量
装饰器 @Entry, @Component, @State, @Builder, import, struct, private, @ObjectLink 8
布局 Column, Row, Text, Button, ForEach, Flex, layoutWeight, padding/margin 8
拖拽 .draggable(), .onDragStart/End/Enter/Leave/Move/Drop/onClick 8

12.2 开发流程总结

1. 创建项目 → 2. 定义数据模型 → 3. 搭建布局
→ 4. 启用拖拽能力 → 5. 实现事件处理 → 6. 数据传递
→ 7. 视觉反馈 → 8. 双端适配 → 9. 测试优化

12.3 学习建议

  1. 从基础开始:先实现简单的拖拽放置,再逐步添加复杂度
  2. 关注状态管理:DragDrop 的核心是状态的正确流转
  3. 重视用户体验:添加适当的动画、震动、声音反馈
  4. 适配多设备:从一开始就考虑不同屏幕
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐