Android 剪貼簿詳解
Android 提供了一個強大的剪貼簿框架,用於複製和貼上。 它支援文字、二進位制資料流或其它複雜的資料。
Android 剪貼簿框架如圖

關於這四個類的簡介如下:
- ClipboardManager 是系統全域性的剪貼簿物件,通過
context.getSystemService(CLIPBOARD_SERVICE)
獲取。 - ClipData ,即 clip 物件,在系統剪貼簿裡只存在一個,當另一個 clip 物件進來時,前一個 clip 物件會消失。
- ClipData.Item ,即 data item,它包含了文字、 Uri 或者 Intent 資料,一個 clip 物件可以包含一個或多個 Item 物件。通過
addItem(ClipData.Item item)
- 文字:文字是直接放在 clip 物件中,然後放在剪貼簿裡;貼上這個字串的時候直接從剪貼簿拿到這個物件,把字串放入你的應用儲存中。
- Uri:對於複雜資料的剪貼拷貝並不是直接將資料放入記憶體,而是通過 Uri 來實現,畢竟 Uri 的中文名叫:統一資源識別符號。通過 Uri 能定位手機上所有資源,這當然能實現拷貝了,只不過需要做一些額外的處理工作。(對於 Uri 不是很理解,如有誤,望指正~)
- Intent:複製的時候 Intent 會被直接放入 clip 物件,這相當於拷貝了一個快捷方式。
ClipDescription ,即 clip metadata,它包含了 ClipData 物件的 metadata 資訊。可以通過
getMimeType(int index)
// 對應 ClipData newPlainText(label, text) 的 MimeType public static final String MIMETYPE_TEXT_PLAIN = "text/plain"; // 對應 ClipData.newHtmlText(label, text, htmlText) 的 MimeType public static final String MIMETYPE_TEXT_HTML = "text/html"; // 對應 ClipData.newUri(cr, label, uri) 的 MimeType
但 MIMETYPE_TEXT_URILIST 有點特殊,當 Uri 為
content://uri
時,它會轉為具體的 MimeType ,後面會有例子講到。
剪貼簿簡單使用
以拷貝文字為例,剪貼簿的使用可以分為以下幾步:
獲取 ClipManager 物件
ClipManager clipManager = (ClipManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
將資料放到 clip 物件中
ClipData clip = ClipData.newPlainText("simple text copy", "Hello World!");
類似的方法還有
//建立一個包含 htmlText 的 ClipData //一般在瀏覽器中對網頁進行拷貝的時候會呼叫此方法 //其中 htmlText 是包含 HTML 標籤的字串 static public ClipData newHtmlText(CharSequence label, CharSequence text, String htmlText); //建立一個包含 Intent 的 ClipData static public ClipData newIntent(CharSequence label, Intent intent); //建立一個包含 Uri 的 ClipData,MimeType 會根據 Uri 進行修改 static public ClipData newUri(ContentResolver resolver, CharSequence label, Uri uri); //與 newUri 相對應,但是並不會根據 Uri 修改 MimeType static public ClipData newRawUri(CharSequence label, Uri uri);
將 clip 物件放入剪貼簿
clipManager.setPrimaryClip(clip);
從剪貼簿中獲取 clip 物件
//判斷剪貼簿裡是否有內容 if(!clipManager.hasPrimaryClip()) { return; } ClipData clip = clipManager.getPrimaryClip(); //獲取 ClipDescription ClipDescription description = clip.getPrimaryClipDescription(); //獲取 label String label = description.getLabel().toString(); //獲取 text String text = clip.getItemAt(0).getText().toString();
實踐出真知
講道理,實踐出真知,咱們程式設計師的實踐就是程式碼,下面上程式碼。等等,先上 Demo 的執行效果圖。第一次做 Gif ,好緊張,哈哈~若動態圖不動,檢視原圖連線應該就可以了~

對於剪貼簿大部分操作都封裝在 ClipboardUtil.java
裡,使用時請記錄呼叫 init(Context context)
方法進行初始化,建議在 Application.onCreate()
中進行,否則會造成記憶體洩漏。
AndroidManifest.xml
:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.littlejie.clipboard">
<!-- content://contacts/people 需要使用該許可權-->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<application
android:name=".ClipboardApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
ClipboardApplication.java
:
public class ClipboardApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ClipboardUtil.init(this);
}
}
build.gradle
:
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "25.0.0"
defaultConfig {
applicationId "com.littlejie.clipboard"
minSdkVersion 16
//由於Android6.0之後有執行時許可權,故修改版本號使其不用執行時獲取讀取聯絡人許可權
//關於 compileSdkVersion 、 minSdkVersion 、 targetSdkVersion 三者之間的區別
//有興趣的可以自行谷歌、百度
targetSdkVersion 21
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
// Something else
}
// Something else
MainActivity.java
:
public class MainActivity extends Activity implements View.OnClickListener,
ClipboardUtil.OnPrimaryClipChangedListener {
private static final String TAG = MainActivity.class.getSimpleName();
private static final String MIME_CONTACT = "vnd.android.cursor.dir/person";
private TextView mTvCopied;
private ClipboardUtil mClipboard;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//ClipboardUtil在Application的onCreate中呼叫init初始化
mClipboard = ClipboardUtil.getInstance();
mClipboard.addOnPrimaryClipChangedListener(this);
mTvCopied = (TextView) findViewById(R.id.tv_copied);
findViewById(R.id.btn_copy_text).setOnClickListener(this);
findViewById(R.id.btn_copy_rich_text).setOnClickListener(this);
findViewById(R.id.btn_copy_intent).setOnClickListener(this);
findViewById(R.id.btn_copy_uri).setOnClickListener(this);
findViewById(R.id.btn_copy_multiple).setOnClickListener(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
mClipboard.removeOnPrimaryClipChangedListener(this);
}
@Override
public void onPrimaryClipChanged(ClipboardManager clipboardManager) {
Log.d(TAG, clipboardManager.getPrimaryClip().toString());
mTvCopied.setText(clipboardManager.getPrimaryClip().toString());
ClipData data = clipboardManager.getPrimaryClip();
String mimeType = mClipboard.getPrimaryClipMimeType();
Log.d(TAG, "mimeType = " + mimeType);
//一般來說,收到系統 onPrimaryClipChanged() 回撥時,剪貼簿一定不為空
//但為了保險起見,在這邊還是做了空指標判斷
if (data == null) {
return;
}
//前四種為剪貼簿預設的MimeType,但是當拷貝資料為Uri時,會出現其它MimeType,需要特殊處理
if (ClipDescription.MIMETYPE_TEXT_INTENT.equals(mimeType)) {
//一個 ClipData 可以有多個 ClipData.Item,這裡假設只有一個
startActivity(data.getItemAt(0).getIntent());
} else if (ClipDescription.MIMETYPE_TEXT_HTML.equals(mimeType)) {
} else if (ClipDescription.MIMETYPE_TEXT_PLAIN.equals(mimeType)) {
} else if (ClipDescription.MIMETYPE_TEXT_URILIST.equals(mimeType)) {
//當uri=content://media/external時,copyUri會進入此if-else語句
} else if (MIME_CONTACT.equals(mimeType)) {
Log.d(TAG, mClipboard.coercePrimaryClipToText().toString());
//當uri=content://contacts/people時,copyUri會進入此if-else語句
StringBuilder sb = new StringBuilder(mTvCopied.getText() + "\n\n");
int index = 1;
Uri uri = data.getItemAt(0).getUri();
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
while (cursor.moveToNext()) {
//列印所有聯絡人姓名
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
sb.append("聯絡人 " + index++ + " : " + name + "\n");
}
mTvCopied.setText(sb.toString());
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_copy_text:
//普通的文字拷貝
mClipboard.copyText("文字拷貝", "我是文字~");
break;
case R.id.btn_copy_rich_text:
//平時在瀏覽器網頁上執行的copy就是這種,一般瀏覽器會使用 ClipData.newHtmlText(label, text, htmlText)往剪貼簿裡塞東西
//所以,將這段內容拷貝到諸如 Google+ 、 Gmail 等能處理富文字內容的應用能看到保留格式的內容
//補充:測試了 QQ瀏覽器 、 UC瀏覽器,發現他們拷貝的內容只是單純的文字,即使用 ClipData.newPlainText(label, text) 往剪貼簿裡塞東西
mClipboard.copyHtmlText("HTML拷貝", "我是HTML",
"<strong style=\"margin: 0px; padding: 0px; border: 0px; color: rgb(64, 64, 64); font-family: STHeiti, 'Microsoft YaHei', Helvetica, Arial, sans-serif; font-size: 17px; font-style: normal; font-variant: normal; letter-spacing: normal; line-height: 25.920001983642578px; orphans: auto; text-align: justify; text-indent: 34.560001373291016px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(235, 23, 23);\">央視</strong>");
break;
case R.id.btn_copy_intent:
mClipboard.copyIntent("Intent拷貝", getOpenBrowserIntent());
break;
case R.id.btn_copy_uri:
mClipboard.copyUri(getContentResolver(), "Uri拷貝", Uri.parse("content://contacts/people"));
//mClipboard.copyUri(getContentResolver(), "Uri拷貝", Uri.parse("content://media/external"));
break;
case R.id.btn_copy_multiple:
copyMultiple();
break;
}
}
/**
* 開啟瀏覽器的Intent
*
* @return
*/
private Intent getOpenBrowserIntent() {
Uri uri = Uri.parse("http://www.baidu.com");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
return intent;
}
/**
* 拷貝多組資料到剪貼簿
*/
private void copyMultiple() {
//ClipData 目前僅能設定單個 MimeType
List<ClipData.Item> items = new ArrayList<>();
//故 ClipData.Item 的型別必須和 MimeType 設定的相符
//比如都是文字,都是URI或都是Intent,而不是混合各種形式。
ClipData.Item item1 = new ClipData.Item("text1");
ClipData.Item item2 = new ClipData.Item("text2");
ClipData.Item item3 = new ClipData.Item("text3");
ClipData.Item item4 = new ClipData.Item("text4");
items.add(item1);
items.add(item2);
items.add(item3);
items.add(item4);
mClipboard.copyMultiple("Multiple Copy", ClipDescription.MIMETYPE_TEXT_PLAIN, items);
}
}
ClipboardUtil.java
:
/**
* 剪貼簿工具類
* http://developer.android.com/guide/topics/text/copy-paste.html
* Created by littlejie on 2016/4/15.
*/
public class ClipboardUtil {
public static final String TAG = ClipboardUtil.class.getSimpleName();
private static final String MIME_CONTACT = "vnd.android.cursor.dir/person";
/**
* 由於系統剪貼簿在某些情況下會多次呼叫,但呼叫間隔基本不會超過5ms
* 考慮到使用者操作,將閾值設為100ms,過濾掉前幾次無效回撥
*/
private static final int THRESHOLD = 100;
private Context mContext;
private static ClipboardUtil mInstance;
private ClipboardManager mClipboardManager;
private Handler mHandler = new Handler();
private ClipboardManager.OnPrimaryClipChangedListener onPrimaryClipChangedListener = new ClipboardManager.OnPrimaryClipChangedListener() {
@Override
public void onPrimaryClipChanged() {
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, THRESHOLD);
}
};
private ClipboardChangedRunnable mRunnable = new ClipboardChangedRunnable();
private List<OnPrimaryClipChangedListener> mOnPrimaryClipChangedListeners = new ArrayList<>();
/**
* 自定義 OnPrimaryClipChangedListener
* 用於處理某些情況下系統多次呼叫 onPrimaryClipChanged()
*/
public interface OnPrimaryClipChangedListener {
void onPrimaryClipChanged(ClipboardManager clipboardManager);
}
private class ClipboardChangedRunnable implements Runnable {
@Override
public void run() {
for (OnPrimaryClipChangedListener listener : mOnPrimaryClipChangedListeners) {
listener.onPrimaryClipChanged(mClipboardManager);
}
}
}
private ClipboardUtil(Context context) {
mContext = context;
mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
mClipboardManager.addPrimaryClipChangedListener(onPrimaryClipChangedListener);
}
/**
* 單例。暫時不清楚此處傳 Activity 的 Context 是否會造成記憶體洩漏
* 建議在 Application 的 onCreate 方法中實現
*
* @param context
* @return
*/
public static ClipboardUtil init(Context context) {
if (mInstance == null) {
mInstance = new ClipboardUtil(context);
}
return mInstance;
}
/**
* 獲取ClipboardUtil例項,記得初始化
*
* @return
*/
public static ClipboardUtil getInstance() {
return mInstance;
}
public void addOnPrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
if (!mOnPrimaryClipChangedListeners.contains(listener)) {
mOnPrimaryClipChangedListeners.add(listener);
}
}
public void removeOnPrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
mOnPrimaryClipChangedListeners.remove(listener);
}
/**
* 判斷剪貼簿內是否有資料
*
* @return
*/
public boolean hasPrimaryClip() {
return mClipboardManager.hasPrimaryClip();
}
/**
* 獲取剪貼簿中第一條String
*
* @return
*/
public String getClipText() {
if (!hasPrimaryClip()) {
return null;
}
ClipData data = mClipboardManager.getPrimaryClip();
if (data != null
&& mClipboardManager.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
return data.getItemAt(0).getText().toString();
}
return null;
}
/**
* 獲取剪貼簿中第一條String
*
* @param context
* @return
*/
public String getClipText(Context context) {
return getClipText(context, 0);
}
/**
* 獲取剪貼簿中指定位置item的string
*
* @param context
* @param index
* @return
*/
public String getClipText(Context context, int index) {
if (!hasPrimaryClip()) {
return null;
}
ClipData data = mClipboardManager.getPrimaryClip();
if (data == null) {
return null;
}
if (data.getItemCount() > index) {
return data.getItemAt(index).coerceToText(context).toString();
}
return null;
}
/**
* 將文字拷貝至剪貼簿
*
* @param text
*/
public void copyText(String label, String text) {
ClipData clip = ClipData.newPlainText(label, text);
mClipboardManager.setPrimaryClip(clip);
}
/**
* 將HTML等富文字拷貝至剪貼簿
*
* @param label
* @param text
* @param htmlText
*/
public void copyHtmlText(String label, String text, String htmlText) {
ClipData clip = ClipData.newHtmlText(label, text, htmlText);
mClipboardManager.setPrimaryClip(clip);
}
/**
* 將Intent拷貝至剪貼簿
*
* @param label
* @param intent
*/
public void copyIntent(String label, Intent intent) {
ClipData clip = ClipData.newIntent(label, intent);
mClipboardManager.setPrimaryClip(clip);
}
/**
* 將Uri拷貝至剪貼簿
* If the URI is a content: URI,
* this will query the content provider for the MIME type of its data and
* use that as the MIME type. Otherwise, it will use the MIME type
* {@link ClipDescription#MIMETYPE_TEXT_URILIST}.
* 如 uri = "content://contacts/people",那麼返回的MIME type將變成"vnd.android.cursor.dir/person"
*
* @param cr ContentResolver used to get information about the URI.
* @param label User-visible label for the clip data.
* @param uri The URI in the clip.
*/
public void copyUri(ContentResolver cr, String label, Uri uri) {
ClipData clip = ClipData.newUri(cr, label, uri);
mClipboardManager.setPrimaryClip(clip);
}
/**
* 將多組資料放入剪貼簿中,如選中ListView多個Item,並將Item的資料一起放入剪貼簿
*
* @param label User-visible label for the clip data.
* @param mimeType mimeType is one of them:{@link android.content.ClipDescription#MIMETYPE_TEXT_PLAIN},
* {@link android.content.ClipDescription#MIMETYPE_TEXT_HTML},
* {@link android.content.ClipDescription#MIMETYPE_TEXT_URILIST},
* {@link android.content.ClipDescription#MIMETYPE_TEXT_INTENT}.
* @param items 放入剪貼簿中的資料
*/
public void copyMultiple(String label, String mimeType, List<ClipData.Item> items) {
if (items == null) {
throw new NullPointerException("items is null");
}
int size = items.size();
ClipData clip = new ClipData(label, new String[]{mimeType}, items.get(0));
for (int i = 1; i < size; i++) {
clip.addItem(items.get(i));
}
mClipboardManager.setPrimaryClip(clip);
}
public CharSequence coercePrimaryClipToText() {
if (!hasPrimaryClip()) {
return null;
}
return mClipboardManager.getPrimaryClip().getItemAt(0).coerceToText(mContext);
}
public CharSequence coercePrimaryClipToStyledText() {
if (!hasPrimaryClip()) {
return null;
}
return mClipboardManager.getPrimaryClip().getItemAt(0).coerceToStyledText(mContext);
}
public CharSequence coercePrimaryClipToHtmlText() {
if (!hasPrimaryClip()) {
return null;
}
return mClipboardManager.getPrimaryClip().getItemAt(0).coerceToHtmlText(mContext);
}
/**
* 獲取當前剪貼簿內容的MimeType
*
* @return 當前剪貼簿內容的MimeType
*/
public String getPrimaryClipMimeType() {
if (!hasPrimaryClip()) {
return null;
}
return mClipboardManager.getPrimaryClipDescription().getMimeType(0);
}
/**
* 獲取剪貼簿內容的MimeType
*
* @param clip 剪貼簿內容
* @return 剪貼簿內容的MimeType
*/
public String getClipMimeType(ClipData clip) {
return clip.getDescription().getMimeType(0);
}
/**
* 獲取剪貼簿內容的MimeType
*
* @param clipDescription 剪貼簿內容描述
* @return 剪貼簿內容的MimeType
*/
public String getClipMimeType(ClipDescription clipDescription) {
return clipDescription.getMimeType(0);
}
/**
* 清空剪貼簿
*/
public void clearClip() {
mClipboardManager.setPrimaryClip(null);
}
}
補充
以下補充幾點,是自己在測試剪貼簿的過程中碰到,一是 OnPrimaryClipChangedListener
的多次回撥,二是將剪貼簿中的內容轉換為字串。
關於 OnPrimaryClipChangedListener 的多次回撥
細心的同學可能已經發現,上述程式碼中,樓主並沒有直接使用 Android 的 OnPrimaryClipChangedListener
,而是自己對此進行了再次封裝。這是有原因的,在最初測試剪貼簿的過程中,樓主發現一次拷貝過程可能會導致多次回撥 onPrimaryClipChanged()
方法,日誌如下:
# 第一次
com.littlejie.clipboard D/MainActivity: text/plain
# 第二次
com.littlejie.clipboard D/MainActivity: text/plain
com.littlejie.clipboard D/MainActivity: 央視
# 第三次
com.littlejie.clipboard D/MainActivity: text/html
com.littlejie.clipboard D/MainActivity: <strong style="margin: 0px; padding: 0px; border: 0px; color: rgb(64, 64, 64); font-family: STHeiti, 'Microsoft YaHei', Helvetica, Arial, sans-serif; font-size: 17px; font-style: normal; font-variant: normal; letter-spacing: normal; line-height: 25.920001983642578px; orphans: auto; text-align: justify; text-indent: 34.560001373291016px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(246, 246, 246);">央視</strong>
com.littlejie.clipboard D/MainActivity: 央視
這肯定不是我們想要的結果,那麼該怎麼解決這個問題呢?
多次測試發現,發生多次回撥的情況下,正確的拷貝結果都是最後一次回撥獲取到的資料。
再列印一下 onPrimaryClipChanged()
回撥時間吧,發現三次的間隔不超過 9ms ,而普通使用者一般不可能在如此短時間內完成多次拷貝。故我們可以定義一個變數儲存 onPrimaryClipChanged
的回撥時間,當下次回撥時相對前一次的時間間隔小於 100ms(合理假設),那麼判定前一次回撥事件無效。
# 第一次
onPrimaryClipChanged,time = 1481792153614
# 第二次
onPrimaryClipChanged,time = 1481792153620
# 第三次
onPrimaryClipChanged,time = 1481792153623
故才有了上訴的程式碼。
將剪貼簿中的資料強轉為字串
一般來說,平時我們拷貝的都是文字,但是從上述內容可知,Android 剪貼簿支援的不僅僅是文字,那對於 Uri 、 Intent 資料 Android 是如何把它們轉換成字串的呢?有興趣的同學可以去檢視 ClipData 下述三個方法的原始碼。這裡限於篇幅就不詳述了。
public CharSequence coerceToText(Context context);
public CharSequence coerceToStyledText(Context context);
public String coerceToHtmlText(Context context);
畫外:如何高效的複製貼上
為了設計有效的複製貼上功能,以下幾點需要注意:
- 任何時間,都只有一個clip物件在剪貼簿裡。新的複製操作都會覆蓋前一個clip物件,因為使用者可能從你的應用中退出,從其他應用中拷貝一個東西,所以你不能假定使用者在你的應用中拷貝的上一個東西一定還放在剪貼簿裡。
- 一個clip物件,即ClipData中的多個ClipData.Item 物件是為了支援多選項的複製貼上,而不是為了支援單選的多種形式。你通常需要clip物件中的所有的專案,即ClipData.Item有一樣的形式,比如都是文字,都是URI或都是Intent,而不是混合各種形式。
- 當你提供資料時,你可以提供不同的MIME表達方式。將你支援的MIME型別加入到ClipDescription中去,然後在你的content provider中實現它。
- 當你從剪貼簿得到資料時,你的應用有責任檢查可用的MIME型別,然後決定使用哪一個。即便有一個clip物件在剪貼簿中並且使用者要求貼上,你的應用有可能不需要進行貼上操作。你應該在MIME型別相容的時候執行貼上操作。你可以選擇使用 coerceToText()方法將貼上的內容轉換為文字。如果你的應用支援多種型別,你可以讓使用者自己選用哪一個。