Google 致力于为黑人社区推动种族平等。查看具体举措

复制和粘贴

Android 提供了一个强大的基于剪贴板的框架,用于复制和粘贴。它支持简单和复杂的数据类型,包括文本字符串、复杂数据结构、文本和二进制流数据甚至是应用资产。简单的文本数据直接存储在剪贴板中,而复杂的数据则存储为参考,粘贴应用使用内容提供程序对其进行解析。复制和粘贴既可以在应用内进行,也可以在实现了框架的应用之间进行。

由于该框架的一部分使用内容提供程序,因此本主题假设您对 Android 内容提供程序 API 已有一定了解。我们在内容提供程序这一主题中对该 API 进行了介绍。

剪贴板框架

使用剪贴板框架时,您会将数据放入一个剪切对象中,然后将该剪切对象放到系统级剪贴板中。剪切对象可以采用以下三种形式之一:

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

剪贴板一次只保留一个剪切对象。当应用将一个剪切对象放到剪贴板时,上一个剪切对象会消失。

如果您希望允许用户将数据粘贴到您的应用中,则无需处理所有类型的数据。您可以先检查剪贴板中的数据,然后再向用户提供粘贴数据的选项。除了具有特定的数据形式之外,剪切对象还包含可告知您可用的 MIME 类型的元数据。该元数据可以帮助您确定应用是否可以利用剪贴板数据做一些有用的事情。例如,如果您有一个主要处理文本的应用,则可能需要忽略包含 URI 或 Intent 的剪切对象。

此外,您可能还希望无论剪贴板中的数据采用何种形式,用户都可以粘贴文本。为此,您可以将剪贴板数据强制转换为文本表示形式,然后粘贴相应文本。将剪贴板数据强制转换为文本部分对这一点进行了介绍。

剪贴板类

这一部分介绍了剪贴板框架所使用的类。

ClipboardManager

在 Android 系统中,系统剪贴板由全局 ClipboardManager 类表示。您不能直接实例化此类;而是应通过调用 getSystemService(CLIPBOARD_SERVICE) 获取对它的引用。

ClipData、ClipData.Item 和 ClipDescription

要将数据添加到剪贴板,您需要创建一个 ClipData 对象,其中包含数据的说明和数据本身。剪贴板一次只保留一个 ClipDataClipData 包含一个 ClipDescription 对象以及一个或多个 ClipData.Item 对象。

ClipDescription 对象包含关于剪切的元数据。具体来说,它包含剪切的数据的可用 MIME 类型数组。当您将一个剪切放到剪贴板中时,系统会向粘贴应用提供该数组。粘贴应用可以检查该数组,以了解自己能否处理任何可用 MIME 类型。

ClipData.Item 对象包含 Text、URI 或 Intent 数据:

Text
一个 CharSequence
URI
一个 Uri。尽管允许使用任何 URI,但它通常包含内容提供程序 URI。提供数据的应用将 URI 放到剪贴板中。要粘贴数据的应用从剪贴板获取 URI,并使用它来访问内容提供程序(或其他数据源)以及检索数据。
Intent
一个 Intent。通过该数据类型,您可以将应用快捷方式复制到剪贴板。然后,用户可以将快捷方式粘贴到自己的应用中以供日后使用。

您可以向一个剪切添加多个 ClipData.Item 对象。这让用户可以将多个选择复制和粘贴为一个剪切。例如,如果您有一个列表微件,可让用户一次选择多个项,则您可以同时将所有这些项复制到剪贴板。为此,您需要分别为每个列表项创建一个 ClipData.Item,然后将 ClipData.Item 对象添加到 ClipData 对象。

ClipData 便捷方法

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

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 中的数据的形式:

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

图 1 汇总了剪贴板框架。要复制数据,应用需要将一个 ClipData 对象放到 ClipboardManager 全局剪贴板中。ClipData 包含一个或多个 ClipData.Item 对象和一个 ClipDescription 对象。要粘贴数据,应用需要获取 ClipData,从 ClipDescription 获取其 MIME 类型,然后从 ClipData.ItemClipData.Item 引用的内容提供程序获取数据。

复制和粘贴框架的程序块示意图

图 1.Android 剪贴板框架

复制到剪贴板

如前所述,要将数据复制到剪贴板,您需要获取全局 ClipboardManager 对象的句柄,创建一个 ClipData 对象,向其中添加一个 ClipDescription 和一个或多个 ClipData.Item 对象,然后将已完成的 ClipData 对象添加到 ClipboardManager 对象。以下过程对此进行了详细介绍:

  1. 如果您要使用内容 URI 复制数据,请设置一个内容提供程序。

    Note Pad 示例应用是使用内容提供程序进行复制和粘贴的一个例子。NotePadProvider 类实现了相应内容提供程序。NotePad 类定义了提供程序与其他应用之间的约定,包括受支持的 MIME 类型。

  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 对象:

    • 对于 Text

      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 based on 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 that you use 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 based on 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 that you use 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

      以下代码段为应用构建一个 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.primaryClip = clip
        

    Java

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

从剪贴板粘贴

如前所述,您可以通过以下方法从剪贴板中粘贴数据:获取全局剪贴板对象,获取剪切对象,查看其数据,然后将数据从剪切对象复制到您自己的存储空间(如果可以)。这一部分详细介绍了如何针对这三种形式的剪贴板数据执行该操作。

粘贴纯文本

要粘贴纯文本,首先请获取全局剪贴板并验证它能否返回纯文本。 然后获取剪切对象,并使用 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. 接下来,确定您是否应在当前 Activity 中启用或停用“粘贴”选项。您应验证剪贴板是否包含剪切,以及您是否可以处理剪切所代表的数据类型:

    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 if you can handle the data.
        pasteItem.isEnabled = when {
            !clipboard.hasPrimaryClip() -> {
                false
            }
            !(clipboard.primaryClipDescription.hasMimeType(MIMETYPE_TEXT_PLAIN)) -> {
                // This disables the paste menu item, since the clipboard has data but it is not plain text
                false
            }
            else -> {
                // This 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 if you can handle the data.
        if (!(clipboard.hasPrimaryClip())) {
    
            pasteItem.setEnabled(false);
    
        } else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) {
    
            // This disables the paste menu item, since the clipboard has data but it is not plain text
            pasteItem.setEnabled(false);
        } else {
    
            // This 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() does not 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 does not 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 is not
                        // presented here.
                        pasteData = resolveUri(pasteUri)
                        true
                    } else {
    
                        // Something is wrong. The MIME type was plain text, but the clipboard does not
                        // contain either 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 does not 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 is not
                // presented here.
                pasteData = resolveUri(Uri);
                return true;
            } else {
    
                // Something is wrong. The MIME type was plain text, but the clipboard does not contain either
                // 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
    
        val pasteUri: Uri? = 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
            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
            val  uriMimeType: String? = pasteUri?.let {
    
                // Is this a content URI?
                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 is not 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 will vary according to the
                            // format of the data model.
                        }
    
                        // Kotlin `use` will automatically close the Cursor
                    }
                }
            }
        }
        

    Java

                // If the return value is not 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 will vary according to the
                            // format of the data model.
                            }
                        }
    
                        // close the Cursor
                        pasteCursor.close();
                     }
                 }
             }
        }
        

粘贴 Intent

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

Kotlin

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

    // Checks to see if the clip item contains an Intent, by testing to see if 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 your application was expecting an Intent to be
        // on the clipboard
    }
    

Java

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

    // Checks to see if the clip item contains an Intent, by testing to see if 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 your application was expecting an Intent to be
        // on the clipboard
    }
    

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

内容提供程序支持复制数据库记录或文件流等复杂的数据。要复制这类数据,您需要将一个内容 URI 放到剪贴板中。然后,粘贴应用会从剪贴板获取该 URI,并使用它来检索数据库数据或文件流描述符。

由于粘贴应用只有您的数据的内容 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。

如果您已经在使用内容提供程序、内部数据库或内部表来整理数据,那么通常会使用该编码方法。在这些情况下,您有多份要复制的数据,且可能每份数据都有一个唯一标识符。为响应来自粘贴应用的查询,您可以按数据标识符查找数据并返回。

如果您没有多份数据,则可能不需要对标识符进行编码。 您可以仅使用一个专属于您的提供程序的 URI。为响应查询,您的提供程序会返回它当前包含的数据。

Note Pad 示例应用使用按 ID 获取单条记录的方法,从记事列表打开某条记事。该示例使用的是 SQL 数据库中的 _id 字段,但您可以根据自己的偏好使用任何数字标识符或字符标识符。

复制数据结构

您应设置一个用于复制和粘贴复杂数据的内容提供程序作为 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. 在用户从中复制数据的 Activity 中,设置会将数据复制到剪贴板的代码。为响应复制请求,将 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)
    
                // Set the clipboard's primary clip.
                clipboard.primaryClip = 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);
    
            // Set 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 -> {
    
                    // query and return the contact for the requested name. Here you would decode
                    // the incoming URI, query the data model based on the last name, and return 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:
    
                // query and return the contact for the requested name. Here you would decode
                // the incoming URI, query the data model based on the last name, and return 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 文件描述符方法的调用中使用该 URI。ContentResolver 方法会调用相应的 ContentProvider 方法,并向其传递内容 URI。您的提供程序会将文件描述符返回到 ContentResolver 方法。然后,粘贴应用负责读取流中的数据。

以下列表显示了内容提供程序最重要的文件描述符方法。每一种方法都有一个对应的 ContentResolver 方法,方法名称后面附加了字符串“Descriptor”;例如,openAssetFile() 对应的 ContentResolver 方法为 openAssetFileDescriptor()

openTypedAssetFile()
此方法应返回一个资源文件描述符,但前提是提供程序支持提供的 MIME 类型。调用程序(执行解析的应用)会提供一个 MIME 类型模式。如果内容提供程序(所有者为已将 URI 复制到剪贴板的应用)可以提供该 MIME 类型,则会返回一个 AssetFileDescriptor 文件句柄;如果无法提供,则会抛出异常。

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

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

您可以根据需要将 openPipeHelper() 方法与您的文件描述符方法配合使用。这让粘贴应用可以使用管道在后台线程中读取流数据。要使用此方法,您需要实现 ContentProvider.PipeDataWriter 接口。Note Pad 示例应用提供了一个执行此操作的示例,位于 NotePadProvider.javaopenTypedAssetFile() 方法中。

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

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

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