Скопируйте и вставьте

Платформа копирования и вставки Android на основе буфера обмена поддерживает примитивные и сложные типы данных, в том числе:

  • Текстовые строки
  • Сложные структуры данных
  • Текстовые и двоичные потоковые данные
  • Ресурсы приложения

Простые текстовые данные хранятся непосредственно в буфере обмена, а сложные данные хранятся в виде ссылки, которую приложение для вставки разрешает поставщику контента.

Копирование и вставка работают как внутри приложения, так и между приложениями, реализующими эту платформу.

Поскольку часть платформы использует поставщиков контента, этот документ предполагает некоторое знакомство с API контент-провайдеров Android .

Работа с текстом

Некоторые компоненты поддерживают копирование и вставку текста из поля, как показано в следующей таблице.

Компонент Копирование текста Вставка текста
Базистекстовое поле
Текстовое поле
Контейнер выбора

Например, вы можете скопировать текст карточки в буфер обмена в следующем фрагменте и вставить скопированный текст в TextField . Меню для вставки текста отображается нажатием и удерживанием TextField или нажатием маркера курсора.

val textFieldState = rememberTextFieldState()

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

Вы можете вставить текст с помощью следующего сочетания клавиш: Ctrl + V. Сочетание клавиш также доступно по умолчанию. Подробности см. в разделе «Обработка действий с клавиатуры» .

Копировать с помощью ClipboardManager

Вы можете копировать тексты в буфер обмена с помощью ClipboardManager . Его метод setText() копирует переданный объект String в буфер обмена. Следующий фрагмент копирует «Привет, буфер обмена» в буфер обмена, когда пользователь нажимает кнопку.

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

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

Следующий фрагмент делает то же самое, но дает более детальный контроль. Распространенным вариантом использования является копирование конфиденциального содержимого , например пароля. ClipEntry описывает элемент в буфере обмена. Он содержит объект ClipData , который описывает данные в буфере обмена. Метод ClipData.newPlainText() — это удобный метод создания объекта ClipData из объекта String. Вы можете поместить созданный объект 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 .

var 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(text.toString())
                }
            }
        }
    ) {
        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 by remember{ mutableListOf<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 в качестве аргумента и вызывает метод onReceive переданного объекта, когда пользователь вставляет данные в BasicTextField внутри измененного компонента.

Объект TransferableContent передается методу onReceive, который описывает данные, которые в данном случае можно передавать между приложениями путем вставки. Вы можете получить доступ к объекту ClipEntry , обратившись к атрибуту clipEntry .

Объект ClipEntry может иметь несколько объектов ClipData.Item , например, когда пользователь выбирает несколько изображений и копирует их в буфер обмена. Вы должны пометить использованный или проигнорированный для каждого объекта ClipData.Item и вернуть TransferableContent содержащий игнорируемые объекты ClipData.Item , чтобы модификатор contentReceiver ближайшего предка мог его получить.

Метод TransferableContent.hasMediaType() может помочь вам определить, может ли объект TransferableContent предоставить элемент с типом мультимедиа. Например, следующий вызов метода возвращает true , если объект TransferableContent может предоставить изображение.

transferableContent.hasMediaType(MediaType.Image)

Работа со сложными данными

Вы можете копировать сложные данные в буфер обмена так же, как и богатый контент. Дополнительные сведения см. в разделе Использование поставщиков контента для копирования сложных данных .

Вы также можете обрабатывать вставки сложных данных таким же образом для расширенного контента. Вы можете получить URI вставленных данных. Фактические данные можно получить из ContentProvider . Дополнительную информацию см. в разделе Получение данных от поставщика .

Отзыв о копировании контента

Пользователи ожидают обратной связи при копировании контента в буфер обмена, поэтому в дополнение к платформе, обеспечивающей копирование и вставку, Android показывает пользователям пользовательский интерфейс по умолчанию при копировании в Android 13 (уровень API 33) и выше. Из-за этой функции существует риск дублирования уведомлений. Подробнее об этом крайнем случае можно узнать в разделе «Избегайте дублирования уведомлений» .

Анимация, показывающая уведомление буфера обмена Android 13.
Рис. 1. Пользовательский интерфейс, отображаемый при попадании содержимого в буфер обмена в Android 13 и более поздних версиях.

Вручную предоставляйте пользователям обратную связь при копировании в Android 12L (уровень API 32) и ниже. Смотрите рекомендацию .

Деликатный контент

Если вы решите, чтобы ваше приложение позволяло пользователю копировать конфиденциальный контент в буфер обмена, например пароли, ваше приложение должно сообщить об этом системе, чтобы система могла избежать отображения скопированного конфиденциального контента в пользовательском интерфейсе (рис. 2).

Предварительный просмотр скопированного текста помечает конфиденциальный контент.
Рисунок 2. Предварительный просмотр скопированного текста с флагом конфиденциального содержимого.

Вы должны добавить флаг ClipDescription в ClipData перед вызовом метода setClip() для объекта ClipboardManager :

// 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)
    }
}