Ya está disponible la segunda Vista previa para desarrolladores de Android 11; pruébala y comparte tus comentarios.

Cómo imprimir documentos personalizados

En algunas aplicaciones, como apps de dibujo, apps de diseño de página y otras apps que se centran en la producción de gráficos, la posibilidad de crear páginas impresas atractivas es una función clave. En este caso, imprimir una imagen o un documento HTML no resulta suficiente. El resultado de la impresión de estos tipos de aplicaciones requiere un control preciso de todos los elementos de una página, como fuentes, flujo de texto, saltos de página, encabezados, pies de página y elementos gráficos.

Crear un resultado de impresión completamente personalizado para tu aplicación implica una mayor inversión en programación que la que se plantea en los enfoques que ya se han mencionado. Debes compilar componentes que se comuniquen con el marco de trabajo de impresión, ajustar la configuración de la impresora, dibujar los elementos de la página y administrar la impresión en varias páginas.

En esta lección, se muestra cómo conectarte con el administrador de impresión, crear un adaptador de impresión y crear contenido para imprimir.

Cuando tu aplicación administra directamente el proceso de impresión, el primer paso después de recibir una solicitud de impresión de tu usuario es conectarte al marco de trabajo de impresión de Android y obtener una instancia de la clase PrintManager. Esta clase te permite inicializar un trabajo de impresión y comenzar el ciclo de vida de impresión. En el siguiente ejemplo de código, se muestra cómo obtener el administrador de impresión y luego iniciar el proceso de impresión.

Kotlin

    private fun doPrint() {
        activity?.also { context ->
            // Get a PrintManager instance
            val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
            // Set job name, which will be displayed in the print queue
            val jobName = "${context.getString(R.string.app_name)} Document"
            // Start a print job, passing in a PrintDocumentAdapter implementation
            // to handle the generation of a print document
            printManager.print(jobName, MyPrintDocumentAdapter(context), null)
        }
    }
    

Java

    private void doPrint() {
        // Get a PrintManager instance
        PrintManager printManager = (PrintManager) getActivity()
                .getSystemService(Context.PRINT_SERVICE);

        // Set job name, which will be displayed in the print queue
        String jobName = getActivity().getString(R.string.app_name) + " Document";

        // Start a print job, passing in a PrintDocumentAdapter implementation
        // to handle the generation of a print document
        printManager.print(jobName, new MyPrintDocumentAdapter(getActivity()),
                null); //
    }
    

En el código de ejemplo anterior, se muestra cómo nombrar a un trabajo de impresión y configurar una instancia de la clase PrintDocumentAdapter que controla los pasos del ciclo de vida de impresión. La implementación de la clase del adaptador de impresión se analiza en la siguiente sección.

Nota: El último parámetro del método print() toma un objeto PrintAttributes. Puedes usar este parámetro para proporcionar sugerencias sobre el marco de trabajo de impresión y preestablecer opciones en función del ciclo de impresión anterior, lo que mejora la experiencia del usuario. También puedes usar este parámetro a fin de establecer opciones que sean más apropiadas para el contenido que se está imprimiendo, como establecer la orientación en horizontal cuando se imprime una foto que está en esa orientación.

Un adaptador de impresión interactúa con el marco de trabajo de impresión de Android y controla los pasos del proceso de impresión. Este proceso requiere que los usuarios seleccionen impresoras y opciones de impresión antes de crear un documento para imprimir. Estas selecciones pueden influir en el resultado final, ya que el usuario puede elegir impresoras con diferentes capacidades de salida, diferentes tamaños de página o diferentes orientaciones de página. A medida que se realizan estas selecciones, el marco de trabajo de impresión le pide a tu adaptador que diseñe y genere un documento de impresión, en preparación para el resultado final. Una vez que un usuario toca el botón de impresión, el marco de trabajo toma el documento de impresión final y lo pasa a un proveedor de impresión para su salida. Durante el proceso de impresión, los usuarios pueden elegir cancelar la acción de impresión, por lo que tu adaptador de impresión también debe escuchar las solicitudes de cancelación y responder a ellas.

La clase abstracta PrintDocumentAdapter está diseñada para controlar el ciclo de vida de la impresión, que tiene cuatro métodos principales de devolución de llamada. Debes implementar estos métodos en tu adaptador de impresión para interactuar correctamente con el marco de trabajo de impresión:

  • onStart(): Se lo llama una vez al comienzo del proceso de impresión. Si tu aplicación tiene que realizar tareas de preparación únicas, como obtener una instantánea de los datos que se imprimirán, ejecútalas aquí. No es obligatorio implementar este método en tu adaptador.
  • onLayout(): Se lo llama cada vez que un usuario cambia una configuración de impresión que afecta el resultado, como un tamaño de página o una orientación de página diferente, lo que le da a tu aplicación la oportunidad de calcular el diseño de las páginas que se imprimirán. Como mínimo, este método debe mostrar cuántas páginas se esperan en el documento impreso.
  • onWrite(): Se lo llama para representar las páginas impresas en un archivo para imprimir. Se puede llamar a este método una o más veces después de cada llamada onLayout().
  • onFinish(): Se lo llama una vez al final del proceso de impresión. Si tu aplicación tiene tareas de anulación únicas que ejecutar, puedes hacerlo aquí. No es obligatorio implementar este método en tu adaptador.

En las siguientes secciones, se describe cómo implementar los métodos de diseño y escritura, que son fundamentales para el funcionamiento de un adaptador de impresión.

Nota: Se llama a estos métodos del adaptador en el subproceso principal de tu aplicación. Si esperas que la ejecución de estos métodos en tu implementación lleve una cantidad significativa de tiempo, impleméntalos para que se ejecuten en un subproceso separado. Por ejemplo, puedes encapsular el trabajo de escritura de impresión o diseño de documentos en objetos AsyncTask separados.

Cómo calcular la información del documento para imprimir

Dentro de una implementación de la clase PrintDocumentAdapter, tu aplicación debe poder especificar el tipo de documento que está creando y calcular la cantidad total de páginas del trabajo de impresión, dada la información sobre el tamaño de la página impresa. La implementación del método onLayout() en el adaptador realiza estos cálculos y proporciona información sobre el resultado esperado del trabajo de impresión en una clase PrintDocumentInfo, que incluye la cantidad de páginas y el tipo de contenido. En el siguiente ejemplo de código, se muestra una implementación básica del método onLayout() para un PrintDocumentAdapter:

Kotlin

    override fun onLayout(
            oldAttributes: PrintAttributes?,
            newAttributes: PrintAttributes,
            cancellationSignal: CancellationSignal?,
            callback: LayoutResultCallback,
            extras: Bundle?
    ) {
        // Create a new PdfDocument with the requested page attributes
        pdfDocument = PrintedPdfDocument(activity, newAttributes)

        // Respond to cancellation request
        if (cancellationSignal?.isCanceled == true) {
            callback.onLayoutCancelled()
            return
        }

        // Compute the expected number of printed pages
        val pages = computePageCount(newAttributes)

        if (pages > 0) {
            // Return print information to print framework
            PrintDocumentInfo.Builder("print_output.pdf")
                    .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                    .setPageCount(pages)
                    .build()
                    .also { info ->
                        // Content layout reflow is complete
                        callback.onLayoutFinished(info, true)
                    }
        } else {
            // Otherwise report an error to the print framework
            callback.onLayoutFailed("Page count calculation failed.")
        }
    }
    

Java

    @Override
    public void onLayout(PrintAttributes oldAttributes,
                         PrintAttributes newAttributes,
                         CancellationSignal cancellationSignal,
                         LayoutResultCallback callback,
                         Bundle metadata) {
        // Create a new PdfDocument with the requested page attributes
        pdfDocument = new PrintedPdfDocument(getActivity(), newAttributes);

        // Respond to cancellation request
        if (cancellationSignal.isCanceled() ) {
            callback.onLayoutCancelled();
            return;
        }

        // Compute the expected number of printed pages
        int pages = computePageCount(newAttributes);

        if (pages > 0) {
            // Return print information to print framework
            PrintDocumentInfo info = new PrintDocumentInfo
                    .Builder("print_output.pdf")
                    .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                    .setPageCount(pages)
                    .build();
            // Content layout reflow is complete
            callback.onLayoutFinished(info, true);
        } else {
            // Otherwise report an error to the print framework
            callback.onLayoutFailed("Page count calculation failed.");
        }
    }
    

La ejecución del método onLayout() puede tener tres resultados: finalización, cancelación o falla en caso de que no se pueda completar el cálculo del diseño. Debes indicar uno de estos resultados llamando al método apropiado del objeto PrintDocumentAdapter.LayoutResultCallback.

Nota: El parámetro booleano del método onLayoutFinished() indica si el contenido del diseño en verdad cambió desde la última solicitud. Configurar de manera correcta este parámetro permite que el marco de trabajo de impresión evite llamar al método onWrite() si no es necesario, básicamente, porque almacena en caché el documento de impresión escrito con anterioridad y mejora el rendimiento.

El trabajo principal de onLayout() consiste en calcular la cantidad de páginas que se esperan como resultado según los atributos de la impresora. La forma en que calculas este número depende, en gran medida, de cómo distribuye tu aplicación las páginas para imprimir. En el siguiente ejemplo de código, se muestra una implementación en la que la orientación de impresión determina la cantidad de páginas:

Kotlin

    private fun computePageCount(printAttributes: PrintAttributes): Int {
        var itemsPerPage = 4 // default item count for portrait mode

        val pageSize = printAttributes.mediaSize
        if (!pageSize.isPortrait) {
            // Six items per page in landscape orientation
            itemsPerPage = 6
        }

        // Determine number of print items
        val printItemCount: Int = getPrintItemCount()

        return Math.ceil((printItemCount / itemsPerPage.toDouble())).toInt()
    }
    

Java

    private int computePageCount(PrintAttributes printAttributes) {
        int itemsPerPage = 4; // default item count for portrait mode

        MediaSize pageSize = printAttributes.getMediaSize();
        if (!pageSize.isPortrait()) {
            // Six items per page in landscape orientation
            itemsPerPage = 6;
        }

        // Determine number of print items
        int printItemCount = getPrintItemCount();

        return (int) Math.ceil(printItemCount / itemsPerPage);
    }
    

Cómo escribir un archivo de documento impreso

Cuando llega el momento de escribir la salida de impresión en un archivo, el marco de trabajo de impresión de Android llama al método onWrite() de la clase PrintDocumentAdapter de tu aplicación. Los parámetros del método especifican qué páginas deben escribirse y el archivo de salida que se utilizará. La implementación de este método debe representar cada página de contenido solicitada en un archivo de documento PDF de varias páginas. Cuando se completa este proceso, se llama al método onWriteFinished() del objeto de devolución de llamada.

Nota: El marco de trabajo de impresión de Android puede llamar al método onWrite() una o más veces para cada llamada a onLayout(). Por este motivo, es importante establecer el parámetro booleano del método onLayoutFinished() en false cuando el diseño del contenido de impresión no se modificó, a fin de evitar reescrituras innecesarias del documento de impresión.

Nota: El parámetro booleano del método onLayoutFinished() indica si el contenido del diseño en verdad cambió desde la última solicitud. Configurar de manera correcta este parámetro permite que el marco de trabajo de impresión evite llamar al método onLayout() si no es necesario, básicamente, porque almacena en caché el documento de impresión escrito con anterioridad y mejora el rendimiento.

En el siguiente ejemplo, se muestra la mecánica básica de este proceso con la clase PrintedPdfDocument para crear un archivo PDF:

Kotlin

    override fun onWrite(
            pageRanges: Array<out PageRange>,
            destination: ParcelFileDescriptor,
            cancellationSignal: CancellationSignal?,
            callback: WriteResultCallback
    ) {
        // Iterate over each page of the document,
        // check if it's in the output range.
        for (i in 0 until totalPages) {
            // Check to see if this page is in the output range.
            if (containsPage(pageRanges, i)) {
                // If so, add it to writtenPagesArray. writtenPagesArray.size()
                // is used to compute the next output page index.
                writtenPagesArray.append(writtenPagesArray.size(), i)
                pdfDocument?.startPage(i)?.also { page ->

                    // check for cancellation
                    if (cancellationSignal?.isCanceled == true) {
                        callback.onWriteCancelled()
                        pdfDocument?.close()
                        pdfDocument = null
                        return
                    }

                    // Draw page content for printing
                    drawPage(page)

                    // Rendering is complete, so page can be finalized.
                    pdfDocument?.finishPage(page)
                }
            }
        }

        // Write PDF document to file
        try {
            pdfDocument?.writeTo(FileOutputStream(destination.fileDescriptor))
        } catch (e: IOException) {
            callback.onWriteFailed(e.toString())
            return
        } finally {
            pdfDocument?.close()
            pdfDocument = null
        }
        val writtenPages = computeWrittenPages()
        // Signal the print framework the document is complete
        callback.onWriteFinished(writtenPages)

        ...
    }
    

Java

    @Override
    public void onWrite(final PageRange[] pageRanges,
                        final ParcelFileDescriptor destination,
                        final CancellationSignal cancellationSignal,
                        final WriteResultCallback callback) {
        // Iterate over each page of the document,
        // check if it's in the output range.
        for (int i = 0; i < totalPages; i++) {
            // Check to see if this page is in the output range.
            if (containsPage(pageRanges, i)) {
                // If so, add it to writtenPagesArray. writtenPagesArray.size()
                // is used to compute the next output page index.
                writtenPagesArray.append(writtenPagesArray.size(), i);
                PdfDocument.Page page = pdfDocument.startPage(i);

                // check for cancellation
                if (cancellationSignal.isCanceled()) {
                    callback.onWriteCancelled();
                    pdfDocument.close();
                    pdfDocument = null;
                    return;
                }

                // Draw page content for printing
                drawPage(page);

                // Rendering is complete, so page can be finalized.
                pdfDocument.finishPage(page);
            }
        }

        // Write PDF document to file
        try {
            pdfDocument.writeTo(new FileOutputStream(
                    destination.getFileDescriptor()));
        } catch (IOException e) {
            callback.onWriteFailed(e.toString());
            return;
        } finally {
            pdfDocument.close();
            pdfDocument = null;
        }
        PageRange[] writtenPages = computeWrittenPages();
        // Signal the print framework the document is complete
        callback.onWriteFinished(writtenPages);

        ...
    }
    

En este ejemplo, se delega el procesamiento del contenido de la página PDF al método drawPage(), que se analiza en la siguiente sección.

Al igual que con el diseño, la ejecución del método onWrite() puede tener tres resultados: finalización, cancelación o falla en caso de que no se pueda escribir el contenido. Debes indicar uno de estos resultados llamando al método apropiado de PrintDocumentAdapter.WriteResultCallback.

Nota: La operación de procesamiento de un documento para imprimir puede requerir muchos recursos. A fin de evitar bloquear el subproceso principal de la interfaz de usuario de tu aplicación, debes considerar realizar las operaciones de procesamiento y escritura de la página en un subproceso separado, por ejemplo, en un objeto AsyncTask. Para obtener más información sobre cómo trabajar con subprocesos de ejecución como tareas asíncronas, consulta Procesos y subprocesos.

Cómo dibujar contenido de la página PDF

Cuando tu aplicación imprime contenido, debe generar un documento PDF y pasarlo al marco de trabajo de impresión de Android para que se imprima. Puedes usar cualquier biblioteca de generación de PDF para este fin. En esta lección, se muestra cómo usar la clase PrintedPdfDocument para generar páginas PDF a partir de tu contenido.

La clase PrintedPdfDocument usa un objeto Canvas para dibujar elementos en una página PDF, lo cual es similar al dibujo en un diseño de actividad. Puedes dibujar elementos en la página impresa con los métodos de dibujo de Canvas. En el siguiente código de ejemplo, se muestra cómo dibujar algunos elementos simples en una página de documento PDF con estos métodos:

Kotlin

    private fun drawPage(page: PdfDocument.Page) {
        page.canvas.apply {

            // units are in points (1/72 of an inch)
            val titleBaseLine = 72f
            val leftMargin = 54f

            val paint = Paint()
            paint.color = Color.BLACK
            paint.textSize = 36f
            drawText("Test Title", leftMargin, titleBaseLine, paint)

            paint.textSize = 11f
            drawText("Test paragraph", leftMargin, titleBaseLine + 25, paint)

            paint.color = Color.BLUE
            drawRect(100f, 100f, 172f, 172f, paint)
        }
    }
    

Java

    private void drawPage(PdfDocument.Page page) {
        Canvas canvas = page.getCanvas();

        // units are in points (1/72 of an inch)
        int titleBaseLine = 72;
        int leftMargin = 54;

        Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setTextSize(36);
        canvas.drawText("Test Title", leftMargin, titleBaseLine, paint);

        paint.setTextSize(11);
        canvas.drawText("Test paragraph", leftMargin, titleBaseLine + 25, paint);

        paint.setColor(Color.BLUE);
        canvas.drawRect(100, 100, 172, 172, paint);
    }
    

Cuando se usa Canvas para dibujar en una página PDF, se especifican los elementos en puntos, que equivalen a 1/72 de pulgada. Asegúrate de utilizar esta unidad de medida para especificar el tamaño de los elementos en la página. Para el posicionamiento de elementos dibujados, el sistema de coordenadas comienza en 0,0 para la esquina superior izquierda de la página.

Sugerencia: Si bien el objeto Canvas te permite colocar elementos de impresión en el borde de un documento PDF, muchas impresoras no pueden imprimir hasta el borde de una hoja de papel. Asegúrate de tener en cuenta los bordes no imprimibles de la página cuando crees un documento de impresión con esta clase.