Android为复制粘贴提供了一个强大的基于剪切板的框架,它支持简单和复杂的数据类型,包括纯文本,复杂的数据结构,二进制流,甚至app资源文件。简单的文本数据直接存储在剪切板中,而复杂的数据则存储的是数据的引用,粘贴对象从content provider中获取数据。复制黏贴可以在应用内部和应用之间工作。
复制黏贴使用content providers,本文假设读者对content provider是熟悉的。
The Clipboard Framework
当你是用剪切板框架的时候,卡发着将数据放入一个剪切对象中,然后将剪切对象放置在系统的剪切板上,剪切对象有以下三种形式:
1)文本:一个文本字符串。开发者可以直接将字符串放进一个剪切对象中,然后放置到剪切板上去。要粘贴字符串,只需要从剪切板上拿到剪切对象,将字符串粘贴到应用的存储上就好;
2)任何形式的URI:这对于从content provider中复制复杂的数据来说非常重要,为了复制数据,你可以将一个Uri对象放入一个剪切对象。粘贴的时候可以解析这个Uri对象。如果是content provider的地址,则解析数据,进行复制;
3)Intent:支持复制应用的快捷方式,为了复制数据,开发者可以创建一个Intent放入剪切对象。
剪切板一次只能存储一个剪切对象。
如果你要允许用户粘贴数据到你的应用,你不必处理所有类型的数据,你在允许用户粘贴之前可以去检测剪切板上数据的类型,如果得不到确定的数据类型,剪切对象也有一个Metadata告诉你是什么类型的数据。Metadata可以帮助你决定你的应用是不是可以用剪切板数据做一些事情。
你或许也会允许用户不管数据格式直接粘贴数据,这种情况下你可以强制剪切板数据转化成文本表示,然后粘贴文本。
Clipboard Classess
这部分讨论剪切板模块将要用到的类。
ClipboardManager
在Android系统中,系统的剪切板是用全局的ClipboardManager类来替代的,开发者不需要直接实例化这个类,而是使用getSystemService(CLIPBOARD_SERVICE).
ClipData,ClipData.Item and ClipDescription
为了往Clipboard上面添加数据,必须创建一个包含数据描述和数据本身的ClipData对象。Clipboard一次只能容纳一个ClipData对象,一个ClipData包含了一个ClipDescription对象和多个ClipData.Item对象。
一个AclipDescription对象化包含了关于剪切板的Metadata,特殊情况下,是一个MIME类型数组。当你在剪切板上放置数据的时候,粘贴数据的应用是可以获取这个数组来检测自己是否能处理其中数据的。一个ClipData.Item包含文本,Uri和Itent。
开发者可以添加不止一个ClipData.Item对象,这允许用户一次复制粘贴多个选择内容,比如你有一个list对象允许用户进行一次选择多个,就可以一次粘贴多个选项。为此你为每一个list item创建一个ClipData.Item对象,然后添加到ClipData对象中去。你可以使用newPlainText,newUri和newIntent方便的创建一些ClipData对象。
Coercing the clipboard data to text
开发者可以使用ClipData.Item.coerceToText()方法将剪切板上的内容强制转化为字符串,返回一个CharSequence对象。
1)文本:则直接返回文本;
2)URI:如果provider可以返回一个text stream,就直接获取文本流,否则就返回Uri.toString();
3)Intent:转化成Intent URI返回回来,结果和方法Intent.toUri(URI_INTENT_SCHME)一样;
Copying to the Clipboard
下面详细描述一下复制的流程:
1)如果你使用content URI复制数据,建立一个content provider。详见例子:Note Pad。
2)获得ClipManager对象:
1 // Gets a handle to the clipboard service. 2 ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
3)将数据复制到ClipData对象中去:
文本:
1 // Creates a new text clip to put on the clipboard 2 ClipData clip = ClipData.newPlainText("simple text","Hello, World!");
URI:
下面的代码片段式将记录ID编码到content URI中形成一个URI来构造的,详情见:Encoding an identifier on the URI:
1 // Creates a Uri based on a base Uri and a record ID based on the contact's last name 2 // Declares the base URI string 3 private static final String CONTACTS = "content://com.example.contacts"; 4 5 // Declares a path string for URIs that you use to copy data 6 private static final String COPY_PATH = "/copy"; 7 8 // Declares the Uri to paste to the clipboard 9 Uri copyUri = Uri.parse(CONTACTS + COPY_PATH + "/" + lastName); 10 11 ... 12 13 // Creates a new URI clip object. The system uses the anonymous getContentResolver() object to 14 // get MIME types from provider. The clip object's label is "URI", and its data is 15 // the Uri previously created. 16 ClipData clip = ClipData.newUri(getContentResolver(),"URI",copyUri);
Intent:
1 // Creates the Intent 2 Intent appIntent = new Intent(this, com.example.demo.myapplication.class); 3 4 ... 5 6 // Creates a clip object with the Intent in it. Its label is "Intent" and its data is 7 // the Intent object created previously 8 ClipData clip = ClipData.newIntent("Intent",appIntent);
4)在剪切板上放置新的剪切对象:
1 // Set the clipboard's primary clip. 2 clipboard.setPrimaryClip(clip);
Pasting form the Clipboard
1)粘贴纯文本:
第一步:获取全局的ClipbaodrManager对象:
1 ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 2 String pasteData = "";
2)决定是否需要在当前的Activity允许粘贴行为,你需要验证数据的格式:
1 // Gets the ID of the "paste" menu item 2 MenuItem mPasteItem = menu.findItem(R.id.menu_paste); 3 4 // If the clipboard doesn't contain data, disable the paste menu item. 5 // If it does contain data, decide if you can handle the data. 6 if (!(clipboard.hasPrimaryClip())) { 7 8 mPasteItem.setEnabled(false); 9 10 } else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) { 11 12 // This disables the paste menu item, since the clipboard has data but it is not plain text 13 mPasteItem.setEnabled(false); 14 } else { 15 16 // This enables the paste menu item, since the clipboard contains plain text. 17 mPasteItem.setEnabled(true); 18 } 19 }
3)粘贴数据,只有当可粘贴的时候该功能才会可用。这时候你不知道剪切板上存在的是纯文本还是指向纯文本的URI,下面的代码可以测试这个,但是它只显示了如何处理纯文本:
1 // Responds to the user selecting "paste" 2 case R.id.menu_paste: 3 4 // Examines the item on the clipboard. If getText() does not return null, the clip item contains the 5 // text. Assumes that this application can only handle one item at a time. 6 ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); 7 8 // Gets the clipboard as text. 9 pasteData = item.getText(); 10 11 // If the string contains data, then the paste operation is done 12 if (pasteData != null) { 13 return; 14 15 // The clipboard does not contain text. If it contains a URI, attempts to get data from it 16 } else { 17 Uri pasteUri = item.getUri(); 18 19 // If the URI contains something, try to get text from it 20 if (pasteUri != null) { 21 22 // calls a routine to resolve the URI and get data from it. This routine is not 23 // presented here. 24 pasteData = resolveUri(Uri); 25 return; 26 } else { 27 28 // Something is wrong. The MIME type was plain text, but the clipboard does not contain either 29 // text or a Uri. Report an error. 30 Log.e("Clipboard contains an invalid data type"); 31 return; 32 } 33 }
Pasting data from a content URI
如果剪切板上是一个URI,你的应用也会处理它,那就创建一个ContentResolver,然后调用何时的content provider方法去获取数据。下面的流程描述了如何去做:
1)声明一个全局变量:
1 // Declares a MIME type constant to match against the MIME types offered by the provider 2 public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
2)获取ClipboardManager和content resolver:
1 // Gets a handle to the Clipboard Manager 2 ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 3 4 // Gets a content resolver instance 5 ContentResolver cr = getContentResolver();
3)获取URI:
1 // Gets the clipboard data from the clipboard 2 ClipData clip = clipboard.getPrimaryClip(); 3 4 if (clip != null) { 5 6 // Gets the first item from the clipboard data 7 ClipData.Item item = clip.getItemAt(0); 8 9 // Tries to get the item's contents as a URI 10 Uri pasteUri = item.getUri();
4)调用方法getType(Uri)看看URI是不是一个content URI,如果uri不是指向一个有效的content provider,这个方法返回null。
// If the clipboard contains a URI reference if (pasteUri != null) { // Is this a content URI? String uriMimeType = cr.getType(pasteUri);
5)测试content provider是否提供一个当前应用理解的MIME类型,如果是,则查询数据:
1 // If the return value is not null, the Uri is a content Uri 2 if (uriMimeType != null) { 3 4 // Does the content provider offer a MIME type that the current application can use? 5 if (uriMimeType.equals(MIME_TYPE_CONTACT)) { 6 7 // Get the data from the content provider. 8 Cursor pasteCursor = cr.query(uri, null, null, null, null); 9 10 // If the Cursor contains data, move to the first record 11 if (pasteCursor != null) { 12 if (pasteCursor.moveToFirst()) { 13 14 // get the data from the Cursor here. The code will vary according to the 15 // format of the data model. 16 } 17 } 18 19 // close the Cursor 20 pasteCursor.close(); 21 } 22 } 23 } 24 }
Psting an Intent
大致流程和前面描述的一致,下面详述:
1 // Gets a handle to the Clipboard Manager 2 ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 3 4 // Checks to see if the clip item contains an Intent, by testing to see if getIntent() returns null 5 Intent pasteIntent = clipboard.getPrimaryClip().getItemAt(0).getIntent(); 6 7 if (pasteIntent != null) { 8 9 // handle the Intent 10 11 } else { 12 13 // ignore the clipboard, or issue an error if your application was expecting an Intent to be 14 // on the clipboard 15 }
Using Content Providers to Copy Complex Data
Content Provider支持复杂的数据复制粘贴,比如数据库记录或者文件流。为了复制这样的数据,开发者可以将一个content URI放置在剪切板上,然后粘贴它的应用就可以使用Uri去检索数据。
由于粘贴的应用只有数据的Uri,它还需要知道去粘贴数据的哪一部分,你可以在URI本身中提供信息,或者你可以提供一个特殊的URI来指明指定的想要复制的数据,采用什么技术取决你的数据组织类型。
Encoding an identifier on the URI
在数据的URI本身中编码一个识别器(其实就是资源ID)是复制数据比较有用的一种技术。content provider就可以使用这个识别器去检索特定的数据,粘贴的应用不需要知道识别器的存在,它只需要获取这个uri,然后将它交给复制应用的content provider然后就可以获得数据。
通常我们将识别器放置在content URI的后面,假设你的provider URI如下:
1 "content://com.example.contacts"
如果你想在后面编码一个名字,你只需要这样:
1 String uriString = "content://com.example.contacts" + "/" + "Smith" 2 3 // uriString now contains content://com.example.contacts/Smith. 4 5 // Generates a uri object from the string representation 6 Uri copyUri = Uri.parse(uriString);
如果你已经在使用一个content provider,你或许会想添加一个新的URI路径来指明这个URI是用来复制的,举个例子,加入你已经拥有下面这些URI path:
1 "content://com.example.contacts"/people 2 "content://com.example.contacts"/people/detail 3 "content://com.example.contacts"/people/images
你可以添加另外一个路径来指明这是专门用来复制的uris:
1 "content://com.example.contacts/copying"
然后你就可以匹配“copy”来确定它是不是用来复制的。
如果你已经在使用一个content provider,内部数据库或者一个内部的表组织数据,你可以使用这种编码技术来复制。在这种情况下,你拥有很多片段的数据可以复制,并且可以为每一个片段设置一个ID。然后可以为一个查询根据ID查询并返回需要的数据。
如果你没有很多的数据,就不需要ID,你可以简单的使用一个指向你的provider的URI。你的provider就会查询所有包含的数据来响应查询请求。详情请见NotePad例子。
Copying data structures
为了复制复杂的数据,你可以建立一个content provider。你需要考虑程序当前的状态:
1)如果你已经有了一个content provider,你可以增加它的功能。你或许只需要更改它的query()方法来处理来自其余应用要求粘贴数据的URIs,你或许想要修改处理匹配“copy”URI模式的方法;
2)如果你的应用维护着一个内部的数据库,你或许想要把数据库转换成content provider来简化复制功能;
3)如果你正在使用一个数据库,你可以实现一个简单的content provider专门给应用提供数据;
在content provider中,你至少需要覆写下面的方法:
1)query():粘贴的应用会假设使用uri就可以获得想要粘贴的数据,为了支持复制,你需要该方法可以检测用于复制的uri;
2)getType():该方法需要返回一个MIME类型的数据来指明你想要复制的数据类型,newUri()方法调用getType()来将MIME类型放置到新的ClipData对象中去;
下面的代码片段展示了如何复制复杂的数据:
1)在应用的全局区域,声明一个用于指明复制数据的Uri字符串和路径,也要为要复制的数据声明一个MIME类型:
1 / Declares the base URI string 2 private static final String CONTACTS = "content://com.example.contacts"; 3 4 // Declares a path string for URIs that you use to copy data 5 private static final String COPY_PATH = "/copy"; 6 7 // Declares a MIME type for the copied data 8 public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact"
2)在用户复制数据段额Activity中,建立复制数据的代码,当需要复制数据的时候,将uri放置到剪切板上去:
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)在你的content provider的全局区域,创建一个URI matcher和一个URI pattern来匹配你放置到剪切板上的URIs:
1 public class MyCopyProvider extends ContentProvider { 2 3 ... 4 5 // A Uri Match object that simplifies matching content URIs to patterns. 6 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 7 8 // An integer to use in switching based on the incoming URI pattern 9 private static final int GET_SINGLE_CONTACT = 0; 10 11 ... 12 13 // Adds a matcher for the content URI. It matches 14 // "content://com.example.contacts/copy/*" 15 sUriMatcher.addURI(CONTACTS, "names/*", GET_SINGLE_CONTACT);
4)建立query()方法。这个方法可以处理不同的URI patterns,决定于你怎么去编码,下面写明的是如何处理用于复制操作的URI:
1 // Sets up your provider's query() method. 2 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 3 String sortOrder) { 4 5 ... 6 7 // Switch based on the incoming content URI 8 switch (sUriMatcher.match(uri)) { 9 10 case GET_SINGLE_CONTACT: 11 12 // query and return the contact for the requested name. Here you would decode 13 // the incoming URI, query the data model based on the last name, and return the result 14 // as a Cursor. 15 16 ... 17 18 }
5)建立getType()方法,返回一个正确的MIME类型给复制数据:
1 / Sets up your provider's getType() method. 2 public String getType(Uri uri) { 3 4 ... 5 6 switch (sUriMatcher.match(uri)) { 7 8 case GET_SINGLE_CONTACT: 9 10 return (MIME_TYPE_CONTACT);
Copying data streams
可以复制粘贴大亮的文本和二进制数据,数据可以具有以下的类型:
1)存储在实际设备上的文件;
2)从sockets中接收到的数据流;
3)在数据库系统下存储的大量数据;
一个一个专门提供数据流的content provider通过诸如AssetFileDescriptor的文件描述符而不是Cursor来提供数据访问,粘贴的应用使用文件描述符来读取数据流。
为了让应用可以使用provider来复制数据,遵循如下步骤:
1)为要放置到剪切板上的数据流建立一个content provider,可以有以下选择:a)加入一个ID号,就像前面讨论的一样,然后在你的provider表中维护这样一个ID和对应的流的名字;b)直接将流的名字编码在URI中;c)使用一个总是返回当前流的特殊的URI,如果你这样做,你必须记住,你需要不论什么时候通过剪切板复制数据,你都需要更新你的provider去指向一个不同的数据流;
2)为每一种类型的数据流提供一个MIME类型,粘贴的应用需要这个恶数据;
3)实现ContentProvider的一个方法来为数据流返回一个文件描述符,如果你在content URI后面编码了一个ID,你就可以使用这个方法来确定要打开哪一个文件流;
4)为为了将数据流复制到剪切板,构建一个content URI放置到剪切板上;
为了粘贴一个数据流,应用从剪切板上获取clip,获取URI,调用contentprovider的文件描述符获取方法来打开数据流。ContentProvider方法调用对应的方法,将URI传递给它,你的provider返回文件描述符,然后粘贴应用就负责从数据流中读取数据;
下面的列表展示了content provider中最重要的文件描述符方法,每一个方法都有相对应的ContentResolver方法,只需要在后面加上Descriptor,举个例子,在ContentProvider中有方法openAssetFile(),ContentResolver中则有方法openAssetFileDescriptor():
1)openTypeAssetFile():这个方法需要返回一个asset文件描述符,但是只有当provider支持它的MIME类型的时候。调用者(粘贴应用)提供一个MIME类型模式。如果content provider(复制应用)可以提供该类型的文件,九返回一个文件描述符,否则抛出异常。
2)openAssetFile():这个方法是上一个方法更加泛化的形式,它不过滤文件类型,但是读取部分文件;
3)openFile():更加泛化,不能读取部分文件;
你可以选择使用openPipeHelper()方法。它允许应用程序使用一个pipe在后台读取数据流,为了使用该方法,你需要实现ContentProvider.PipeDataWriter接口。同样在NotePad中也有相应的使用:在NotePadProvider.java中的openTypeAssetFile()方法。
Designing Effective Copy/Paste Functionality
为了设计高效的复制粘贴功能,记住以下几点:
1)任何时候,剪切板上只有一个剪切数据;
2)ClipData.Item的设计是为了一次剪切多个选择项,而不是为了容纳一次剪切的多个不同的部分,这就说明,ClipData.Item中容纳的应该全部是同一个类型的数据;
3)当你提供数据,你可以提供不同的MIME表示,将你支持的MIME类型添加到ClipDescription中,然后在你的content provider中实现你的MIME类型;
4)当你从剪切板复制数据下来的时候,你的应用负责检查数据的类型,决定使用哪一个,即使剪切板上有数据,用户也要求粘贴,你的应用也不一定需要执行黏贴操作,你应该只在数据类型匹配的时候执行操作,当然你也可以强制转化为文本。如果你的应用支持不止一种MIME类型,你可以允许用户选择一种。