跨設備剪貼板數據:實現應用間內容共享

概述

在 HarmonyOS 生態系統中,跨設備剪貼板功能讓用户能夠在一台設備上覆制內容,然後在同一賬號下的其他設備上粘貼使用。這項技術打破了設備邊界,為開發者提供了全新的內容共享體驗。

官方參考資料

  • HarmonyOS API 參考
  • 剪貼板服務指南
  • 跨設備剪貼板

重要提示:本文所有示例基於 HarmonyOS Next API 10+和 DevEco Studio 4.0+,請確保開發環境正確配置。

開發環境準備

必備條件

在開始編碼前,請確保滿足以下條件:

  • 賬號體系:所有設備登錄同一華為賬號

權限配置

跨設備剪貼板需要以下權限:

// 在module.json5文件中添加權限
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC"
      }
    ]
  }
}

基礎剪貼板操作

單設備剪貼板

首先了解基本的剪貼板操作,這是跨設備功能的基礎:

import { pasteboard } from "@kit.ArkData";

// 創建PasteData對象
let pasteData = pasteboard.createData("text/plain");

// 設置文本內容
pasteData.addText("Hello HarmonyOS!");

// 寫入剪貼板
pasteboard
  .setSystemPasteData(pasteData)
  .then(() => {
    console.info("Data written to clipboard successfully");
  })
  .catch((err: BusinessError) => {
    console.error(`Failed to write data: ${err.code}, ${err.message}`);
  });

讀取剪貼板數據

// 從剪貼板讀取數據
pasteboard
  .getSystemPasteData()
  .then((data: pasteboard.PasteData) => {
    if (data) {
      // 檢查並獲取文本內容
      if (data.hasType(pasteboard.MIMETYPE_TEXT_PLAIN)) {
        let text = data.getPrimaryText();
        console.info(`Retrieved text: ${text}`);
      }
    }
  })
  .catch((err: BusinessError) => {
    console.error(`Failed to read data: ${err.code}, ${err.message}`);
  });

跨設備剪貼板實現

核心 API 概覽

跨設備剪貼板主要依賴以下 API:

API 名稱

功能描述

適用場景

createData()

創建剪貼板數據對象

初始化數據

addText()

添加文本內容

文本數據複製

addHtml()

添加 HTML 內容

富文本複製

addPixelMap()

添加圖片數據

圖像複製

setSystemPasteData()

設置系統剪貼板

寫入操作

getSystemPasteData()

獲取系統剪貼板

讀取操作

完整的跨設備複製實現

import { pasteboard } from "@kit.ArkData";
import { BusinessError } from "@kit.BasicServicesKit";

class CrossDeviceClipboard {
  /**
   * 複製文本到跨設備剪貼板
   * @param text 要複製的文本內容
   */
  async copyTextToCrossDevice(text: string): Promise<void> {
    try {
      // 1. 創建剪貼板數據
      let pasteData = pasteboard.createData("text/plain");

      // 2. 添加文本內容
      pasteData.addText(text);

      // 3. 設置屬性標籤(可選,用於標識應用)
      pasteData.addProperty("sourceApp", "com.example.myapp");

      // 4. 寫入系統剪貼板(自動同步到其他設備)
      await pasteboard.setSystemPasteData(pasteData);

      console.info("Text copied to cross-device clipboard");
    } catch (err) {
      let error = err as BusinessError;
      console.error(`Copy failed: ${error.code}, ${error.message}`);
      throw error;
    }
  }

  /**
   * 從跨設備剪貼板粘貼文本
   */
  async pasteTextFromCrossDevice(): Promise<string | null> {
    try {
      // 1. 獲取剪貼板數據
      let pasteData = await pasteboard.getSystemPasteData();

      if (!pasteData) {
        console.info("Clipboard is empty");
        return null;
      }

      // 2. 檢查數據類型
      if (pasteData.hasType(pasteboard.MIMETYPE_TEXT_PLAIN)) {
        // 3. 獲取文本內容
        let text = pasteData.getPrimaryText();
        console.info(`Pasted text: ${text}`);
        return text;
      } else {
        console.info("Clipboard does not contain text data");
        return null;
      }
    } catch (err) {
      let error = err as BusinessError;
      console.error(`Paste failed: ${error.code}, ${error.message}`);
      throw error;
    }
  }
}

富文本內容處理

除了純文本,跨設備剪貼板還支持富文本內容:

class RichTextClipboard {
  /**
   * 複製HTML內容到跨設備剪貼板
   */
  async copyHtmlContent(): Promise<void> {
    let htmlContent = `
      <h1>HarmonyOS開發指南</h1>
      <p>這是<strong>加粗文本</strong>和<em>斜體文本</em></p>
      <ul>
        <li>列表項1</li>
        <li>列表項2</li>
      </ul>
    `;

    let pasteData = pasteboard.createData("text/html");
    pasteData.addHtml(htmlContent);

    // 同時添加純文本版本作為備選
    pasteData.addText(
      "HarmonyOS開發指南 - 這是加粗文本和斜體文本 - 列表項1 - 列表項2"
    );

    await pasteboard.setSystemPasteData(pasteData);
    console.info("HTML content copied to cross-device clipboard");
  }

  /**
   * 粘貼HTML內容
   */
  async pasteHtmlContent(): Promise<{ html?: string; text?: string }> {
    let pasteData = await pasteboard.getSystemPasteData();

    if (!pasteData) {
      return {};
    }

    let result: { html?: string; text?: string } = {};

    // 優先獲取HTML內容
    if (pasteData.hasType(pasteboard.MIMETYPE_TEXT_HTML)) {
      result.html = pasteData.getPrimaryHtml();
    }

    // 備選純文本
    if (pasteData.hasType(pasteboard.MIMETYPE_TEXT_PLAIN)) {
      result.text = pasteData.getPrimaryText();
    }

    return result;
  }
}

高級功能實現

自定義數據類型

對於應用特定的數據結構,可以使用自定義 MIME 類型:

// 定義自定義MIME類型
const MIMETYPE_APP_DATA = "application/vnd.example.appdata+json";

class CustomDataClipboard {
  /**
   * 複製自定義數據到剪貼板
   */
  async copyCustomData(userData: object): Promise<void> {
    try {
      let pasteData = pasteboard.createData(MIMETYPE_APP_DATA);

      // 將對象轉換為JSON字符串
      let jsonData = JSON.stringify(userData);
      pasteData.addText(jsonData);

      // 添加屬性標記數據來源
      pasteData.addProperty("dataType", "userProfile");
      pasteData.addProperty("timestamp", Date.now().toString());

      await pasteboard.setSystemPasteData(pasteData);
      console.info("Custom data copied to clipboard");
    } catch (err) {
      let error = err as BusinessError;
      console.error(`Custom data copy failed: ${error.code}, ${error.message}`);
    }
  }

  /**
   * 粘貼並解析自定義數據
   */
  async pasteCustomData(): Promise<object | null> {
    try {
      let pasteData = await pasteboard.getSystemPasteData();

      if (!pasteData || !pasteData.hasType(MIMETYPE_APP_DATA)) {
        return null;
      }

      let jsonString = pasteData.getPrimaryText();
      let userData = JSON.parse(jsonString);

      // 驗證數據來源(可選)
      let dataType = pasteData.getProperty("dataType");
      if (dataType === "userProfile") {
        console.info("Valid custom data received");
        return userData;
      }

      return null;
    } catch (err) {
      let error = err as BusinessError;
      console.error(
        `Custom data paste failed: ${error.code}, ${error.message}`
      );
      return null;
    }
  }
}

剪貼板變化監聽

實時監聽剪貼板內容變化:

class ClipboardMonitor {
  private listener: pasteboard.SystemPasteboardChangedListener | null = null;

  /**
   * 開始監聽剪貼板變化
   */
  startMonitoring(): void {
    this.listener = (pasteData: pasteboard.PasteData) => {
      console.info("Clipboard content changed");

      if (pasteData) {
        // 檢查新內容類型
        if (pasteData.hasType(pasteboard.MIMETYPE_TEXT_PLAIN)) {
          let text = pasteData.getPrimaryText();
          console.info(`New text content: ${text}`);
        }

        if (pasteData.hasType(pasteboard.MIMETYPE_TEXT_HTML)) {
          console.info("New HTML content available");
        }

        // 檢查數據來源設備
        let sourceDevice = pasteData.getProperty("sourceDevice");
        if (sourceDevice) {
          console.info(`Data from device: ${sourceDevice}`);
        }
      }
    };

    // 註冊監聽器
    pasteboard.on("systemPasteboardChanged", this.listener);
    console.info("Clipboard monitoring started");
  }

  /**
   * 停止監聽
   */
  stopMonitoring(): void {
    if (this.listener) {
      pasteboard.off("systemPasteboardChanged", this.listener);
      this.listener = null;
      console.info("Clipboard monitoring stopped");
    }
  }
}

實戰案例:跨設備筆記共享

讓我們構建一個完整的跨設備筆記共享應用:

import { pasteboard } from "@kit.ArkData";
import { BusinessError } from "@kit.BasicServicesKit";
import { UIAbility, AbilityConstant, Want } from "@kit.AbilityKit";

class CrossDeviceNoteApp {
  private readonly MIMETYPE_NOTE = "application/vnd.notepad.note+json";

  /**
   * 複製筆記到其他設備
   */
  async copyNoteToDevices(note: Note): Promise<void> {
    try {
      let pasteData = pasteboard.createData(this.MIMETYPE_NOTE);

      // 構建筆記數據
      let noteData = {
        title: note.title,
        content: note.content,
        createdAt: note.createdAt,
        category: note.category,
      };

      pasteData.addText(JSON.stringify(noteData));

      // 添加元數據
      pasteData.addProperty("dataType", "crossDeviceNote");
      pasteData.addProperty("version", "1.0");
      pasteData.addProperty("sourceApp", "NotePadApp");

      await pasteboard.setSystemPasteData(pasteData);

      console.info(`Note "${note.title}" copied to cross-device clipboard`);
    } catch (err) {
      let error = err as BusinessError;
      console.error(`Note copy failed: ${error.code}, ${error.message}`);
      throw new Error("Failed to copy note to other devices");
    }
  }

  /**
   * 從剪貼板導入筆記
   */
  async importNoteFromClipboard(): Promise<Note | null> {
    try {
      let pasteData = await pasteboard.getSystemPasteData();

      if (!pasteData || !pasteData.hasType(this.MIMETYPE_NOTE)) {
        return null;
      }

      // 驗證數據格式
      let dataType = pasteData.getProperty("dataType");
      if (dataType !== "crossDeviceNote") {
        return null;
      }

      let jsonString = pasteData.getPrimaryText();
      let noteData = JSON.parse(jsonString);

      // 創建筆記對象
      let note: Note = {
        title: noteData.title || "Imported Note",
        content: noteData.content || "",
        createdAt: noteData.createdAt || Date.now(),
        category: noteData.category || "General",
        sourceDevice: pasteData.getProperty("sourceDevice") || "Unknown",
      };

      console.info(`Note imported from device: ${note.sourceDevice}`);
      return note;
    } catch (err) {
      let error = err as BusinessError;
      console.error(`Note import failed: ${error.code}, ${error.message}`);
      return null;
    }
  }

  /**
   * 清空跨設備剪貼板
   */
  async clearCrossDeviceClipboard(): Promise<void> {
    try {
      await pasteboard.clearSystemPasteData();
      console.info("Cross-device clipboard cleared");
    } catch (err) {
      let error = err as BusinessError;
      console.error(`Clear clipboard failed: ${error.code}, ${error.message}`);
    }
  }
}

// 筆記數據類型定義
interface Note {
  title: string;
  content: string;
  createdAt: number;
  category: string;
  sourceDevice?: string;
}

性能優化與最佳實踐

數據大小限制

跨設備剪貼板有數據大小限制,需要合理管理:

class ClipboardOptimizer {
  private readonly MAX_TEXT_SIZE = 1024 * 1024; // 1MB

  /**
   * 優化大文本數據的複製
   */
  async copyLargeTextOptimized(text: string): Promise<void> {
    if (text.length > this.MAX_TEXT_SIZE) {
      console.warn("Text exceeds size limit, truncating...");
      text = text.substring(0, this.MAX_TEXT_SIZE);
    }

    let pasteData = pasteboard.createData("text/plain");
    pasteData.addText(text);

    await pasteboard.setSystemPasteData(pasteData);
  }

  /**
   * 分塊處理大內容
   */
  async copyLargeContentInChunks(
    largeContent: string,
    chunkSize: number = 50000
  ): Promise<void> {
    let chunks: string[] = [];

    for (let i = 0; i < largeContent.length; i += chunkSize) {
      chunks.push(largeContent.substring(i, i + chunkSize));
    }

    // 只複製第一個塊,其他塊通過其他方式傳輸
    if (chunks.length > 0) {
      let pasteData = pasteboard.createData("text/plain");
      pasteData.addText(chunks[0]);
      pasteData.addProperty("totalChunks", chunks.length.toString());
      pasteData.addProperty("contentId", this.generateContentId());

      await pasteboard.setSystemPasteData(pasteData);
    }
  }

  private generateContentId(): string {
    return `content_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

注意事項與常見問題

重要安全提示

⚠️ 安全警告

  • 剪貼板可能包含敏感信息,確保應用有明確的隱私政策
  • 不要自動讀取剪貼板內容,必須用户主動觸發
  • 定期清理不再需要的剪貼板數據

版本兼容性

HarmonyOS 版本

跨設備剪貼板支持

注意事項

API 10+

✅ 完整支持

本文示例基於此版本

API 9

⚠️ 部分支持

缺少某些高級特性

API 8 及以下

❌ 不支持

需要升級目標版本

常見錯誤處理

class ClipboardErrorHandler {
  static handleCommonErrors(error: BusinessError): string {
    const ERROR_CODES = {
      201: "Permission denied - Check DISTRIBUTED_DATASYNC permission",
      202: "Parameter error - Verify input parameters",
      203: "Operation timeout - Network connectivity issue",
      204: "Service unavailable - Cross-device service not running",
    };

    let errorCode = error.code.toString();
    return (
      ERROR_CODES[errorCode] || `Unknown error: ${error.code}, ${error.message}`
    );
  }

  // 使用示例
  static async safeClipboardOperation(
    operation: () => Promise<void>
  ): Promise<boolean> {
    try {
      await operation();
      return true;
    } catch (err) {
      let error = err as BusinessError;
      let errorMessage = this.handleCommonErrors(error);
      console.error(`Clipboard operation failed: ${errorMessage}`);
      return false;
    }
  }
}

調試技巧

// 剪貼板調試工具
class ClipboardDebugger {
  /**
   * 調試剪貼板內容
   * @param verbose 是否顯示詳細信息
   */
  static async debugClipboard(verbose: boolean = false): Promise<void> {
    try {
      console.info('=== Clipboard Debug Info ===');

      // 獲取剪貼板數據
      let pasteData = await pasteboard.getSystemPasteData();

      if (!pasteData) {
        console.info('Clipboard is currently empty');
        return;
      }

      // 檢查數據類型
      console.info('Available data types:');

      const MIME_TYPES = [
        {type: pasteboard.MIMETYPE_TEXT_PLAIN, label: 'Plain Text'},
        {type: pasteboard.MIMETYPE_TEXT_HTML, label: 'HTML'},
        {type: pasteboard.MIMETYPE_IMAGE_PIXELMAP, label: 'Image'}
      ];

      for (const {type, label} of MIME_TYPES) {
        if (pasteData.hasType(type)) {
          console.info(`- ${label}`);

          // 顯示內容預覽
          if (verbose) {
            if (type === pasteboard.MIMETYPE_TEXT_PLAIN) {
              let text = pasteData.getPrimaryText();
              // 限制輸出長度
              let preview = text.length > 100 ? text.substring(0, 100) + '...' : text;
              console.info(`  Content preview: ${preview}`);
            } else if (type === pasteboard.MIMETYPE_TEXT_HTML) {
              let html = pasteData.getPrimaryHtml();
              let preview = html.length > 100 ? html.substring(0, 100) + '...' : html;
              console.info(`  HTML preview: ${preview}`);
            }
          }
        }
      }

      // 顯示屬性信息
      if (verbose) {
        console.info('\nClipboard properties:');

        // 獲取所有屬性鍵
        const properties = ['sourceApp', 'dataType', 'timestamp', 'sourceDevice', 'version'];

        for (const prop of properties) {
          let value = pasteData.getProperty(prop);
          if (value) {
            console.info(`- ${prop}: ${value}`);
          }
        }
      }

      console.info('=== Debug Complete ===');

    } catch (err) {
      let error = err as BusinessError;
      console.error(`Debug failed: ${error.code}, ${error.message}`);
    }
  }

  /**
   * 監控剪貼板變化並記錄日誌
   */
  static enableMonitoringLog(): void {
    console.info('Enabling clipboard monitoring logs');

    const listener = (pasteData: pasteboard.PasteData) => {
      console.log('CLIPBOARD EVENT:', new Date().toISOString());

      if (pasteData) {
        // 檢查主要內容類型
        if (pasteData.hasType(pasteboard.MIMETYPE_TEXT_PLAIN)) {
          let text = pasteData.getPrimaryText();
          console.log('  New text content (length):', text?.length || 0);
        }

        // 檢查是否來自其他設備
        let sourceDevice = pasteData.getProperty('sourceDevice');
        if (sourceDevice) {
          console.log('  Cross-device data from:', sourceDevice);
        }
      }
    };

    pasteboard.on('systemPasteboardChanged', listener);
    console.info('Clipboard monitor attached');

    // 返回清理函數
    return () => {
      pasteboard.off('systemPasteboardChanged', listener);
      console.info('Clipboard monitor detached');
    };
  }
}

總結與展望

核心能力總結

跨設備剪貼板是HarmonyOS生態系統中的一項強大功能,為開發者提供了以下關鍵能力:

  1. 無縫內容共享:實現不同設備間的文本、富文本和自定義數據的即時共享
  2. 多類型數據支持:從簡單文本到複雜的自定義結構化數據,滿足各種應用場景需求
  3. 實時數據同步:基於分佈式數據同步技術,確保內容快速可靠地傳輸
  4. 事件監聽機制:通過監聽器模式,實時響應剪貼板內容變化

開發建議

在實現跨設備剪貼板功能時,建議遵循以下最佳實踐:

  1. 權限聲明:確保正確聲明DISTRIBUTED_DATASYNC權限
  2. 錯誤處理:實現全面的錯誤處理,特別是對跨設備操作可能出現的網絡或同步問題
  3. 數據安全:敏感信息處理時,考慮加密和及時清理策略
  4. 用户體驗:提供明確的用户反饋,讓用户知道內容已成功複製到其他設備
  5. 性能優化:對大文件採用分塊處理,避免超過剪貼板大小限制

未來發展方向

隨着HarmonyOS生態的不斷髮展,跨設備剪貼板功能有望在以下方面進一步增強:

  1. 更多媒體類型支持:未來可能支持更多類型的媒體內容,如視頻片段、音頻文件等
  2. 智能內容識別:自動識別複製內容的類型,並提供相應的處理建議
  3. 設備篩選:允許用户選擇特定的目標設備進行內容共享
  4. 歷史記錄管理:提供剪貼板歷史記錄,方便用户回溯和再次使用

通過合理利用跨設備剪貼板功能,開發者可以打造更具沉浸感和連貫性的多設備應用體驗,讓用户在不同設備間無縫切換工作流程,大大提高使用效率和便利性。

最終建議:在實際應用中,始終保持對用户隱私的尊重,避免濫用剪貼板功能,並提供清晰的用户控制選項。