コピーして貼り付け

コピー&ペースト用の 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 オブジェクトをクリップボードに設定するには、setClip() メソッドを ClipboardManager オブジェクトで呼び出します。

// 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 で貼り付ける

クリップボードにコピーされたテキストにアクセスするには、getText() メソッドを ClipboardManager で呼び出します。クリップボードにテキストがコピーされると、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)
    }
}