Copiar y pegar

El framework de Android basado en el portapapeles para copiar y pegar admite tipos de datos primitivos y complejos, como los siguientes:

  • Cadenas de texto
  • Estructuras de datos complejas
  • Datos de flujo de texto y binario
  • Recursos de aplicación

Los datos de texto simples se almacenan directamente en el portapapeles mientras que los datos complejos se almacenan como referencia que la aplicación de pegado resuelve con un proveedor de contenido.

La opción de copiar y pegar funciona tanto dentro de una aplicación como entre aplicaciones que implementan el framework.

Debido a que parte del framework usa proveedores de contenido, En este documento, se asume que conoces la API de Android Content Provider.

Cómo trabajar con texto

Algunos componentes admiten copiar y pegar texto de forma predeterminada, como se muestra en la siguiente tabla.

Componente Copiando texto Cómo pegar texto
BasicTextField
TextField
SelectionContainer

Por ejemplo, puedes copiar el texto de la tarjeta en el portapapeles en el siguiente fragmento y pegar el texto copiado en TextField. Muestras el menú para pegar el texto por un elemento tocar y mantén presionado el TextField o presiona el controlador del cursor.

val textFieldState = rememberTextFieldState()

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

Puedes pegar el texto con la siguiente combinación de teclas: Ctrl + V La combinación de teclas también está disponible de forma predeterminada. Consulta Cómo controlar las acciones del teclado para obtener más información.

Copiar con ClipboardManager

Puedes copiar textos en el portapapeles con ClipboardManager. Su método setText() copia el objeto de cadena pasado al portapapeles. En el siguiente fragmento, se copia “Hola, portapapeles” al portapapeles cuando el usuario hace clic en el botón.

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

El siguiente fragmento hace lo mismo, pero te brinda un control más detallado. Un caso de uso común es copiar contenido sensible, como una contraseña. ClipEntry describe un elemento del portapapeles. Contiene un objeto ClipData que describe los datos del portapapeles. El método ClipData.newPlainText() es un método de conveniencia para crear un objeto ClipData a partir de un objeto String. Puedes configurar el objeto ClipEntry creado en el portapapeles llamando al método setClip() sobre el objeto 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")
}

Cómo pegar con ClipboardManager

Para acceder al texto copiado en el portapapeles, llama al método getText() sobre ClipboardManager. Su método getText() muestra un objeto AnnotatedString cuando se copia un texto en el portapapeles. El siguiente fragmento agrega texto del portapapeles al texto de 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")
    }
}

Cómo trabajar con contenido enriquecido

A los usuarios les encantan las imágenes, los videos y otro contenido expresivo. Tu app puede permitir que el usuario copie contenido enriquecido con ClipboardManager y ClipEntry. El modificador contentReceiver te ayuda a implementar el pegado de contenido enriquecido.

Cómo copiar contenido enriquecido

Tu app no puede copiar contenido enriquecido directamente en el portapapeles. En cambio, tu app pasa un objeto URI al portapapeles y proporciona acceso al contenido con un ContentProvider. En el siguiente fragmento de código, se muestra cómo copiar una imagen JPEG en el portapapeles. Consulta Cómo copiar flujos de datos para obtener más información.

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

Cómo pegar contenido enriquecido

Con el modificador contentReceiver, puedes controlar el pegado de contenido enriquecido en BasicTextField en el componente modificado. En el siguiente fragmento de código, se agrega el URI pegado de los datos de imagen a una lista de objetos 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)
)

El modificador contentReceiver toma un objeto ReceiveContentListener como su argumento y llama a onReceive del objeto que se pasó cuando el usuario pega datos a BasicTextField dentro del componente modificado.

Se pasa un objeto TransferableContent al método onRecibir. que describe los datos que se pueden transferir entre apps pegando en este caso. Para acceder al objeto ClipEntry, puedes consultar el atributo clipEntry.

Un objeto ClipEntry puede tener varios objetos ClipData.Item Cuando el usuario selecciona varias imágenes y las copia en el portapapeles por ejemplo. Debes marcar como consumido o ignorado cada objeto ClipData.Item y mostrar un TransferableContent que contenga los objetos ClipData.Item ignorados para que el modificador contentReceiver de ancestro más cercano pueda recibirlo.

El método TransferableContent.hasMediaType() puede ayudarte a determinar si el objeto TransferableContent puede proporcionar un elemento con el tipo de medio. Por ejemplo, la siguiente llamada de método muestra true. si el objeto TransferableContent puede proporcionar una imagen.

transferableContent.hasMediaType(MediaType.Image)

Cómo trabajar con datos complejos

Puedes copiar datos complejos en el portapapeles tal como lo haces para el contenido enriquecido. Para obtener más información, consulta Usa proveedores de contenido para copiar datos complejos.

También puedes controlar las pegaciones de datos complejos del mismo modo para el contenido enriquecido. Puedes recibir un URI de los datos pegados. Los datos reales se pueden recuperar a partir de un ContentProvider. Para obtener más información, consulta Cómo recuperar datos del proveedor.

Comentarios sobre la copia de contenido

Los usuarios esperan algún tipo de comentario cuando copian contenido al portapapeles, por lo que, además del framework que permite las acciones de copiar y pegar, Android muestra una IU predeterminada a los usuarios cuando copian contenido en Android 13 (nivel de API 33) y versiones posteriores. Debido a esta función, existe el riesgo de notificaciones duplicadas. Puedes obtener más información sobre este caso extremo en Cómo evitar notificaciones duplicadas.

Una animación que muestra la notificación del portapapeles de Android 13
Figura 1: IU que se muestra cuando el contenido ingresa al portapapeles en Android 13 y versiones posteriores.

Proporciona comentarios a los usuarios de forma manual cuando se copian en Android 12L (nivel de API 32) y versiones anteriores. Consulta la recomendación.

Contenido sensible

Si decides que tu app permita que el usuario copie contenido sensible en el portapapeles, como contraseñas, la app debe informarle al sistema para que este pueda evitar mostrar el contenido sensible copiado en la IU (figura 2).

Vista previa del texto copiado con marcas en el contenido sensible.
Figura 2: Vista previa del texto copiado con una marca de contenido sensible.

Debes agregar una marca a ClipDescription en ClipData antes de llamar al método setClip() sobre el objeto 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)
    }
}