Skip to content

剪贴板

源:Android ClipboardManager | iOS UIPasteboard

剪贴板操作是应用间数据交换的重要方式。本文将展示如何跨平台实现文本、图片等内容的复制与粘贴。

平台差异对比

平台原生 API支持类型权限要求特性
AndroidClipboardManager文本、URI、Intent无需权限剪贴板监听、多类型数据
iOSUIPasteboard文本、图片、URL无需权限过期时间、跨设备同步
DesktopToolkit.getDefaultToolkit()文本、图片无需权限系统剪贴板

expect/actual 实现方案

API 核心签名说明

  • expect object ClipboardManager
  • fun ClipboardManager.copyText(text: String)
  • fun ClipboardManager.getText(): String?
  • fun ClipboardManager.hasText(): Boolean
  • fun ClipboardManager.clear()

标准代码块

kotlin
expect object ClipboardManager {
    fun copyText(text: String)
    fun getText(): String?
    fun hasText(): Boolean
    fun clear()
}

// 业务层使用
class ShareHelper {
    
    fun copyToClipboard(text: String) {
        ClipboardManager.copyText(text)
        // 可选:显示 Toast 提示
        println("已复制到剪贴板")
    }
    
    fun pasteFromClipboard(): String? {
        return if (ClipboardManager.hasText()) {
            ClipboardManager.getText()
        } else {
            null
        }
    }
    
    fun shareInviteCode(code: String) {
        copyToClipboard("邀请码:$code")
    }
}
kotlin
import android.content.ClipData
import android.content.ClipboardManager as AndroidClipboardManager
import android.content.Context

actual object ClipboardManager {
    
    private lateinit var clipboardManager: AndroidClipboardManager
    
    fun init(context: Context) {
        clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) 
            as AndroidClipboardManager
    }
    
    actual fun copyText(text: String) {
        val clip = ClipData.newPlainText("text", text)
        clipboardManager.setPrimaryClip(clip)
    }
    
    actual fun getText(): String? {
        if (!clipboardManager.hasPrimaryClip()) {
            return null
        }
        
        val clip = clipboardManager.primaryClip ?: return null
        if (clip.itemCount == 0) {
            return null
        }
        
        return clip.getItemAt(0)?.text?.toString()
    }
    
    actual fun hasText(): Boolean {
        return clipboardManager.hasPrimaryClip() && 
               clipboardManager.primaryClipDescription?.hasMimeType("text/plain") == true
    }
    
    actual fun clear() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
            clipboardManager.clearPrimaryClip()
        } else {
            // Android 8 及以下无法清空剪贴板
            val clip = ClipData.newPlainText("", "")
            clipboardManager.setPrimaryClip(clip)
        }
    }
}
kotlin
import platform.UIKit.UIPasteboard

actual object ClipboardManager {
    
    private val pasteboard = UIPasteboard.generalPasteboard
    
    actual fun copyText(text: String) {
        pasteboard.string = text
    }
    
    actual fun getText(): String? {
        return pasteboard.string
    }
    
    actual fun hasText(): Boolean {
        return pasteboard.string != null
    }
    
    actual fun clear() {
        pasteboard.items = emptyList()
    }
}
kotlin
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection

actual object ClipboardManager {
    
    private val clipboard = Toolkit.getDefaultToolkit().systemClipboard
    
    actual fun copyText(text: String) {
        val selection = StringSelection(text)
        clipboard.setContents(selection, null)
    }
    
    actual fun getText(): String? {
        return try {
            if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
                clipboard.getData(DataFlavor.stringFlavor) as? String
            } else {
                null
            }
        } catch (e: Exception) {
            null
        }
    }
    
    actual fun hasText(): Boolean {
        return clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)
    }
    
    actual fun clear() {
        clipboard.setContents(StringSelection(""), null)
    }
}

代码封装示例

以下是带自动清空和提示的完整封装:

kotlin
// commonMain
class SecureClipboard(private val manager: ClipboardManager) {
    
    private var clearJob: Job? = null
    
    fun copyWithAutoClear(
        text: String,
        clearAfterMillis: Long = 60_000L,
        onCopied: () -> Unit = {}
    ) {
        manager.copyText(text)
        onCopied()
        
        // 取消之前的清空任务
        clearJob?.cancel()
        
        // 定时清空
        clearJob = CoroutineScope(Dispatchers.Default).launch {
            delay(clearAfterMillis)
            manager.clear()
        }
    }
    
    fun cancelAutoClear() {
        clearJob?.cancel()
        clearJob = null
    }
}

依赖补充

无需额外依赖

剪贴板功能仅依赖平台标准库,无需添加第三方依赖。

Android 初始化

需要在应用启动时初始化:

kotlin
// androidMain
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        ClipboardManager.init(this)
    }
}

实战坑点

Android 后台访问限制

Android 10+ 限制

Android 10+ 后台应用访问剪贴板会返回空内容。

解决方案:
只在应用前台时读取剪贴板,或使用 ContentProvider 共享数据。

iOS 剪贴板提示

iOS 14+ 隐私提示

iOS 14+ 首次访问剪贴板会在顶部显示"粘贴自..."提示。

最佳实践:

kotlin
// 仅在用户点击"粘贴"按钮时读取
fun onPasteButtonClick() {
    val text = ClipboardManager.getText()
    // 使用文本
}

剪贴板数据类型

::: caution 非文本数据 本示例仅处理文本,图片、文件等需要额外处理。 :::

图片复制示例(Android):

kotlin
// androidMain
fun copyImage(imageUri: Uri) {
    val clip = ClipData.newUri(contentResolver, "image", imageUri)
    clipboardManager.setPrimaryClip(clip)
}

Desktop 线程安全

AWT 线程

Desktop 剪贴板操作应在 EDT(Event Dispatch Thread)执行。

解决方案:

kotlin
// jvmMain
import javax.swing.SwingUtilities

fun copyTextSafe(text: String) {
    SwingUtilities.invokeLater {
        clipboard.setContents(StringSelection(text), null)
    }
}