Android APP 剪切板应用

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 的代码实现如下所示:

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实现为:

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代码配置为:

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

相关推荐
程序员卷卷狗19 小时前
MySQL 慢查询优化:从定位、分析到索引调优的完整流程
android·mysql·adb
写点啥呢19 小时前
Android Studio 多语言助手插件:让多语言管理变得简单高效
android·ai·ai编程·多语言
泥嚎泥嚎21 小时前
【Android】给App添加启动画面——SplashScreen
android·java
全栈派森21 小时前
初见 Dart:这门新语言如何让你的 App「动」起来?
android·flutter·ios
q***985221 小时前
图文详述:MySQL的下载、安装、配置、使用
android·mysql·adb
恋猫de小郭1 天前
Dart 3.10 发布,快来看有什么更新吧
android·前端·flutter
恋猫de小郭1 天前
Flutter 3.38 发布,快来看看有什么更新吧
android·前端·flutter
百锦再1 天前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
会跑的兔子1 天前
Android 16 Kotlin协程 第二部分
android·windows·kotlin
键来大师1 天前
Android15 RK3588 修改默认不锁屏不休眠
android·java·framework·rk3588