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中添加一张图片用于测试,一个基本的剪切板功能就设计完成了。

05-12 06:20