复制粘贴

Android 基于剪贴板的复制粘贴框架支持原始和复杂的数据类型,包括:

  • 文本字符串
  • 复杂数据结构
  • 文本和二进制流数据
  • 应用程序资源

简单的文本数据直接存储在剪贴板中,而复杂数据则存储为一个引用,粘贴应用程序使用内容提供程序解析该引用。

复制和粘贴可以在应用程序内以及在实现该框架的应用程序之间进行。

因为框架的一部分使用了内容提供程序,所以本文档假设您已经熟悉 Android 内容提供程序 API

使用文本

某些组件开箱即用地支持复制和粘贴文本,如下表所示。

组件 复制文本 粘贴文本
BasicTextField
TextField
SelectionContainer

例如,您可以将卡片中的文本复制到剪贴板(在下面的代码片段中),并将复制的文本粘贴到 TextField 中。您可以通过在 TextField 上触摸并按住或点击光标句柄来显示粘贴文本的菜单。

val textFieldState = rememberTextFieldState()

Column {
    Card {
        SelectionContainer {
            Text("You can copy this text")
        }
    }
    BasicTextField(state = textFieldState)
}

您可以使用以下键盘快捷键粘贴文本:Ctrl+V。此键盘快捷键默认情况下也可用。有关详细信息,请参阅 处理键盘操作

使用 ClipboardManager 复制

您可以使用 ClipboardManager 将文本复制到剪贴板。其 setText() 方法将传入的 String 对象复制到剪贴板。以下代码片段在用户单击按钮时将“Hello, clipboard”复制到剪贴板。

// Retrieve a ClipboardManager object
val clipboardManager = LocalClipboardManager.current

Button(
    onClick = {
        // Copy "Hello, clipboard" to the clipboard
        clipboardManager.setText("Hello, clipboard")
    }
) {
   Text("Click to copy a text")
}

以下代码片段执行相同的操作,但为您提供了更精细的控制。一个常见的用例是 复制敏感内容,例如密码。ClipEntry 描述剪贴板上的一个项目。它包含一个 ClipData 对象,该对象描述剪贴板上的数据。ClipData.newPlainText() 方法是一个方便的方法,用于从 String 对象创建一个 ClipData 对象。您可以通过调用 setClip() 方法(在 ClipboardManager 对象上)来将创建的 ClipEntry 对象设置为剪贴板。

// Retrieve a ClipboardManager object
val clipboardManager = LocalClipboardManager.current

Button(
    onClick = {
        val clipData = ClipData.newPlainText("plain text", "Hello, clipboard")
        val clipEntry = ClipEntry(clipData)
        clipboardManager.setClip(clipEntry)
    }
) {
   Text("Click to copy a text")
}

使用 ClipboardManager 粘贴

您可以通过在 ClipboardManager 对象上调用 getText() 方法来访问复制到剪贴板的文本。当文本复制到剪贴板时,其 getText() 方法返回一个 AnnotatedString 对象。以下代码片段将剪贴板中的文本添加到 TextField 中的文本。

var textFieldState = rememberTextFieldState()

Column {
    TextField(state = textFieldState)

    Button(
        onClick = {
            // The getText method returns an AnnotatedString object or null
            val annotatedString = clipboardManager.getText()
            if(annotatedString != null) {
                // The pasted text is placed on the tail of the TextField
                textFieldState.edit {
                    append(text.toString())
                }
            }
        }
    ) {
        Text("Click to paste the text in the clipboard")
    }
}

使用富媒体内容

用户喜欢图像、视频和其他富有表现力的内容。您的应用可以使用 ClipboardManagerClipEntry 允许用户复制富媒体内容。contentReceiver 修饰符可帮助您实现富媒体内容的粘贴。

复制富媒体内容

您的应用无法直接将富媒体内容复制到剪贴板。相反,您的应用将 URI 对象传递到剪贴板,并使用 ContentProvider 提供对内容的访问。以下代码片段演示如何将 JPEG 图像复制到剪贴板。有关详细信息,请参阅 复制数据流

// Get a reference to the context
val context = LocalContext.current

Button(
    onClick = {
        // URI of the copied JPEG data
        val uri = Uri.parse("content://your.app.authority/0.jpg")
        // Create a ClipData object from the URI value
        // A ContentResolver finds a proper ContentProvider so that ClipData.newUri can set appropriate MIME type to the given URI
        val clipData = ClipData.newUri(context.contentResolver, "Copied", uri)
        // Create a ClipEntry object from the clipData value
        val clipEntry = ClipEntry(clipData)
        // Copy the JPEG data to the clipboard
        clipboardManager.setClip(clipEntry)
    }
) {
    Text("Copy a JPEG data")
}

粘贴富媒体内容

使用 contentReceiver 修饰符,您可以处理将富媒体内容粘贴到修改后的组件中的 BasicTextField。以下代码片段将粘贴的图像数据的 URI 添加到 Uri 对象列表中。

// A URI list of images
val imageList by remember{ mutableListOf<Uri>() }

// Remember the ReceiveContentListener object as it is created inside a Composable scope
val receiveContentListener = remember {
    ReceiveContentListener { transferableContent ->
        // Handle the pasted data if it is image data
        when {
            // Check if the pasted data is an image or not
            transferableContent.hasMediaType(MediaType.Image)) -> {
                // Handle for each ClipData.Item object
                // The consume() method returns a new TransferableContent object containging ignored ClipData.Item objects
                transferableContent.consume { item ->
                    val uri = item.uri
                    if (uri != null) {
                        imageList.add(uri)
                    }
                   // Mark the ClipData.Item object consumed when the retrieved URI is not null
                    uri != null
                }
            }
            // Return the given transferableContent when the pasted data is not an image
            else -> transferableContent
        }
    }
}

val textFieldState = rememberTextFieldState()

BasicTextField(
    state = textFieldState,
    modifier = Modifier
        .contentReceiver(receiveContentListener)
        .fillMaxWidth()
        .height(48.dp)
)

contentReceiver 修饰符以 ReceiveContentListener 对象作为参数,并在用户将数据粘贴到修改后的组件内的 BasicTextField 时调用传入对象的 onReceive 方法。

一个 TransferableContent 对象传递给 onReceive 方法,该对象描述在这种情况下可以通过粘贴在应用之间传输的数据。您可以通过引用 clipEntry 属性来访问 ClipEntry 对象。

例如,当用户选择多张图像并将它们复制到剪贴板时,ClipEntry 对象可以有多个 ClipData.Item 对象。您应该为每个 ClipData.Item 对象标记已使用或已忽略,并返回包含已忽略 ClipData.Item 对象的 TransferableContent,以便最近的祖先 contentReceiver 修饰符可以接收它。

TransferableContent.hasMediaType() 方法可以帮助您确定 TransferableContent 对象是否可以提供具有媒体类型的项目。例如,以下方法调用如果 TransferableContent 对象可以提供图像,则返回 true

transferableContent.hasMediaType(MediaType.Image)

使用复杂数据

您可以按照处理富媒体内容的方式将复杂数据复制到剪贴板。有关详细信息,请参阅 使用内容提供程序复制复杂数据

您也可以按照处理富媒体内容的方式处理复杂数据的粘贴。您可以接收粘贴数据的 URI。实际数据可以从 ContentProvider 中检索。有关更多信息,请参阅 从提供程序检索数据

复制内容的反馈

用户希望在将内容复制到剪贴板时获得反馈,因此除了支持复制和粘贴的框架之外,Android 在 Android 13(API 级别 33)及更高版本中向用户显示默认 UI。由于此功能,存在重复通知的风险。您可以在 避免重复通知 中了解有关此极端情况的更多信息。

An animation showing Android 13 clipboard notification
图 1. Android 13 及更高版本中内容进入剪贴板时显示的 UI。

在 Android 12L(API 级别 32)及更低版本中,手动向用户提供复制反馈。请参阅 建议

敏感内容

如果您选择让您的应用允许用户将敏感内容(例如密码)复制到剪贴板,您的应用必须让系统知道,以便系统可以避免在 UI 中显示复制的敏感内容(图 2)。

Copied text preview flagging sensitive content.
图 2. 带有敏感内容标记的复制文本预览。

ClipboardManager 对象上调用 setClip() 方法之前,必须向 ClipData 中的 ClipDescription 添加标志。

// If your app is compiled with the API level 33 SDK or higher.
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
    }
}

// If your app is compiled with a lower SDK.
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean("android.content.extra.IS_SENSITIVE", true)
    }
}