Cómo imprimir documentos personalizados

En algunas aplicaciones, como las de dibujo, las de diseño de página y otras que se centran en la salida de gráficos, la creación de atractivas páginas impresas es una función clave. En este caso, imprimir una imagen o un documento HTML no alcanza. El resultado de impresión de estos tipos de aplicaciones requiere un control preciso de todo lo que se incluye en una página, como las fuentes, el flujo de texto, los saltos de página, los encabezados, los pies de página y los elementos gráficos.

Crear un resultado de impresión completamente personalizado para tu aplicación requiere más inversión en programación que los enfoques mencionados con anterioridad. Debes compilar componentes que se comuniquen con el marco de trabajo de impresión, ajustar la configuración de la impresora, dibujar elementos de 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 framework 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 e 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 un trabajo de impresión y configurar una instancia de la clase PrintDocumentAdapter que controle los pasos del ciclo de vida de impresión. La implementación de la clase de 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 para el framework de impresión y opciones predefinidas basadas en el ciclo de impresión anterior, lo que mejora la experiencia del usuario. También puedes usar este parámetro a fin de configurar opciones que sean más apropiadas para el contenido que se imprime, como establecer la orientación en horizontal al imprimir 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 elige impresoras con diferentes capacidades de salida, diferentes tamaños de página u orientaciones de página diferentes. 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 el usuario presiona el botón de impresión, el framework 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 el adaptador de impresión también debe escuchar las solicitudes de cancelación y reaccionar 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 framework 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 necesario implementar este método en tu adaptador.
  • onLayout(): Se 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 renderizar 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 a onLayout().
  • onFinish(): Se lo llama una vez al final del proceso de impresión. Si tu aplicación tiene que realizar tareas de anulación únicas, ejecútalas 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 demore 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, lo 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 realmente cambió desde la última solicitud. Configurar de forma correcta este parámetro permite que el framework de impresión evite llamar innecesariamente al método onWrite(), 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 tu aplicación distribuye 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 se deben escribir y el archivo de salida que se usará. La implementación de este método debe procesar 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 framework de impresión de Android puede llamar al método onWrite() una o más veces por 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ó, para evitar reescrituras innecesarias del documento de impresión.

Nota: El parámetro booleano del método onLayoutFinished() indica si el contenido del diseño realmente cambió desde la última solicitud. Configurar de forma correcta este parámetro permite que el framework de impresión evite llamar innecesariamente al método onLayout(), 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 del objeto PrintDocumentAdapter.WriteResultCallback.

Nota: La operación de procesamiento de un documento para imprimir puede requerir muchos recursos. Para 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 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, esta debe generar un documento PDF y pasarlo al marco de trabajo de impresión de Android para su impresión. 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, los elementos se especifican en puntos, que son 1/72 de pulgada. Asegúrate de usar 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 física. Asegúrate de tener en cuenta los bordes no imprimibles de la página cuando crees un documento de impresión con esta clase.