Cómo imprimir documentos personalizados

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

Crear una salida de impresión completamente personalizada para tu aplicación requiere más más inversión en programación que los enfoques antes analizados. Debes compilar componentes que comunicarse con el marco 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.

Esta lección te 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 un 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. El implementación de la clase de adaptador de impresión se analiza en la siguiente sección.

Nota: El último parámetro de print() toma un objeto PrintAttributes. Puedes usar este parámetro para proporcionar sugerencias para el marco de trabajo de impresión y las opciones preestablecidas basadas en el ciclo de impresión anterior, lo que mejora la experiencia del usuario. También puedes usar este parámetro para configurar las opciones que son sea más apropiada para el contenido que se está imprimiendo, como establecer la orientación en horizontal. al imprimir una foto con esa orientación.

Un adaptador de impresión interactúa con el framework de impresión de Android y controla los pasos de la proceso de impresión. Este proceso requiere que los usuarios seleccionen impresoras y opciones de impresión antes de crear un documento para imprimirlo. Estas selecciones pueden influir en el resultado final a medida que el usuario elige. con diferentes funciones de salida, diferentes tamaños de página o diferentes orientaciones de página. A medida que se realizan estas selecciones, el framework de impresión le pide a tu adaptador que diseñe y genere una imprimir el documento para preparar el resultado final. Una vez que el usuario presiona 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 la impresión 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 detectar y reaccionar a las solicitudes de cancelación.

La clase abstracta PrintDocumentAdapter está diseñada para controlar las de 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 llama una vez a las inicio del proceso de impresión. Si tu aplicación tiene tareas de preparación únicas realizar, como obtener una instantánea de los datos que se imprimirán, ejecútalos aquí. Implementación este método en el adaptador no es obligatorio.
  • onLayout(): Se llama cada vez que un el usuario cambia una configuración de impresión que afecta el resultado, como un tamaño de página diferente o la orientación de la página, lo que le da a tu aplicación la oportunidad de procesar el diseño de la las páginas que se imprimirán. Como mínimo, este método debe mostrar la cantidad de páginas esperadas. del documento impreso.
  • onWrite(): Se lo llama para renderizar el contenido impreso. las páginas en un archivo para imprimir. Se puede llamar a este método una o más veces después de cada onLayout() llamada.
  • onFinish(): Se llama una vez al final del proceso de impresión. Si tu aplicación tiene que realizar alguna tarea de eliminación única, ejecutarlas 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 que son esenciales 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 significativamente impleméntalas para que se ejecuten en un subproceso independiente. Por ejemplo, puedes encapsular el trabajo de escritura de documentos de diseño o impresión en objetos AsyncTask separados

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

En una implementación de la clase PrintDocumentAdapter, tu archivo la aplicación debe poder especificar el tipo de documento que está creando y calcular el total la cantidad de páginas del trabajo de impresión, según la información del 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 de la de impresión en una clase PrintDocumentInfo, incluida 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 tienen tres resultados: finalización, cancelación o falla en caso de que el cálculo del no se puede completar el diseño. Debes indicar uno de estos resultados llamando al del objeto PrintDocumentAdapter.LayoutResultCallback.

Nota: El parámetro booleano del El método onLayoutFinished() indica si el contenido del diseño cambió o no. desde la última solicitud. Configurar correctamente este parámetro permite que el marco de trabajo de impresión evite llamar innecesariamente al método onWrite(), para almacenar en caché el documento de impresión que se escribió anteriormente y mejorar el rendimiento.

El trabajo principal de onLayout() es 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 diseña las páginas para imprimir. En el siguiente ejemplo de código, se muestra una implementación en la que la cantidad de páginas es de determinado por la orientación de impresión:

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 y el archivo de salida que se usará. Tu implementación de este método debe renderizar cada la página de contenido solicitada a un archivo de documento PDF de varias páginas. Cuando se completa este proceso, 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 de Usa el método onLayoutFinished() en false cuando no se haya cambiado el diseño del contenido de impresión. para evitar reescrituras innecesarias del documento impreso.

Nota: El parámetro booleano del El método onLayoutFinished() indica si el contenido del diseño cambió o no. desde la última solicitud. Configurar correctamente este parámetro permite que el marco de trabajo de impresión evite llamar innecesariamente al método onLayout(), para almacenar en caché el documento de impresión que se escribió anteriormente y mejorar 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 a drawPage() que se analiza en la siguiente sección.

Al igual que con el diseño, la ejecución de onWrite() puede tener tres resultados: finalización, cancelación o falla en caso de que el no se puede 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. En 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 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 imprima, deberá generar un documento PDF y pasarlo a el framework de impresión de Android para imprimir. Puedes usar cualquier biblioteca de generación de PDF para esto que no tiene ningún propósito específico. 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 Canvas. para dibujar elementos en una página PDF, de manera similar a dibujar en un diseño de actividad. Puedes dibujar elementos de la página impresa con los métodos de dibujo Canvas Lo siguiente código de ejemplo que muestra cómo dibujar algunos elementos simples en la página de un documento PDF usando 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 es 1/72 de pulgada. Asegúrate de utilizar esta unidad de medida para especificar el tamaño de elementos de la página. Para el posicionamiento de elementos dibujados, el sistema de coordenadas comienza en 0,0 en la esquina superior izquierda de la página.

Nota: Si bien el objeto Canvas te permite colocar elementos de impresión, al borde de un documento PDF, muchas impresoras no pueden imprimir en el borde de un hoja de papel física. Asegúrate de tener en cuenta los bordes no imprimibles de la página cuando crearás un documento impreso con esta clase.