1 Android剪切板简介
Android 剪贴板是一个系统级服务,它允许应用程序之间共享文本、图像、二进制数据等多种形式的信息。用户可以通过常见的复制和粘贴操作,在不同的应用之间传递数据。该设计考虑到了易用性和灵活性,使得开发者可以轻松地为自己的应用实现复制粘贴功能,同时它也强调了数据的安全性,确保剪贴板内容不会被未授权的应用访问。
接下来从剪切板的框架、数据类型处理、剪切板局限性、MIME类型说明、剪切板高效复制粘贴设计角度来先详细介绍剪切板。
剪贴板框架说明:Android的剪贴板框架由几个关键类组成,包括ClipboardManager、ClipData、ClipData.Item和ClipDescription。具体如下:
- ClipboardManager:这是系统剪贴板的代表,通过调用getSystemService(CLIPBOARD_SERVICE)来获取对它的引用。
- ClipData:这是一个包含数据说明(ClipDescription)和数据本身(ClipData.Item)的容器,代表了剪贴板中的一组数据。
- ClipData.Item:这是实际的数据项,可以包含文本、URI或Intent数据。
- ClipDescription:这个类包含关于ClipData的元数据,例如它包含的可用MIME类型数组。
数据类型处理:根据数据的类型(文本、URI、Intent等),可能需要执行不同的操作来处理或使用这些数据。例如,如果数据是文本,可以直接使用;如果数据是URI,可能需要解析它以获取实际的数据源;如果数据是Intent,可能需要执行相应的操作。
剪贴板的局限性:剪贴板只能保留一个ClipData对象。当一个新的ClipData对象被放入剪贴板时,旧的ClipData对象将被自动清除,这意味着需要确保每次只放置一个有效的ClipData对象在剪贴板上。
MIME类型说明:在Android剪贴板中,MIME类型用于表示数据的格式。例如,文本数据通常使用text/plain MIME类型,而HTML文本使用text/html。对于URI列表,使用的是text/uri-list,而对于Intent数据,则使用text/vnd.android.intent。
剪切板高效复制粘贴设计:设计有效的复制粘贴功能时,需要注意以下几点:
- 任何时间都只有一个clip对象在剪贴板里,新的复制操作都会覆盖前一个clip对象。
- 一个clip对象中的多个ClipData.Item对象是为了支持多选项的复制粘贴,而不是为了支持单选的多种形式。
- 当提供数据时,可以提供不同的MIME表达方式,并将支持的MIME类型加入到ClipDescription中。
- 安全和隐私:在使用剪贴板时,开发者应注意数据的安全性和隐私性,避免敏感信息的不当共享。
2 剪切板设计实战
实现功能:实现2个按键:一个功能是复制内容(文本和图片)到剪切板,另一个功能是从剪切板中获取粘贴内容到本地并通过TextView和Image来显示。
关于该程序,自定义 ClipboardUtils.java 的代码实现如下所示:
package com.example.myapplication3;
import android.content.Context;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ClipDescription;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReference;
import android.graphics.BitmapFactory;
import android.graphics.Bitmap;
import android.provider.MediaStore;
import android.net.Uri;
import android.util.Log;
public class ClipboardUtils {
private static final String TAG = "ClipboardUtils";
public static final int CLIPBOARD_DATA_TYPE_TEXT = 0;
public static final int CLIPBOARD_DATA_TYPE_IMAGE = 1;
public static final int CLIPBOARD_DATA_TYPE_UNSUPPORT = -1;
//private static final int ERROR_INDEX_OVERRIDE = -2;
//private long mCallbackPtr = 0;
private ClipboardManager mClipboardManager = null;
//private ClipData mSetClipData = null;
//private ClipData mGetClipData = null;
static Context context;
public ClipboardUtils() {
if(context == null){
Log.e(TAG,"set Content first");
return;
}
mClipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
if(mClipboardManager == null){
Log.e(TAG, "get ClipboardManager error");
}
}
public static void setContext(Context inContext) {
context = inContext;
}
/**
* 获取 ClipboardUtils 单例
*/
public static ClipboardUtils getInstance() {
return Holder.sInstance;
}
private static class Holder { private static ClipboardUtils sInstance = new ClipboardUtils(); }
public static AtomicReference<ClipData> createClipdataRef(){
return new AtomicReference<>(null);
}
/**
* 剪切板是否有数据
*/
public boolean hasClip() {
Log.d(TAG, "java call:hasclip");
return mClipboardManager.hasPrimaryClip();
}
/**
* 清除剪切板数据
*/
public int clearClip() {
Log.d(TAG, "java call:clearClip");
mClipboardManager.clearPrimaryClip();
return 0;
}
/**
* 添加文本类型Item数据
*/
public int addTextItem(AtomicReference<ClipData> clipDataRef, String text){
try {
if (clipDataRef.get() == null) {
ClipData clipData = ClipData.newPlainText("text_label", text);
clipDataRef.set(clipData);
}else{
ClipData.Item item = ClipData.newPlainText("text_label", text).getItemAt(0);
clipDataRef.get().addItem(item);
}
Log.e(TAG,"lenTextItem1="+clipDataRef.get().getItemCount());
return 0;
} catch (Exception e) {
Log.e(TAG, "Error adding text item to ClipData");
return -1;
}
}
/**
* 添加图片类型Item数据
*/
public int addImageItem(AtomicReference<ClipData> clipDataRef, Bitmap image) {
try {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.PNG, 100, stream);
String path = MediaStore.Images.Media.insertImage(context.getContentResolver(),image,"ImageX",null);
if (clipDataRef.get() == null) {
ClipData clipData = ClipData.newRawUri("image_label", Uri.parse(path));
clipDataRef.set(clipData);
}else{
ClipData.Item item = ClipData.newRawUri("image_label", Uri.parse(path)).getItemAt(0);
clipDataRef.get().addItem(item);
}
return 0;
} catch (Exception e) {
Log.e(TAG, "Error adding image item to ClipData");
return -1;
}
}
/**
* 根据索引获取剪贴板中的文本项
*/
public String getTextItem(AtomicReference<ClipData> clipDataRef, int index) {
try {
if (clipDataRef.get() != null && index >= 0 && index < clipDataRef.get().getItemCount()) {
ClipData.Item item = clipDataRef.get().getItemAt(index);
int type = getItemType(clipDataRef,index);
if(type!=CLIPBOARD_DATA_TYPE_TEXT){
return null;
}
// 直接返回文本内容,如果获取成功
return item.getText().toString();
}else {
Log.d(TAG, "index override");
}
} catch (Exception e) {
Log.e(TAG, "Error getting text item from ClipData");
}
// 如果索引无效或出现异常,返回null表示获取失败
return null;
}
/**
* 根据索引获取剪贴板中的图片项
*/
public Bitmap getImageItem(AtomicReference<ClipData> clipDataRef, int index) {
try {
if (clipDataRef.get()!= null && index >= 0 && index < clipDataRef.get().getItemCount()) {
ClipData.Item item = clipDataRef.get().getItemAt(index);
int type = getItemType(clipDataRef,index);
if(type!=CLIPBOARD_DATA_TYPE_IMAGE){
return null;
}
if (item.getUri() != null) {
InputStream inputStream = context.getContentResolver().openInputStream(item.getUri());
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
return bitmap;
}
}
}else {
Log.d(TAG, "index override");
}
} catch (Exception e) {
Log.e(TAG, "Error getting image item from ClipData");
}
return null; // 索引无效或数据类型不匹配
}
/**
* 获取剪切板中Item的数量
*/
public int getItemCount(AtomicReference<ClipData> clipDataRef){
return clipDataRef.get().getItemCount();
}
/**
* 将当前的mGetClipData设置为剪贴板的主内容
*/
public void setPrimaryClip(AtomicReference<ClipData> clipDataRef) {
if (mClipboardManager != null && clipDataRef.get() != null) {
mClipboardManager.setPrimaryClip(clipDataRef.get());
}
}
/**
* 获取剪贴板中主剪贴板的内容
*/
public void getPrimaryClip(AtomicReference<ClipData> clipDataRef) {
if (mClipboardManager != null && mClipboardManager.hasPrimaryClip()) {
ClipData clipdata= mClipboardManager.getPrimaryClip();
clipDataRef.set(clipdata);
}
}
/**
* 获取Item类型
*/
public int getItemType(AtomicReference<ClipData> clipDataRef,int index) {
if (clipDataRef.get() != null && index >= 0 && index < clipDataRef.get().getItemCount()) {
ClipData.Item item = clipDataRef.get().getItemAt(index);
Uri uri = item.getUri();
if(uri == null){
return CLIPBOARD_DATA_TYPE_TEXT;
}else{
String mimeType = context.getContentResolver().getType(item.getUri());
if (mimeType != null) {
if (mimeType.startsWith("image/")) {
return CLIPBOARD_DATA_TYPE_IMAGE;
}else{
return CLIPBOARD_DATA_TYPE_UNSUPPORT;
}
}
}
}
return -2;
}
}
基于对 ClipboardUtils 的调用,MainActivity.java实现为:
package com.example.myapplication3;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ClipData;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.concurrent.atomic.AtomicReference;
public class MainActivity extends AppCompatActivity {
private Button btnGetImage;
private Button btnSetImage;
private ImageView imageView;
private TextView textView;
Bitmap bitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnGetImage = findViewById(R.id.btnGetImage);
btnSetImage = findViewById(R.id.btnSetImage);
imageView = findViewById(R.id.imageView);
textView = findViewById(R.id.textView);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test5);
//imageView.setImageBitmap(bitmap);
ClipboardUtils.setContext(getApplication());
// 设置点击监听器,从剪贴板获取图片
btnGetImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getClipFromClipboard();
}
});
// 设置点击监听器,将图片设置到剪贴板
btnSetImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setClipToClipboard();
}
});
}
private void getClipFromClipboard() {
AtomicReference<ClipData> clipDataRef = new AtomicReference<>(null);
ClipboardUtils clipboardUtils = ClipboardUtils.getInstance();
clipboardUtils.getPrimaryClip(clipDataRef);
Log.e("clip","len="+clipboardUtils.getItemCount(clipDataRef));
for(int i =0;i<clipboardUtils.getItemCount(clipDataRef);i++){
if(clipboardUtils.getItemType(clipDataRef,i) == clipboardUtils.CLIPBOARD_DATA_TYPE_TEXT){
String text = clipboardUtils.getTextItem(clipDataRef,i);
textView.setText(text);
}else if(clipboardUtils.getItemType(clipDataRef,i) == clipboardUtils.CLIPBOARD_DATA_TYPE_IMAGE){
Bitmap bitmap1 = clipboardUtils.getImageItem(clipDataRef,i);
imageView.setImageBitmap(bitmap);
}else{
Log.e("clip","not support format");
}
}
}
private void setClipToClipboard() {
ClipboardUtils clipboardUtils = ClipboardUtils.getInstance();
AtomicReference<ClipData> clipDataRef = ClipboardUtils.createClipdataRef();
clipboardUtils.addTextItem(clipDataRef, "test text1");
clipboardUtils.addImageItem(clipDataRef,bitmap);
clipboardUtils.setPrimaryClip(clipDataRef);
}
}
对应的layout xml代码配置为:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">
<Button
android:id="@+id/btnGetImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Get Image from Clipboard" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_gravity="center" />
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:scaleType="centerInside" />
<Button
android:id="@+id/btnSetImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Set Image to Clipboard" />
</LinearLayout>
最后在drawable中添加一张图片用于测试,一个基本的剪切板功能就设计完成了。