复制和粘贴

Android 提供了一个强大的基于剪贴板的框架,用于复制和粘贴。它支持简单和复杂的数据类型,包括文本字符串、复杂数据结构、文本和二进制流数据以及应用程序资产。简单文本数据直接存储在剪贴板中,而复杂数据则存储为粘贴应用程序使用内容提供程序解析的引用。复制和粘贴在应用程序内部和在实现该框架的应用程序之间都有效。

由于框架的一部分使用内容提供程序,因此本文档假定您对 Android 内容提供程序 API 有所了解,该 API 在 内容提供程序 中有描述。

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

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

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

剪贴板框架

使用剪贴板框架时,将数据放入剪贴板对象,然后将剪贴板对象放在系统范围的剪贴板上。剪贴板对象可以采用三种形式之一

文本
文本字符串。将字符串直接放入剪贴板对象,然后将其放在剪贴板上。要粘贴字符串,请从剪贴板中获取剪贴板对象,并将字符串复制到应用程序的存储区中。
URI
一个表示任何形式的 URI 的 Uri 对象。这主要用于从内容提供程序复制复杂数据。要复制数据,请将 Uri 对象放入剪贴板对象,然后将剪贴板对象放在剪贴板上。要粘贴数据,请获取剪贴板对象,获取 Uri 对象,将其解析为数据源(例如内容提供程序),然后将数据从源复制到应用程序的存储区中。
意图
一个 Intent。这支持复制应用程序快捷方式。要复制数据,请创建一个 Intent,将其放入剪贴板对象,然后将剪贴板对象放在剪贴板上。要粘贴数据,请获取剪贴板对象,然后将 Intent 对象复制到应用程序的内存区域中。

剪贴板一次只能保存一个剪贴板对象。当应用程序将剪贴板对象放在剪贴板上时,之前的剪贴板对象会消失。

如果您想让用户将数据粘贴到您的应用程序中,则不必处理所有类型的数据。您可以在为用户提供粘贴选项之前检查剪贴板上的数据。除了具有特定数据形式外,剪贴板对象还包含元数据,告诉您哪些 MIME 类型可用。这些元数据可以帮助您确定您的应用程序是否可以对剪贴板数据执行有用的操作。例如,如果您有一个主要处理文本的应用程序,您可能希望忽略包含 URI 或意图的剪贴板对象。

您可能还想让用户无论剪贴板上的数据形式如何,都能粘贴文本。为此,请将剪贴板数据强制转换为文本表示形式,然后粘贴该文本。这在 将剪贴板强制转换为文本 部分中进行了描述。

剪贴板类

本节介绍剪贴板框架使用的类。

ClipboardManager

Android 系统剪贴板由全局 ClipboardManager 类表示。不要直接实例化此类。相反,请通过调用 getSystemService(CLIPBOARD_SERVICE) 获取对其的引用。

ClipData、ClipData.Item 和 ClipDescription

要向剪贴板添加数据,请创建一个包含数据描述和数据本身的 ClipData 对象。剪贴板一次只保存一个 ClipData。一个 ClipData 包含一个 ClipDescription 对象和一个或多个 ClipData.Item 对象。

一个 ClipDescription 对象包含有关剪贴板的元数据。特别是,它包含剪贴板数据的可用 MIME 类型的数组。此外,在 Android 12(API 级别 31)及更高版本上,元数据包括有关该对象是否包含 带样式的文本 以及有关 对象中文本类型 的信息。当您将剪贴板放在剪贴板上时,这些信息可用于粘贴应用程序,它们可以检查自己是否可以处理剪贴板数据。

一个 ClipData.Item 对象包含文本、URI 或意图数据

文本
一个 CharSequence
URI
一个 Uri。这通常包含内容提供程序 URI,尽管允许任何 URI。提供数据的应用程序将 URI 放在剪贴板上。想要粘贴数据的应用程序从剪贴板获取 URI,并使用它访问内容提供程序或其他数据源并检索数据。
意图
一个 Intent。此数据类型允许您将应用程序快捷方式复制到剪贴板。用户随后可以将快捷方式粘贴到其应用程序中以供日后使用。

您可以将多个 ClipData.Item 对象添加到一个剪贴板中。这允许用户将多个选择作为单个剪贴板复制和粘贴。例如,如果您有一个列表小部件,它允许用户一次选择多个项目,您可以将所有项目一次复制到剪贴板。为此,请为每个列表项创建一个单独的 ClipData.Item,然后将 ClipData.Item 对象添加到 ClipData 对象中。

ClipData 便利方法

ClipData 类提供静态便利方法,用于使用单个 ClipData.Item 对象和一个简单的 ClipDescription 对象创建 ClipData 对象

newPlainText(label, text)
返回一个 ClipData 对象,其单个 ClipData.Item 对象包含文本字符串。ClipDescription 对象的标签设置为 labelClipDescription 中的单个 MIME 类型是 MIMETYPE_TEXT_PLAIN

使用 newPlainText() 从文本字符串创建剪贴板。

newUri(resolver, label, URI)
返回一个 ClipData 对象,其单个 ClipData.Item 对象包含一个 URI。ClipDescription 对象的标签设置为 label。如果 URI 是内容 URI,即如果 Uri.getScheme() 返回 content:,则该方法使用在 resolver 中提供的 ContentResolver 对象从内容提供程序检索可用的 MIME 类型。然后,它将它们存储在 ClipDescription 中。对于不是 content: URI 的 URI,该方法将 MIME 类型设置为 MIMETYPE_TEXT_URILIST

使用 newUri() 从 URI(尤其是 content: URI)创建剪贴板。

newIntent(label, intent)
返回一个 ClipData 对象,其单个 ClipData.Item 对象包含一个 IntentClipDescription 对象的标签设置为 label。MIME 类型设置为 MIMETYPE_TEXT_INTENT

使用 newIntent()Intent 对象创建剪贴板。

将剪贴板数据强制转换为文本

即使您的应用程序只处理文本,您也可以通过使用 ClipData.Item.coerceToText() 方法转换非文本数据来从剪贴板复制非文本数据。

此方法将 ClipData.Item 中的数据转换为文本,并返回一个 CharSequenceClipData.Item.coerceToText() 返回的值取决于 ClipData.Item 中数据的形式。

文本
如果 ClipData.Item 是文本——也就是说,如果 getText() 不为空——coerceToText() 返回文本。
URI
如果 ClipData.Item 是 URI——也就是说,如果 getUri() 不为空——coerceToText() 会尝试将其用作内容 URI。
  • 如果 URI 是内容 URI 且提供程序可以返回文本流,则 coerceToText() 返回文本流。
  • 如果 URI 是内容 URI 但提供程序不提供文本流,则 coerceToText() 返回 URI 的表示形式。此表示形式与 Uri.toString() 返回的表示形式相同。
  • 如果 URI 不是内容 URI,则 coerceToText() 返回 URI 的表示形式。此表示形式与 Uri.toString() 返回的表示形式相同。
意图
如果 ClipData.Item 是一个 Intent——也就是说,如果 getIntent() 不为空——coerceToText() 将其转换为 Intent URI 并返回。此表示形式与 Intent.toUri(URI_INTENT_SCHEME) 返回的表示形式相同。

剪贴板框架总结如图 2 所示。要复制数据,应用程序将一个 ClipData 对象放置在 ClipboardManager 全局剪贴板上。 ClipData 包含一个或多个 ClipData.Item 对象和一个 ClipDescription 对象。要粘贴数据,应用程序获取 ClipData,从 ClipDescription 获取其 MIME 类型,并从 ClipData.ItemClipData.Item 所引用的内容提供程序获取数据。

An image showing a block diagram of the copy and paste framework
图 2. Android 剪贴板框架。

复制到剪贴板

要将数据复制到剪贴板,请获取全局 ClipboardManager 对象的句柄,创建一个 ClipData 对象,并向其中添加一个 ClipDescription 和一个或多个 ClipData.Item 对象。然后,将完成的 ClipData 对象添加到 ClipboardManager 对象中。以下过程对此进行了进一步说明

  1. 如果使用内容 URI 复制数据,请设置内容提供程序。
  2. 获取系统剪贴板

    Kotlin

    when(menuItem.itemId) {
        ...
        R.id.menu_copy -> { // if the user selects copy
            // Gets a handle to the clipboard service.
            val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
        }
    }
    

    Java

    ...
    // If the user selects copy.
    case R.id.menu_copy:
    
    // Gets a handle to the clipboard service.
    ClipboardManager clipboard = (ClipboardManager)
            getSystemService(Context.CLIPBOARD_SERVICE);
    
  3. 将数据复制到新的 ClipData 对象

    • 对于文本

      Kotlin

      // Creates a new text clip to put on the clipboard.
      val clip: ClipData = ClipData.newPlainText("simple text", "Hello, World!")
      

      Java

      // Creates a new text clip to put on the clipboard.
      ClipData clip = ClipData.newPlainText("simple text", "Hello, World!");
      
    • 对于 URI

      此代码段通过将记录 ID 编码到提供程序的内容 URI 上来构造 URI。此技术在 URI 上编码标识符 部分中进行了更详细的介绍。

      Kotlin

      // Creates a Uri using a base Uri and a record ID based on the contact's last
      // name. Declares the base URI string.
      const val CONTACTS = "content://com.example.contacts"
      
      // Declares a path string for URIs, used to copy data.
      const val COPY_PATH = "/copy"
      
      // Declares the Uri to paste to the clipboard.
      val copyUri: Uri = Uri.parse("$CONTACTS$COPY_PATH/$lastName")
      ...
      // Creates a new URI clip object. The system uses the anonymous
      // getContentResolver() object to get MIME types from provider. The clip object's
      // label is "URI", and its data is the Uri previously created.
      val clip: ClipData = ClipData.newUri(contentResolver, "URI", copyUri)
      

      Java

      // Creates a Uri using a base Uri and a record ID based on the contact's last
      // name. Declares the base URI string.
      private static final String CONTACTS = "content://com.example.contacts";
      
      // Declares a path string for URIs, used to copy data.
      private static final String COPY_PATH = "/copy";
      
      // Declares the Uri to paste to the clipboard.
      Uri copyUri = Uri.parse(CONTACTS + COPY_PATH + "/" + lastName);
      ...
      // Creates a new URI clip object. The system uses the anonymous
      // getContentResolver() object to get MIME types from provider. The clip object's
      // label is "URI", and its data is the Uri previously created.
      ClipData clip = ClipData.newUri(getContentResolver(), "URI", copyUri);
      
    • 对于意图

      此代码段为应用程序构建一个 Intent,然后将其放入剪贴对象中

      Kotlin

      // Creates the Intent.
      val appIntent = Intent(this, com.example.demo.myapplication::class.java)
      ...
      // Creates a clip object with the Intent in it. Its label is "Intent"
      // and its data is the Intent object created previously.
      val clip: ClipData = ClipData.newIntent("Intent", appIntent)
      

      Java

      // Creates the Intent.
      Intent appIntent = new Intent(this, com.example.demo.myapplication.class);
      ...
      // Creates a clip object with the Intent in it. Its label is "Intent"
      // and its data is the Intent object created previously.
      ClipData clip = ClipData.newIntent("Intent", appIntent);
      
  4. 将新的剪贴对象放到剪贴板上

    Kotlin

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip)
    

    Java

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip);
    

在复制到剪贴板时提供反馈

用户希望在应用程序将内容复制到剪贴板时获得视觉反馈。对于 Android 13 及更高版本的用户,此操作会自动完成,但在早期版本中必须手动实现。

从 Android 13 开始,系统会在内容添加到剪贴板时显示标准视觉确认。新的确认将执行以下操作

  • 确认内容已成功复制。
  • 提供已复制内容的预览。

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

在 Android 12L(API 级别 32)及更低版本中,用户可能不确定他们是否成功复制了内容或他们复制了什么内容。此功能标准化了应用程序在复制后显示的各种通知,并为用户提供了更多关于剪贴板的控制权。

避免重复通知

在 Android 12L(API 级别 32)及更低版本中,我们建议在成功复制时通过发出视觉的应用内反馈来提醒用户,使用小部件(如 ToastSnackbar)在复制后进行操作。

为避免重复显示信息,我们强烈建议在 Android 13 及更高版本中删除在应用内复制后显示的吐司或零食栏。

Post snackbar after an in-app copy.
图 4. 如果在 Android 13 中显示复制确认零食栏,用户将看到重复消息。
Post toast after an in-app copy.
图 5. 如果在 Android 13 中显示复制确认吐司,用户将看到重复消息。

以下是如何实现此功能的示例

fun textCopyThenPost(textCopied:String) {
    val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
    // When setting the clipboard text.
    clipboardManager.setPrimaryClip(ClipData.newPlainText   ("", textCopied))
    // Only show a toast for Android 12 and lower.
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
        Toast.makeText(context, “Copied”, Toast.LENGTH_SHORT).show()
}

将敏感内容添加到剪贴板

如果您的应用程序允许用户将敏感内容(如密码或信用卡信息)复制到剪贴板,则必须在调用 ClipboardManager.setPrimaryClip() 之前,在 ClipData 中的 ClipDescription 中添加一个标志。添加此标志可以防止敏感内容出现在 Android 13 及更高版本中复制内容的视觉确认中。

Copied text preview without flagging sensitive content
图 6. 没有敏感内容标志的已复制文本预览。
Copied text preview flagging sensitive content.
图 7. 带有敏感内容标志的已复制文本预览。

要标记敏感内容,请将布尔型额外内容添加到 ClipDescription 中。所有应用程序都必须执行此操作,无论目标 API 级别如何。

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

从剪贴板粘贴

如前所述,通过获取全局剪贴板对象、获取剪贴对象、查看其数据,以及如果可能将剪贴对象中的数据复制到您自己的存储区来从剪贴板粘贴数据。本节详细说明了如何粘贴三种形式的剪贴板数据。

粘贴纯文本

要粘贴纯文本,请获取全局剪贴板并验证它是否可以返回纯文本。然后获取剪贴对象,并使用 getText() 将其文本复制到您自己的存储区,如以下过程所述

  1. 使用 getSystemService(CLIPBOARD_SERVICE) 获取全局 ClipboardManager 对象。此外,声明一个全局变量来保存粘贴的文本

    Kotlin

    var clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    var pasteData: String = ""
    

    Java

    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    String pasteData = "";
    
  2. 确定您是否需要在当前活动中启用或禁用“粘贴”选项。验证剪贴板是否包含剪贴以及您是否可以处理剪贴所代表的数据类型

    Kotlin

    // Gets the ID of the "paste" menu item.
    val pasteItem: MenuItem = menu.findItem(R.id.menu_paste)
    
    // If the clipboard doesn't contain data, disable the paste menu item.
    // If it does contain data, decide whether you can handle the data.
    pasteItem.isEnabled = when {
        !clipboard.hasPrimaryClip() -> {
            false
        }
        !(clipboard.primaryClipDescription.hasMimeType(MIMETYPE_TEXT_PLAIN)) -> {
            // Disables the paste menu item, since the clipboard has data but it
            // isn't plain text.
            false
        }
        else -> {
            // Enables the paste menu item, since the clipboard contains plain text.
            true
        }
    }
    

    Java

    // Gets the ID of the "paste" menu item.
    MenuItem pasteItem = menu.findItem(R.id.menu_paste);
    
    // If the clipboard doesn't contain data, disable the paste menu item.
    // If it does contain data, decide whether you can handle the data.
    if (!(clipboard.hasPrimaryClip())) {
    
        pasteItem.setEnabled(false);
    
    } else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) {
    
        // Disables the paste menu item, since the clipboard has data but
        // it isn't plain text.
        pasteItem.setEnabled(false);
    } else {
    
        // Enables the paste menu item, since the clipboard contains plain text.
        pasteItem.setEnabled(true);
    }
    
  3. 从剪贴板复制数据。代码中的此点仅在“粘贴”菜单项启用时才能访问,因此您可以假设剪贴板包含纯文本。您还不知道它是否包含文本字符串或指向纯文本的 URI。以下代码段对此进行了测试,但它只显示了处理纯文本的代码

    Kotlin

    when (menuItem.itemId) {
        ...
        R.id.menu_paste -> {    // Responds to the user selecting "paste".
            // Examines the item on the clipboard. If getText() doesn't return null,
            // the clip item contains the text. Assumes that this application can only
            // handle one item at a time.
            val item = clipboard.primaryClip.getItemAt(0)
    
            // Gets the clipboard as text.
            pasteData = item.text
    
            return if (pasteData != null) {
                // If the string contains data, then the paste operation is done.
                true
            } else {
                // The clipboard doesn't contain text. If it contains a URI,
                // attempts to get data from it.
                val pasteUri: Uri? = item.uri
    
                if (pasteUri != null) {
                    // If the URI contains something, try to get text from it.
    
                    // Calls a routine to resolve the URI and get data from it.
                    // This routine isn't presented here.
                    pasteData = resolveUri(pasteUri)
                    true
                } else {
    
                    // Something is wrong. The MIME type was plain text, but the
                    // clipboard doesn't contain text or a Uri. Report an error.
                    Log.e(TAG,"Clipboard contains an invalid data type")
                    false
                }
            }
        }
    }
    

    Java

    // Responds to the user selecting "paste".
    case R.id.menu_paste:
    
    // Examines the item on the clipboard. If getText() does not return null,
    // the clip item contains the text. Assumes that this application can only
    // handle one item at a time.
     ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
    
    // Gets the clipboard as text.
    pasteData = item.getText();
    
    // If the string contains data, then the paste operation is done.
    if (pasteData != null) {
        return true;
    
    // The clipboard doesn't contain text. If it contains a URI, attempts to get
    // data from it.
    } else {
        Uri pasteUri = item.getUri();
    
        // If the URI contains something, try to get text from it.
        if (pasteUri != null) {
    
            // Calls a routine to resolve the URI and get data from it.
            // This routine isn't presented here.
            pasteData = resolveUri(Uri);
            return true;
        } else {
    
            // Something is wrong. The MIME type is plain text, but the
            // clipboard doesn't contain text or a Uri. Report an error.
            Log.e(TAG, "Clipboard contains an invalid data type");
            return false;
        }
    }
    

从内容 URI 粘贴数据

如果 ClipData.Item 对象包含内容 URI,并且您确定可以处理其 MIME 类型之一,则创建一个 ContentResolver 并调用相应的内容提供程序方法来检索数据。

以下过程介绍了如何从剪贴板上的内容 URI 获取基于内容提供程序的数据。它会检查应用程序可以使用的 MIME 类型是否可以从提供程序获得。

  1. 声明一个全局变量来保存 MIME 类型

    Kotlin

    // Declares a MIME type constant to match against the MIME types offered
    // by the provider.
    const val MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact"
    

    Java

    // Declares a MIME type constant to match against the MIME types offered by
    // the provider.
    public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
    
  2. 获取全局剪贴板。此外,获取一个内容解析器,以便您可以访问内容提供程序

    Kotlin

    // Gets a handle to the Clipboard Manager.
    val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    
    // Gets a content resolver instance.
    val cr = contentResolver
    

    Java

    // Gets a handle to the Clipboard Manager.
    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    
    // Gets a content resolver instance.
    ContentResolver cr = getContentResolver();
    
  3. 从剪贴板获取主剪贴,并将其内容作为 URI 获取

    Kotlin

    // Gets the clipboard data from the clipboard.
    val clip: ClipData? = clipboard.primaryClip
    
    clip?.run {
    
        // Gets the first item from the clipboard data.
        val item: ClipData.Item = getItemAt(0)
    
        // Tries to get the item's contents as a URI.
        val pasteUri: Uri? = item.uri
    

    Java

    // Gets the clipboard data from the clipboard.
    ClipData clip = clipboard.getPrimaryClip();
    
    if (clip != null) {
    
        // Gets the first item from the clipboard data.
        ClipData.Item item = clip.getItemAt(0);
    
        // Tries to get the item's contents as a URI.
        Uri pasteUri = item.getUri();
    
  4. 通过调用 getType(Uri) 来测试 URI 是否是内容 URI。如果 Uri 不指向有效的内容提供程序,则此方法将返回 null。

    Kotlin

        // If the clipboard contains a URI reference...
        pasteUri?.let {
    
            // ...is this a content URI?
            val uriMimeType: String? = cr.getType(it)
    

    Java

        // If the clipboard contains a URI reference...
        if (pasteUri != null) {
    
            // ...is this a content URI?
            String uriMimeType = cr.getType(pasteUri);
    
  5. 测试内容提供程序是否支持应用程序理解的 MIME 类型。如果支持,请调用 ContentResolver.query() 来获取数据。返回值是 Cursor

    Kotlin

            // If the return value isn't null, the Uri is a content Uri.
            uriMimeType?.takeIf {
    
                // Does the content provider offer a MIME type that the current
                // application can use?
                it == MIME_TYPE_CONTACT
            }?.apply {
    
                // Get the data from the content provider.
                cr.query(pasteUri, null, null, null, null)?.use { pasteCursor ->
    
                    // If the Cursor contains data, move to the first record.
                    if (pasteCursor.moveToFirst()) {
    
                        // Get the data from the Cursor here.
                        // The code varies according to the format of the data model.
                    }
    
                    // Kotlin `use` automatically closes the Cursor.
                }
            }
        }
    }
    

    Java

            // If the return value isn't null, the Uri is a content Uri.
            if (uriMimeType != null) {
    
                // Does the content provider offer a MIME type that the current
                // application can use?
                if (uriMimeType.equals(MIME_TYPE_CONTACT)) {
    
                    // Get the data from the content provider.
                    Cursor pasteCursor = cr.query(uri, null, null, null, null);
    
                    // If the Cursor contains data, move to the first record.
                    if (pasteCursor != null) {
                        if (pasteCursor.moveToFirst()) {
    
                        // Get the data from the Cursor here.
                        // The code varies according to the format of the data model.
                        }
                    }
    
                    // Close the Cursor.
                    pasteCursor.close();
                 }
             }
         }
    }
    

粘贴意图

要粘贴意图,首先获取全局剪贴板。检查 ClipData.Item 对象以查看它是否包含一个 Intent。然后调用 getIntent() 将意图复制到您自己的存储区。以下代码段演示了此过程

Kotlin

// Gets a handle to the Clipboard Manager.
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager

// Checks whether the clip item contains an Intent by testing whether
// getIntent() returns null.
val pasteIntent: Intent? = clipboard.primaryClip?.getItemAt(0)?.intent

if (pasteIntent != null) {

    // Handle the Intent.

} else {

    // Ignore the clipboard, or issue an error if
    // you expect an Intent to be on the clipboard.
}

Java

// Gets a handle to the Clipboard Manager.
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);

// Checks whether the clip item contains an Intent, by testing whether
// getIntent() returns null.
Intent pasteIntent = clipboard.getPrimaryClip().getItemAt(0).getIntent();

if (pasteIntent != null) {

    // Handle the Intent.

} else {

    // Ignore the clipboard, or issue an error if
    // you expect an Intent to be on the clipboard.
}

您的应用程序访问剪贴板数据时显示的系统通知

在 Android 12(API 级别 31)及更高版本上,系统通常会在您的应用程序调用 getPrimaryClip() 时显示吐司消息。消息中的文本包含以下格式

APP pasted from your clipboard

如果您的应用程序执行以下操作之一,系统不会显示吐司消息

  • 访问您自己应用程序的 ClipData
  • 重复访问特定应用程序的 ClipData。吐司仅在您的应用程序第一次从该应用程序访问数据时出现。
  • 检索剪贴对象的元数据,例如通过调用 getPrimaryClipDescription() 而不是 getPrimaryClip()

使用内容提供程序复制复杂数据

内容提供程序支持复制复杂数据,例如数据库记录或文件流。要复制数据,请将内容 URI 放置到剪贴板上。然后,粘贴应用程序从剪贴板获取此 URI,并使用它来检索数据库数据或文件流描述符。

由于粘贴应用程序只拥有数据的 content URI,因此它需要知道要检索哪些数据。您可以通过将数据的标识符编码到 URI 本身,或者提供一个返回要复制数据的唯一 URI 来提供此信息。您选择哪种技术取决于数据的组织方式。

以下部分介绍了如何设置 URI、提供复杂数据和提供文件流。这些描述假设您熟悉内容提供程序设计的一般原则。

在 URI 上编码标识符

使用 URI 将数据复制到剪贴板的一种有用技术是在 URI 本身中编码数据的标识符。然后,您的内容提供程序可以从 URI 中获取标识符,并使用它来检索数据。粘贴应用程序不必知道标识符是否存在。它只需要从剪贴板获取您的“引用”——URI 加上标识符——将其提供给您的内容提供程序,并获取回数据。

通常通过将标识符连接到 URI 的末尾来将标识符编码到内容 URI 上。例如,假设您将提供程序 URI 定义为以下字符串

"content://com.example.contacts"

如果要将名称编码到此 URI 上,请使用以下代码段

Kotlin

val uriString = "content://com.example.contacts/Smith"

// uriString now contains content://com.example.contacts/Smith.

// Generates a uri object from the string representation.
val copyUri = Uri.parse(uriString)

Java

String uriString = "content://com.example.contacts" + "/" + "Smith";

// uriString now contains content://com.example.contacts/Smith.

// Generates a uri object from the string representation.
Uri copyUri = Uri.parse(uriString);

如果您已经使用内容提供程序,您可能希望添加一个新的 URI 路径,以指示 URI 用于复制。例如,假设您已经拥有以下 URI 路径

"content://com.example.contacts/people"
"content://com.example.contacts/people/detail"
"content://com.example.contacts/people/images"

您可以添加另一个路径用于复制 URI

"content://com.example.contacts/copying"

然后,您可以通过模式匹配来检测“复制”URI,并使用专门用于复制和粘贴的代码来处理它。

如果您已经使用内容提供程序、内部数据库或内部表来组织数据,通常会使用编码技术。在这些情况下,您有多个想要复制的数据,并且每个数据可能都有一个唯一的标识符。响应粘贴应用程序的查询,您可以通过标识符查找数据并返回它。

如果您没有多个数据片段,则可能不需要对标识符进行编码。您可以使用对您的提供商唯一的 URI。响应查询时,您的提供商将返回它当前包含的数据。

复制数据结构

将内容提供者设置为 ContentProvider 组件的子类,以复制和粘贴复杂数据。对您放在剪贴板上的 URI 进行编码,以便它指向您要提供的精确记录。此外,请考虑应用程序的现有状态

  • 如果您已经拥有内容提供者,则可以添加其功能。您可能只需要修改其 query() 方法以处理来自想要粘贴数据的应用程序的 URI。您可能希望修改该方法以处理“复制” URI 模式。
  • 如果您的应用程序维护内部数据库,您可能希望将此数据库移至内容提供者以方便从中复制。
  • 如果您没有使用数据库,则可以实现一个简单的内容提供者,其唯一目的是为从剪贴板粘贴的应用程序提供数据。

在内容提供者中,至少覆盖以下方法

query()
粘贴应用程序假设它们可以使用此方法和您放在剪贴板上的 URI 获取您的数据。为了支持复制,让此方法检测包含特殊“复制”路径的 URI。然后,您的应用程序可以创建一个要放在剪贴板上的“复制”URI,其中包含复制路径和指向要复制的确切记录的指针。
getType()
此方法必须返回您要复制数据的 MIME 类型。该方法 newUri() 调用 getType() 将 MIME 类型放入新的 ClipData 对象中。

复杂数据的 MIME 类型在 内容提供者 中有描述。

您不需要拥有任何其他内容提供者方法,例如 insert()update()。粘贴应用程序只需要获取您支持的 MIME 类型并从您的提供者复制数据。如果您已经拥有这些方法,它们不会干扰复制操作。

以下代码片段演示了如何设置您的应用程序以复制复杂数据

  1. 在应用程序的全局常量中,声明一个基本 URI 字符串和一个用于标识用于复制数据的 URI 字符串的路径。还要声明复制数据的 MIME 类型。

    Kotlin

    // Declares the base URI string.
    private const val CONTACTS = "content://com.example.contacts"
    
    // Declares a path string for URIs that you use to copy data.
    private const val COPY_PATH = "/copy"
    
    // Declares a MIME type for the copied data.
    const val MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact"
    

    Java

    // Declares the base URI string.
    private static final String CONTACTS = "content://com.example.contacts";
    
    // Declares a path string for URIs that you use to copy data.
    private static final String COPY_PATH = "/copy";
    
    // Declares a MIME type for the copied data.
    public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
    
  2. 在用户从中复制数据的活动中,设置代码以将数据复制到剪贴板。响应复制请求,将 URI 放置在剪贴板上。

    Kotlin

    class MyCopyActivity : Activity() {
        ...
    when(item.itemId) {
        R.id.menu_copy -> { // The user has selected a name and is requesting a copy.
            // Appends the last name to the base URI.
            // The name is stored in "lastName".
            uriString = "$CONTACTS$COPY_PATH/$lastName"
    
            // Parses the string into a URI.
            val copyUri: Uri? = Uri.parse(uriString)
    
            // Gets a handle to the clipboard service.
            val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    
            val clip: ClipData = ClipData.newUri(contentResolver, "URI", copyUri)
    
            // Sets the clipboard's primary clip.
            clipboard.setPrimaryClip(clip)
        }
    }
    

    Java

    public class MyCopyActivity extends Activity {
        ...
    // The user has selected a name and is requesting a copy.
    case R.id.menu_copy:
    
        // Appends the last name to the base URI.
        // The name is stored in "lastName".
        uriString = CONTACTS + COPY_PATH + "/" + lastName;
    
        // Parses the string into a URI.
        Uri copyUri = Uri.parse(uriString);
    
        // Gets a handle to the clipboard service.
        ClipboardManager clipboard = (ClipboardManager)
            getSystemService(Context.CLIPBOARD_SERVICE);
    
        ClipData clip = ClipData.newUri(getContentResolver(), "URI", copyUri);
    
        // Sets the clipboard's primary clip.
        clipboard.setPrimaryClip(clip);
    
  3. 在内容提供者的全局范围内,创建一个 URI 匹配器并添加一个与您放在剪贴板上的 URI 匹配的 URI 模式。

    Kotlin

    // A Uri Match object that simplifies matching content URIs to patterns.
    private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    
        // Adds a matcher for the content URI. It matches.
        // "content://com.example.contacts/copy/*"
        addURI(CONTACTS, "names/*", GET_SINGLE_CONTACT)
    }
    
    // An integer to use in switching based on the incoming URI pattern.
    private const val GET_SINGLE_CONTACT = 0
    ...
    class MyCopyProvider : ContentProvider() {
        ...
    }
    

    Java

    public class MyCopyProvider extends ContentProvider {
        ...
    // A Uri Match object that simplifies matching content URIs to patterns.
    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    
    // An integer to use in switching based on the incoming URI pattern.
    private static final int GET_SINGLE_CONTACT = 0;
    ...
    // Adds a matcher for the content URI. It matches
    // "content://com.example.contacts/copy/*"
    sUriMatcher.addURI(CONTACTS, "names/*", GET_SINGLE_CONTACT);
    
  4. 设置 query() 方法。此方法可以处理不同的 URI 模式,具体取决于您的编码方式,但仅显示用于剪贴板复制操作的模式。

    Kotlin

    // Sets up your provider's query() method.
    override fun query(
            uri: Uri,
            projection: Array<out String>?,
            selection: String?,
            selectionArgs: Array<out String>?,
            sortOrder: String?
    ): Cursor? {
        ...
        // When based on the incoming content URI:
        when(sUriMatcher.match(uri)) {
    
            GET_SINGLE_CONTACT -> {
    
                // Queries and returns the contact for the requested name. Decodes
                // the incoming URI, queries the data model based on the last name,
                // and returns the result as a Cursor.
            }
        }
        ...
    }
    

    Java

    // Sets up your provider's query() method.
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
        String sortOrder) {
        ...
        // Switch based on the incoming content URI.
        switch (sUriMatcher.match(uri)) {
    
        case GET_SINGLE_CONTACT:
    
            // Queries and returns the contact for the requested name. Decodes the
            // incoming URI, queries the data model based on the last name, and
            // returns the result as a Cursor.
        ...
    }
    
  5. 设置 getType() 方法以返回复制数据的适当 MIME 类型

    Kotlin

    // Sets up your provider's getType() method.
    override fun getType(uri: Uri): String? {
        ...
        return when(sUriMatcher.match(uri)) {
            GET_SINGLE_CONTACT -> MIME_TYPE_CONTACT
            ...
        }
    }
    

    Java

    // Sets up your provider's getType() method.
    public String getType(Uri uri) {
        ...
        switch (sUriMatcher.match(uri)) {
        case GET_SINGLE_CONTACT:
            return (MIME_TYPE_CONTACT);
        ...
        }
    }
    

从内容 URI 粘贴数据 部分描述了如何从剪贴板获取内容 URI 并使用它来获取和粘贴数据。

复制数据流

您可以复制和粘贴大量文本和二进制数据作为流。数据可以采用以下形式

  • 存储在实际设备上的文件
  • 来自套接字的流
  • 存储在提供者底层数据库系统中的大量数据

用于数据流的内容提供者使用文件描述符对象(例如 AssetFileDescriptor)而不是 Cursor 对象来访问其数据。粘贴应用程序使用此文件描述符读取数据流。

要设置您的应用程序以使用提供者复制数据流,请执行以下步骤

  1. 为要放在剪贴板上的数据流设置内容 URI。执行此操作的选项包括以下选项
    • 根据 在 URI 上编码标识符 部分中的描述,将数据流的标识符编码到 URI 上,然后在您的提供者中维护一个包含标识符和相应流名称的表。
    • 直接在 URI 上编码流名称。
    • 使用始终从提供者返回当前流的唯一 URI。如果您使用此选项,请记住更新您的提供者以指向不同的流,无论何时您使用 URI 将流复制到剪贴板。
  2. 为计划提供的每种类型的数据流提供 MIME 类型。粘贴应用程序需要此信息来确定它们是否可以将数据粘贴到剪贴板。
  3. 实现返回流的文件描述符的 ContentProvider 方法之一。如果您在内容 URI 上编码标识符,请使用此方法来确定要打开哪个流。
  4. 要将数据流复制到剪贴板,请构造内容 URI 并将其放置在剪贴板上。

要粘贴数据流,应用程序从剪贴板获取剪辑,获取 URI,并在调用打开流的 ContentResolver 文件描述符方法时使用它。该 ContentResolver 方法调用相应的 ContentProvider 方法,将内容 URI 传递给它。您的提供者将文件描述符返回给 ContentResolver 方法。然后,粘贴应用程序有责任从流中读取数据。

以下列表显示了内容提供者最重要的文件描述符方法。每个方法都有一个相应的 ContentResolver 方法,该方法在方法名称后面附加了字符串“Descriptor”。例如,openAssetFile()ContentResolver 等效方法是 openAssetFileDescriptor()

openTypedAssetFile()

此方法返回一个资产文件描述符,但前提是提供者支持提供的 MIME 类型。调用者(执行粘贴操作的应用程序)提供一个 MIME 类型模式。如果复制 URI 到剪贴板的应用程序的内容提供者可以提供该 MIME 类型,则它将返回一个 AssetFileDescriptor 文件句柄,如果它不能提供该 MIME 类型,则会抛出异常。

此方法处理文件的子部分。您可以使用它来读取内容提供者已复制到剪贴板的资产。

openAssetFile()
此方法是 openTypedAssetFile() 的更通用形式。它不会过滤允许的 MIME 类型,但它可以读取文件的子部分。
openFile()
这是 openAssetFile() 的更通用形式。它不能读取文件的子部分。

您可以选择使用 openPipeHelper() 方法以及您的文件描述符方法。这使粘贴应用程序可以使用管道在后台线程中读取流数据。要使用此方法,请实现 ContentProvider.PipeDataWriter 接口。

设计有效的复制和粘贴功能

要为您的应用程序设计有效的复制和粘贴功能,请记住以下几点

  • 在任何时候,剪贴板上都只有一个剪辑。系统中任何应用程序的新复制操作都会覆盖以前的剪辑。由于用户可能会从您的应用程序导航到其他地方并在返回之前复制,因此您不能假设剪贴板包含用户之前在*您的*应用程序中复制的剪辑。
  • 每个剪辑包含多个 ClipData.Item 对象的预期目的是支持多个选择的复制和粘贴,而不是对单个选择的不同形式的引用。通常,您希望一个剪辑中的所有 ClipData.Item 对象都具有相同的形式。也就是说,它们必须全部是简单文本、内容 URI 或 Intent,而不是混合使用。
  • 当您提供数据时,您可以提供不同的 MIME 表示形式。将您支持的 MIME 类型添加到 ClipDescription 中,然后在您的内容提供者中实现 MIME 类型。
  • 当您从剪贴板获取数据时,您的应用程序有责任检查可用的 MIME 类型,然后决定使用哪个 MIME 类型(如果有)。即使剪贴板上有一个剪辑,并且用户请求粘贴,您的应用程序也不必执行粘贴操作。如果 MIME 类型兼容,请执行粘贴操作。您可以使用 coerceToText() 将剪贴板上的数据强制转换为文本。如果您的应用程序支持多个可用的 MIME 类型,您可以让用户选择使用哪一个。