Android为复制和粘贴提供了强大的基于剪贴板的框架。它支持简单和复杂的数据类型,包括文本字符串、复杂的数据结构、文本和二进制流数据、甚至应用成程序的资产。简单的文本数据被保存在剪贴板中,而复杂的数据会保存一个引用,粘贴应用程序会使用内容提供器来解析这个引用。复制和粘贴工作可以在应用程序内部进行,也可以在实现了该框架的两个应用程序之间进行。
因为该框架部分使用了内容提供器,所以本专题会假设大家已经熟悉了Android Content Provider API,这个API在Content Providers专题中被介绍。
在使用剪贴板框架时,你把数据放入一个clip对象,然后把这个clip对象放到系统剪贴板。Clip对象可以是以下三种格式之一:
Text
一个文本字符串,你直接把这个字符串放入clip对象,然后把它放到系统剪贴板上。从剪贴板上获取这个clip对象,就可以把字符串复制到你的应用程序存储器中。
URI
代表人和URI格式的Uri对象。它主要用于从内容提供器中复制复杂的数据。把Uri对象放入一个clip对象,然后把这个clip对象放到系统的剪贴板上来复制数据。从系统剪贴板上获取clip对象,然后获取其中的Uri对象,解析其中的数据源(如内容提供器)并从数据源把数据复制到你的应用程序的存储器中。
Intent
它支持复制应用程序的快捷方式。要复制这种数据,你就要创建一个Intent对象,把它放到一个clip对象中,并这个clip对象放到系统剪贴板上。要粘贴数据,你就要从剪贴板上获取这个clip对象,然后把这个Intent对象放到你的应用程序的内存中。
系统剪贴板每次只会持有一个clip对象。当一个应用程序把一个clip对象放到剪贴板上时,前一个clip对象就会被废弃。
如果你希望允许用户把数据粘贴到你的应用程序中,你不必处理所有的数据类型。你可以在粘贴之前检查剪贴板上的数据,clip对象包含了告诉你MIME类型或可用类型的元数据。这个元数据会帮助你判断你的应用程序是否可以使用剪贴板上的数据。例如,如果你有主要想要处理文本,那么你可以忽略包含URI或Intent对象的clip对象。
你可能还想要让用户粘贴文本而不管剪贴板上的数据的格式。你可以强制把剪贴板数据转换成文本形式,然后粘贴这个文本。
在Android系统中,系统的剪贴板是由一个全局的ClipboardManager类来代表的。你不能直接实例化这个类,相反,要通过调用getSystemService(CLIPBOARD_SERVICE)方法来获得对它的引用。
要把数据添加到剪贴板上,你就要创建一个包含数据描述和数据自身的ClipData对象。剪贴板每次只会持有一个ClipData对象。ClipData对象中包含了一个ClipDescription对象和一个或多个ClipData.Item对象。
ClipDescription对象包含了关于Clip的元数据,尤其是它包含了一个针对剪贴数据有效的MIME类型数组。当你把一个剪贴对象放到剪贴板上时,这个数组对执行粘贴处理的应用程序是有效的,可以用它来检查粘贴应用程序是否能够处理这些有效的MIME类型。
ClipData.Item对象包含了文本、URI或Intent类型的数据:
文本是一个CharSequence类型的字符串
URI是一个Uri对象,尽管任意类型的URI都是允许的,但通常这个对象中包含了一个内容提供器的URI。提供数据的应用程序把URI放到剪贴板上。想要粘贴数据的应用程序会从剪贴板上获取这个URI,并使用它访问内容提供器(或其他的数据源)来获取数据。
Intent是一个Intent对象。这种类型允许你把一个应用程序的快捷方式复制到剪贴板上。然后用户把该快捷方式剪贴到它们的应用程序中留作以后使用。
你可以把多个ClipData.Item对象添加到剪贴板上。这就允许用户把多项选择作为一个单独的剪贴对象。例如,如果你有一个列表窗口,它允许用一次选择多个列表项,你就可以一次性的把所有的选择项目复制到剪贴板上。要做这件事情,你就要给每个列表项分别创建一个ClipData.Item对,然后把每个ClipData.Item对象添加到ClipData对象中。
ClipData类为创建一个带有一个ClipData.Item对象和一个简单的ClipDescription对象的ClipData对象提供了静态的便利的方法:
newPlainText(label, text)
这个方法返回了一个ClipData对象,它带有一个包含文本字符串的ClipData.Item对象。ClipDescription对象的标签被设置为label,MIME类型是MIMETYPE_TEXT_PLAIN
使用newPlainText()方法来创建一个文本字符串类型的剪贴对象。
newUri(resolver, label, URI)
这个方法返回一个ClipData对象,它带有一个包含URI的ClipData.Item对象。ClipDescription对象的标签被设置为label。如果URI是一个内容的URI(Uri.getScheme()方法返回content:),那么这个方法就要使用resolver参数中提供的ContentResolver对象从内容提供器中获取有效的MIME类型,并把它们保存在ClipDescription对象中。对于不是content:类型的URI,这个方法会把MIME类型设置为MIMETYPE_TEXT_URILIST。
使用newUri()方法从一个URI中创建一个剪贴对象,特别是content:类型的。
newIntent(label, intent)
这个方法返回一个ClipData对象,它带有一个单独的包含Intent对象的ClipData.Item对象。ClipDescription对象标签被设置为label,MIME类型是MIMETYPE_TEXT_INTENT。
使用newIntent()方法从一个Intent对象中创建一个剪贴对象。
即使你的应用程序只处理文本,你也可以使用ClipData.Item.coerceToText()方法把剪贴板的非文本数据转换成文本。
这个方法把ClipData.Item中的数据转换成文本,并返回一个CharSequence类型的字符串。ClipData.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。
如前所述,要把数据复制到剪贴板,你需要获取一个全局的ClipboardManager对象,用它再创建一个ClipData对象,然后把一个ClipDescription和一个或多个ClipData.Item对象添加到ClipData对象中,最后把ClipData对象添加回ClipboardManager对象中,详细过程如下:
如果使用内容URI来复制数据,那么就要建立一个内容提供器。
Note Pad示例应用程序使用内容提供器来复制和粘贴数据。NotePadProvider类实现该内容提供器。NotePad类定义提供器和其他应用程序之间的约定,包括支持的MIME类型。
获得系统剪贴板:
...
// if the user selects copy
case R.id.menu_copy:
// Gets a handle to the clipboard service.
ClipboardManager clipboard = (ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
把数据复制到一个新的ClipData对象中:
文本数据ClipData clip =ClipData.newPlainText("simple text","Hello, World!");
URI数据
以下代码把一个记录ID编码到对应的提供器的内容URI上。这项技术会在URI标识编码中详细介绍。
privatestaticfinalString CONTACTS ="content://com.example.contacts";
privatestaticfinalString COPY_PATH ="/copy";
Uri copyUri =Uri.parse(CONTACTS + COPY_PATH +"/"+ lastName);
...
ClipData clip =ClipData.newUri(getContentResolver(),"URI",copyUri);
Intent数据
以下代码构造了一个对应应用程序的Intent对象,然后把它放到剪贴对象中:
// 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);
// Set the clipboard's primary clip.
clipboard.setPrimaryClip(clip);
如前所述,要从剪贴板上粘贴数据,首先就要获取全局的剪贴板对象,再获取其中的剪贴对象,查看是否可能从剪贴对象中把数据复制到你自己的存储中。本节详细介绍如果来粘贴这三种格式的剪贴数据。
要粘贴纯文本数据,首先要或的全局的剪贴板,并确认它可以返回纯文本。然后获得剪贴对象,并使用getText()方法把数据复制到你自己的存储中,详细过程如下:
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
String pasteData = "";
// Gets the ID of the "paste" menu item
MenuItem mPasteItem = 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())){
mPasteItem.setEnabled(false);
}elseif(!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))){
// This disables the paste menu item, since the clipboard has data but it is not plain text
mPasteItem.setEnabled(false);
}else{
// This enables the paste menu item, since the clipboard contains plain text.
mPasteItem.setEnabled(true);
}
}
// 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;
// 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;
}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("Clipboard contains an invalid data type");
return;
}
}
如果ClipData.Item对象包含了一个内容的URI,并你已经确定你可以处理它的MIME类型,那么你就可以创建一个ContentResolver对象,然后调用对应的内容提供器方法来获取数据。
下面介绍如何基于剪贴板上的内容的URI,从内容提供器中获取数据。它要检查来自该提供器的那个MIME类型对于本应用程序是否可用:
// Declares a MIME type constant to match against the MIME types offered by the provider
publicstaticfinalString MIME_TYPE_CONTACT ="vnd.android.cursor.item/vnd.example.contact"
// Gets a handle to the Clipboard Manager
ClipboardManager clipboard =(ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// Gets a content resolver instance
ContentResolver cr = getContentResolver();
// 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();
// If the clipboard contains a URI reference
if(pasteUri !=null){
// Is this a content URI?
String uriMimeType = cr.getType(pasteUri);
// 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对象,首先要获得全局的剪贴板。调用getIntent()方法来检查ClipData.Item对象中是否包含Intent对象。如果包含则把该Intent对象复制到你自己存储器中,如:
// 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:
"content://com.example.contacts"
如果你想要把一个名称编码放到这个URI中,你会使用以下代码:
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路径:
"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提供给内容提供器。在对应的查询中,你的内容提供器会返回当前所包含的数据。
在Note Pad示例应用程序中通过使用一条记录的ID来打开一个来自笔记列表的笔记记录。这个示例程序使用来SQL数据库的_id字段,但是你可以使用任何你所希望的数字或字符标识。
为了复制和粘贴复杂的数据,你可以创建一个继承ContentProvider组件的内容提供器。你还应该把要放到剪贴板上的URI编码,以便它指向你想要提取的数据。另外,你必须要考虑你的应用程序的存在状态:
如果你已经有了一个内容提供器,你可以给它添加功能。你可能只需要修改它的query()方法来处理粘贴数据的应用程序所输入的URI。为了处理“copy”URI的模式,也可能会修改这个方法。
如果你的应用程序维护了一个内部的数据库,你可能想要把这个数据库移到内容提供器中,为从中复制数据提供方便。
如果你当前没有使用数据库,你可以实现一个简单的内容提供器,它的唯一目的就是把数据提供给从剪贴板上粘贴数据的应用程序。
在内容提供器中,你至少要覆盖以下方法:
query()
粘贴数据的应用程序会假设它们可以使用你放入剪贴板中的URI来获取数据。为了支持复制数据,你应该有一个检查URI中是否包含“copy”路径的方法。然后你的应用程序再创建这个要放到剪贴板上的“copy”URI,它包含了复制路径,并指向了你想要复制的正确的记录。
getType()
这个方法应该返回一个MIME类型或者是你要复制的数据的类型。为了把MIME类型放入新的clipData对象,newUri()方法会调用getType()方法。
对于复杂数据的MIME类型在“内容提供器”专题中已经介绍了。
注意,用于复制/粘贴的内容提供器不必其他的任何方法,如insert()或update()。粘贴数据的应用程序只需要获得其所支持的MIME类型,并从你的内容提供器中复制数据。如果你已经有了这些方法,那么在复制操作中不应该引用它们。
以下代码演示了如何创建能够复制复杂数据的应用程序:
// 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"
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);
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);
// 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.
...
}
// Sets up your provider's getType() method.
public String getType(Uri uri) {
...
switch (sUriMatcher.match(uri)) {
case GET_SINGLE_CONTACT:
return (MIME_TYPE_CONTACT);
你可以复制和粘贴被流化的大文本和二进制数据。这种数据可以是以下形式:
保存在实际设备上的的文件;
来自套接字中的数据流;
保存在底层数据库系统中的大数据。
针对数据流的内容提供器使用一个文件描述对象(如AssetFileDescriptor,而不是Cursor对象)来提供对数据的访问。粘贴数据的应用程序会使用这个文件描述对象来读取数据流。
按照以下步骤创建使用提供其来复制数据流的应用程序:
创建一个准备放入剪贴板上的数据流所对应的内容URI。包括以下可选的内容:
给数据流编制一个要放到URI上的标识,然后在包含该标识和对应数据流名称的提供器中维护一个表;
直接把数据流的名称编写到该URI上;
使用能够始终从该提供器中返回当前数据流的唯一URI。如果你选择这种方法,你必须要记住,无论何时,通过URI把数据流复制到剪贴板时,都要更新你的提供器,以便指向不同的数据流。
给你计划提供的每种数据流都提供一个MIME类型。粘贴数据的应用程序需要这个信息来判断它们是否可以粘贴剪贴板上的数据。
实现ContentProvider类中的一个方法,让它返回对应数据流的文件描述对象。如果你把标识编写到了内容的URI上,就可以使用这个方法来判断对应的数据流是否打开。
要把数据流复制到剪贴板上,就要构造该内容的URI并把它放到剪贴板上。
要粘贴一个数据流,应用程序就要从剪贴板上获取剪贴对象和其中的URI,并在调用ContentResolver对象的打开数据流的文件描述对象的方法中使用它。ContentResolver方法会调用对应的ContentProvider方法,并把内容的URI传递给它。你提供器会把这个文件描述对象返回给ContentResolver的方法。然后粘贴数据的应用程序就有能力从数据流中读取该数据了。
下面介绍了对内容提供器来说最为重要的几个文件描述对象方法。其中的每一个方法在ContentResolver对象都有对应的附加了Descriptor字符串的方法名称,例如:ContentReslolver中对应openAssetFile()方法的是openAssetFileDescriptor()方法。
openTypeAssetFile()
如果内容提供器支持所提供的MIME类型,那么这个方法会返回一个资源文件的描述对象。调用者应用提供一个MIME类型模式。如果内容提供器可以提供这个MIME类型, 那么它就会返回一个AssetFileDescriptor文件句柄,否则它会抛出一个异常。
这个方法能够处理文件的内容,你可以使用它来读取内容提供器已经复制到剪贴板中资源。
openAssetFile()
这个方法是比openTypeAssetFile()方法更一般化的形式。它不针对MIME类型来进行过滤,但可以读取文件的内容。
openFile()
这个方法比openAssetFile()方法还要一般化。它不能够文件的内容。
你可以选择使用代用文件描述符方法的openPipeHelper()方法,它允许粘贴数据的应用程序在后台线程中使用管道来读取流式数据。要使用这个方法,你需要实现ContentProvider.PipeDataWriter接口。在Note Pad示例中就是这么做的,详细请看NotePadProvider.java的openTypeAssetFile()方法。
要给你的应用程序设计有效的复制/粘贴功能,就要记住以下几点:
任何时候,在剪贴板上都只有一个剪贴对象。系统中,任何一个新的应用程序的复制操作都会覆写之前的剪贴对象。因为用户在做了复制操作之后离开你的应用程序,因此你不能假设剪贴板中所包含的剪贴对象就是之前在你的应用程序中复制的那个对象。
每个剪切对象中的多个ClipData.Item对象的用途是支持对多个被选择的对象的复制和粘贴,而不是对同一个选择对象应用的不同形式。通常,剪贴对象中的所有的ClipData.Item对象都要要有相同的格式,也就是说,它们应用都是简单的文本、内容URI或Intent对象,而不是混合的。
在你提供数据时,你能够提供不同的MIME说明。把MIME类型添加到ClipDescription对象中,然后在你的内容提供器中实现该MIME类型。
当你从剪贴板中获取数据时,你的应用程序要负责检查MIME类型是否可用,如果可用,然后再决定使用那种类型,否则即使在剪贴板上有剪贴对象,并且用户请求粘贴操作,那么你的应用程序也不会执行粘贴操作。如果MIME类型是兼容,你才应该执行粘贴操作。你可以选择使用coerceToText()方法,把剪贴板上的数据强制转换成文本。如果你的应用程序支持多种可用的MIME类型,那么你可以允许用户来选择使用那种类型。