复制和粘贴

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

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

简单的文本数据直接存储在剪贴板中, 而复杂数据则作为引用存储 应用通过 content provider 进行解析。

复制和粘贴既可以在应用内进行,也可以在应用间进行 实施该框架的应用。

由于该框架的一部分使用内容提供程序 本文档假定您对 Android Content Provider 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, 剪贴板” 复制到剪贴板

// 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 对象。您可以通过对 ClipboardManager 对象调用 setClip() 方法,将创建的 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)

处理复杂数据

您可以将复杂数据复制到剪贴板 处理方式与处理富媒体内容相同。 如需了解详情,请参阅使用 content provider 复制复杂的数据

你还可以处理粘贴复杂的数据 以同样的方式呈现富媒体内容 您可以接收粘贴数据的 URI。实际数据可以从 ContentProvider 中检索。 如需了解详情,请参阅从提供程序检索数据

复制内容的反馈

用户希望在将内容复制到剪贴板时获得反馈,因此除了支持复制和粘贴的框架之外,在 Android 13(API 级别 33)及更高版本中进行复制时,Android 还会向用户显示一个默认界面。因为此功能,有出现重复通知的风险。 如需详细了解此极端情况,请参阅避免重复通知

展示 Android 13 剪贴板通知的动画
图 1. 在 Android 13 及更高版本中,内容进入剪贴板时显示的界面。

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

敏感内容

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

所复制文本的预览(已标记敏感内容)。
图 2. 所复制文本的预览(带有敏感内容标记)。

您必须先向 ClipData 中的 ClipDescription 添加标志,然后才能对 ClipboardManager 对象调用 setClip() 方法:

// 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)
    }
}