コピーして貼り付け

Android のコピー&ペースト用のクリップボード ベース フレームワークは、次のようなプリミティブ データ型と複雑なデータ型をサポートしています。

  • テキスト文字列
  • 複雑なデータ構造
  • テキストとバイナリのストリーム データ
  • アプリケーション アセット

シンプルなテキストデータはクリップボード内に直接保存されます。複雑なデータは参照として保存され、貼り付け側アプリがコンテンツ プロバイダを使用して解決します。

コピー&ペーストは、単一のアプリ内でも、フレームワークを実装している複数のアプリ間でも機能します。

フレームワークの一部でコンテンツ プロバイダが使用されるため、このドキュメントは、ある程度 Android Content Provider API に精通していることを前提としています。

テキストを操作する

次の表に示すように、一部のコンポーネントは、テキストのコピーと貼り付けを標準でサポートしています。

コンポーネント テキストをコピーする テキストを貼り付ける
BasicTextField ✅ ✅
TextField ✅ ✅
SelectionContainer ✅

たとえば、次のスニペットでは、カード内のテキストをクリップボードにコピーし、コピーしたテキストを TextField に貼り付けることができます。TextField を長押しするか、カーソルハンドルをタップすると、テキストを貼り付けるメニューが表示されます。

val textFieldState = rememberTextFieldState()

Column {
    Card {
        SelectionContainer {
            Text("You can copy this text")
        }
    }
    BasicTextField(state = textFieldState)
}

テキストを貼り付けるには、キーボード ショートカットの Ctrl+V を使用します。キーボード ショートカットもデフォルトで使用できます。詳しくは、キーボード アクションを処理するをご覧ください。

ClipboardManager でコピー

ClipboardManager を使用して、テキストをクリップボードにコピーできます。setText() メソッドは、渡された AnnotatedString オブジェクトをクリップボードにコピーします。次のスニペットは、ユーザーがボタンをクリックしたときに「Hello, clipboard」をクリップボードにコピーします。

// Retrieve a ClipboardManager object
val clipboardManager = LocalClipboardManager.current

Button(
    onClick = {
        // Copy "Hello, clipboard" to the clipboard
        clipboardManager.setText(AnnotatedString("Hello, clipboard"))
    }
) {
    Text("Click to copy a text")
}

次のスニペットは同じ処理を行いますが、よりきめ細かい制御が可能です。一般的なユースケースは、パスワードなどの機密コンテンツのコピーです。ClipEntry は、クリップボード上のアイテムを記述します。これには、クリップボードのデータを記述する ClipData オブジェクトが含まれています。ClipData.newPlainText() メソッドは、String オブジェクトから ClipData オブジェクトを作成するための便利なメソッドです。作成した ClipEntry オブジェクトをクリップボードに設定するには、ClipboardManager オブジェクトに対して setClip() メソッドを呼び出します。

// Retrieve a ClipboardManager object
val clipboardManager = LocalClipboardManager.current

Button(
    onClick = {
        val clipData = ClipData.newPlainText("plain text", "Hello, clipboard")
        val clipEntry = ClipEntry(clipData)
        clipboardManager.setClip(clipEntry)
    }
) {
    Text("Click to copy a text")
}

ClipboardManager を使用して貼り付ける

クリップボードにコピーされたテキストにアクセスするには、ClipboardManager で getText() メソッドを呼び出します。getText() メソッドは、テキストがクリップボードにコピーされると AnnotatedString オブジェクトを返します。次のスニペットは、クリップボードのテキストを TextField のテキストに追加します。

val textFieldState = rememberTextFieldState()

Column {
    TextField(state = textFieldState)

    Button(
        onClick = {
            // The getText method returns an AnnotatedString object or null
            val annotatedString = clipboardManager.getText()
            if (annotatedString != null) {
                // The pasted text is placed on the tail of the TextField
                textFieldState.edit {
                    append(annotatedString.text)
                }
            }
        }
    ) {
        Text("Click to paste the text in the clipboard")
    }
}

リッチ コンテンツを操作する

ユーザーは画像、動画、およびその他の表現力豊かなコンテンツを好みます。アプリで ClipboardManager と ClipEntry を使用して、ユーザーがリッチ コンテンツをコピーできるようにすることができます。contentReceiver 修飾子を使用すると、リッチ コンテンツの貼り付けを実装できます。

リッチ コンテンツをコピーする

アプリは、リッチ コンテンツをクリップボードに直接コピーすることはできません。代わりに、アプリは URI オブジェクトをクリップボードに渡し、ContentProvider を使用してコンテンツへのアクセスを提供します。次のコード スニペットは、JPEG 画像をクリップボードにコピーする方法を示しています。詳しくは、データ ストリームをコピーするをご覧ください。

// Get a reference to the context
val context = LocalContext.current

Button(
    onClick = {
        // URI of the copied JPEG data
        val uri = Uri.parse("content://your.app.authority/0.jpg")
        // Create a ClipData object from the URI value
        // A ContentResolver finds a proper ContentProvider so that ClipData.newUri can set appropriate MIME type to the given URI
        val clipData = ClipData.newUri(context.contentResolver, "Copied", uri)
        // Create a ClipEntry object from the clipData value
        val clipEntry = ClipEntry(clipData)
        // Copy the JPEG data to the clipboard
        clipboardManager.setClip(clipEntry)
    }
) {
    Text("Copy a JPEG data")
}

リッチ コンテンツを貼り付ける

contentReceiver 修飾子を使用すると、変更されたコンポーネントの BasicTextField へのリッチ コンテンツの貼り付けを処理できます。次のコード スニペットは、貼り付けた画像データの URI を Uri オブジェクトのリストに追加します。

// A URI list of images
val imageList = remember { mutableStateListOf<Uri>() }

// Remember the ReceiveContentListener object as it is created inside a Composable scope
val receiveContentListener = remember {
    ReceiveContentListener { transferableContent ->
        // Handle the pasted data if it is image data
        when {
            // Check if the pasted data is an image or not
            transferableContent.hasMediaType(MediaType.Image) -> {
                // Handle for each ClipData.Item object
                // The consume() method returns a new TransferableContent object containging ignored ClipData.Item objects
                transferableContent.consume { item ->
                    val uri = item.uri
                    if (uri != null) {
                        imageList.add(uri)
                    }
                    // Mark the ClipData.Item object consumed when the retrieved URI is not null
                    uri != null
                }
            }
            // Return the given transferableContent when the pasted data is not an image
            else -> transferableContent
        }
    }
}

val textFieldState = rememberTextFieldState()

BasicTextField(
    state = textFieldState,
    modifier = Modifier
        .contentReceiver(receiveContentListener)
        .fillMaxWidth()
        .height(48.dp)
)

contentReceiver 修飾子は、ReceiveContentListener オブジェクトを引数として受け取り、ユーザーが変更されたコンポーネント内の BasicTextField にデータを貼り付けると、渡されたオブジェクトの onReceive メソッドを呼び出します。

TransferableContent オブジェクトが onReceive メソッドに渡されます。このオブジェクトは、このケースでは貼り付けによってアプリ間で転送できるデータを記述します。ClipEntry オブジェクトにアクセスするには、clipEntry 属性を参照します。

たとえば、ユーザーが複数の画像を選択してクリップボードにコピーすると、ClipEntry オブジェクトに複数の ClipData.Item オブジェクトが含まれる場合があります。各 ClipData.Item オブジェクトを消費済みまたは無視済みとしてマークし、無視された ClipData.Item オブジェクトを含む TransferableContent を返して、最も近い祖先の contentReceiver 修飾子がそれを受け取れるようにする必要があります。

TransferableContent.hasMediaType() メソッドを使用すると、TransferableContent オブジェクトがメディアタイプを含むアイテムを提供できるかどうかを判断できます。たとえば、次のメソッド呼び出しは、TransferableContent オブジェクトが画像を提供できる場合に true を返します。

transferableContent.hasMediaType(MediaType.Image)

複雑なデータを操作する

リッチ コンテンツの場合と同じ方法で、複雑なデータをクリップボードにコピーできます。詳しくは、コンテンツ プロバイダを使用して複雑なデータをコピーするをご覧ください。

リッチ コンテンツの場合と同じ方法で、複雑なデータの貼り付けを処理することもできます。貼り付けられたデータの URI を受け取ることができます。実際のデータは ContentProvider から取得できます。詳しくは、プロバイダからデータを取得するをご覧ください。

コンテンツのコピーに関するフィードバック

ユーザーがクリップボードにコンテンツをコピーする際にはフィードバックが期待されるため、Android 13(API レベル 33)以降では、コピー&ペーストを強化するフレームワークに加えて、デフォルトの UI がユーザーに表示されます。この機能により、通知が重複するリスクがあります。このエッジケースの詳細については、通知の重複を回避するをご覧ください。

Android 13 のクリップボード通知を示すアニメーション
図 1. Android 13 以降でコンテンツがクリップボードに入ると表示される UI

Android 12L(API レベル 32)以前でコピーする場合は、手動でフィードバックを提供します。推奨事項をご覧ください。

デリケートなコンテンツ

パスワードなどの機密コンテンツをユーザーがクリップボードにコピーできるようにアプリを設定する場合は、コピーされた機密コンテンツが UI に表示されないように、システムに通知する必要があります(図 2)。

機密コンテンツのフラグを設定してコピーされたテキストのプレビュー
図 2. 機密コンテンツのフラグを設定してコピーされたテキストのプレビュー。

ClipboardManager オブジェクトで setClip() メソッドを呼び出す前に、ClipData の ClipDescription にフラグを追加する必要があります。

// If your app is compiled with the API level 33 SDK or higher.
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
    }
}

// If your app is compiled with a lower SDK.
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean("android.content.extra.IS_SENSITIVE", true)
    }
}