DEV Community

HarmonyOS
HarmonyOS

Posted on

Custom Drag Preview & PlainText Transfer via UnifiedDataChannel

Read the original article:Custom Drag Preview & PlainText Transfer via UnifiedDataChannel

Requirement Description

Implement a drag-and-drop interaction on HarmonyOS wearables where tapping a button starts a drag with a custom preview, and dropping over a target reads PlainText data via UnifiedDataChannel and updates the UI. The drop zone should provide visual feedback (scale + border color) and animate the drag preview on hover.

Background Knowledge

  1. ArkUI dragController: Starts drags (executeDrag), accesses the current preview (getDragPreview()), and supports preview animation.
  2. Unified Data Channel (@kit.ArkData): Standard container for transferable data; here we use PlainText.
  3. UIContext: Entry point to the drag controller for the active UI.
  4. Drag lifecycle: onDragEnter → onDragLeave → onDrop to manage visuals and data handling.
  5. Logging: Use hilog to trace success/failure and error codes.

Implementation Steps

  • State: Keep dropText, dragLabel, isDragging, dropScale, dropBorder as reactive UI state.
  • Preview Builder: Define @Builder DragPreviewBuilder() for the custom badge-style preview (emoji + label).
  • Pack Data: Create PlainText, then wrap in UnifiedData.
  • Start Drag: Call executeDrag(() => DragPreviewBuilder(), info, callback) in startDrag(pointerId).
  • Source Trigger: On the Drag button, start drag on TouchType.Down for snappy UX.
  • Drop Target:
    • onDragEnter: enlarge, recolor border, animate preview (optional).
    • onDragLeave: reset visuals.
    • onDrop: extract records, cast to PlainText, set dropText.
  • Error Handling: Wrap preview animation and drag execution in try/catch; log BusinessError details.

Code Snippet / Configuration

import { dragController, curves, UIContext } from '@kit.ArkUI';
import { unifiedDataChannel } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';
import type { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  @State private dropText: string = 'Drop here';
  @State private dragLabel: string = 'Hello from drag!';
  @State private isDragging: boolean = false;
  @State private dropScale: number = 1.0;
  @State private dropBorder: ResourceColor = '#111827';

  private readonly watchSize: string = '100%';

  @Builder DragPreviewBuilder() {
    Column() {
      Text('📦').fontSize(22).textAlign(TextAlign.Center).margin({ bottom: 4 })
      Text(this.dragLabel)
        .fontColor(Color.White).fontSize(16).fontWeight(FontWeight.Bold)
        .textAlign(TextAlign.Center).maxLines(2).lineHeight(20)
    }
    .width(150).height(90).borderRadius(16).backgroundColor('#2563EB')
    .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
    .padding({ left: 10, right: 10 })
    .shadow({ radius: 16, color: '#33000000', offsetX: 0, offsetY: 2 })
  }

  private startDrag(pointerId: number = 0): void {
    const txt = new unifiedDataChannel.PlainText();
    txt.textContent = this.dragLabel;
    txt.abstract = 'demo';
    const unified = new unifiedDataChannel.UnifiedData(txt);

    const info: dragController.DragInfo = { pointerId, data: unified, extraParams: '' };
    this.isDragging = true;
    try {
      this.getUIContext().getDragController().executeDrag(
        () => { this.DragPreviewBuilder(); },
        info,
        (err: BusinessError | undefined, res) => {
          this.isDragging = false;
          if (err) {
            hilog.error(0x0000, 'drag', `executeDrag error: ${err.message}`);
            return;
          }
          if (res?.event) {
            const ok = res.event.getResult() === DragResult.DRAG_SUCCESSFUL;
            hilog.info(0x0000, 'drag', ok ? 'SUCCESS' : 'FAILED');
          }
        }
      );
    } catch (e) {
      this.isDragging = false;
      hilog.error(0x0000, 'drag', `executeDrag threw: ${String(e)}`);
    }
  }

  build() {
    Stack() {
      Column() {}
        .width(this.watchSize).height(this.watchSize)
        .clip(Circle({ width: '100%', height: '100%' }))
        .backgroundColor('#0B1220')

      Column() {
        Button('Drag')
          .type(ButtonType.Capsule).fontSize(12).height(36)
          .margin({ top: 8, bottom: 8 }).backgroundColor('#2563EB')
          .fontColor(Color.White).opacity(this.isDragging ? 0.9 : 1)
          .shadow({ radius: 12, color: '#26000000', offsetX: 0, offsetY: 2 })
          .onClick(() => { this.dragLabel = 'Hello from drag!'; })
          .onTouch((ev?: TouchEvent) => {
            if (!ev) return;
            if (ev.type === TouchType.Down) this.startDrag(0);
          })

        Column() {
          Text(this.dropText)
            .fontSize(14).textAlign(TextAlign.Center).fontColor(Color.White)
            .width('100%').maxLines(2).lineHeight(18)
            .padding({ left: 6, right: 6 })
        }
        .width(180).height(92).borderRadius(20).backgroundColor('#111827')
        .border({ color: this.dropBorder, width: 1 })
        .scale({ x: this.dropScale, y: this.dropScale })
        .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .onDragEnter(() => {
          this.dropScale = 1.05; this.dropBorder = '#22C55E';
          try {
            const ui: UIContext = this.getUIContext();
            const preview = ui.getDragController().getDragPreview();
            const anim: dragController.AnimationOptions = {
              duration: 250,
              curve: curves.cubicBezierCurve(0.2, 0, 0, 1)
            };
            preview.animate(anim, () => { preview.setForegroundColor(Color.Green); });
          } catch (err) {
            const e = err as BusinessError;
            hilog.error(0x0000, 'preview', `animate error: ${e?.code} ${e?.message}`);
          }
        })
        .onDragLeave(() => { this.dropScale = 1.0; this.dropBorder = '#111827'; })
        .onDrop((dragEvent?: DragEvent) => {
          this.dropScale = 1.0; this.dropBorder = '#111827';
          if (!dragEvent) return;
          const records = dragEvent.getData().getRecords();
          if (records.length > 0) {
            const first = records[0] as unifiedDataChannel.PlainText;
            this.dropText = first.textContent ?? 'dropped';
          }
        })

        Text('Tip: Press “Drag”, move the 📦 preview, and drop it into the box.')
          .fontSize(10).fontColor('#94A3B8').margin({ top: 8 }).opacity(0.9)
      }
      .width('70%').height('100%')
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
      .padding(8)
    }
    .width('100%').height('100%').align(Alignment.Center)
  }
}
Enter fullscreen mode Exit fullscreen mode

Test Results

  • Flow: tap Drag → preview appears → hover target (box scales + green border, preview animates) → drop → target displays the transferred text.
  • Logs: success logs show DragResult.DRAG_SUCCESSFUL; all failures print BusinessError code/message.

1.png2.png

Limitations or Considerations

  1. Type Safety: The first record might not be PlainText; guard with instanceof if you add more formats.
  2. Device Variations: getDragPreview().animate() may vary across devices/versions; always try/catch.
  3. Performance: Shadows/animations can be costly on low-power wearables; keep durations and radii modest.
  4. Reentrancy: Use isDragging (or disable the button) to avoid concurrent drags.
  5. Localization: If replacing literals with resource strings, ensure proper Resource → string conversion where required.

Related Documents or Links

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-common-events-drag-event#drag-process

Written by Baris Tuzemen

Top comments (0)