打印自定义文档

对于某些应用程序,例如绘图应用程序、页面布局应用程序和其他专注于图形输出的应用程序,创建漂亮的打印页面是一个关键功能。在这种情况下,仅打印图像或 HTML 文档是不够的。这些类型应用程序的打印输出需要精确控制页面中的所有内容,包括字体、文本流、分页符、页眉、页脚和图形元素。

创建完全自定义于您应用程序的打印输出需要比前面讨论的方法更多的编程投入。您必须构建与打印框架通信的组件,调整打印机设置,绘制页面元素并在多个页面上管理打印。

本课程将向您展示如何连接打印管理器、创建打印适配器以及构建打印内容。

当您的应用程序直接管理打印过程时,在收到来自用户的打印请求后的第一步是连接到 Android 打印框架并获取 PrintManager 类的实例。此类允许您初始化打印作业并开始打印生命周期。以下代码示例显示了如何获取打印管理器并启动打印过程。

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

上面的示例代码演示了如何命名打印作业并设置 PrintDocumentAdapter 类的实例,该实例处理打印生命周期的步骤。打印适配器类的实现将在下一节中讨论。

注意: print() 方法中的最后一个参数接受一个 PrintAttributes 对象。您可以使用此参数向打印框架提供提示,并根据先前的打印周期预设选项,从而改善用户体验。您还可以使用此参数设置更适合打印内容的选项,例如,在打印方向为横向的照片时将其方向设置为横向。

打印适配器与 Android 打印框架交互并处理打印过程的步骤。此过程要求用户在创建打印文档之前选择打印机和打印选项。这些选择会影响最终输出,因为用户会选择具有不同输出功能、不同页面大小或不同页面方向的打印机。在进行这些选择时,打印框架会要求您的适配器进行布局并生成打印文档,以准备最终输出。用户点击打印按钮后,框架会获取最终的打印文档并将其传递给打印提供程序进行输出。在打印过程中,用户可以选择取消打印操作,因此您的打印适配器还必须侦听并对取消请求做出反应。

PrintDocumentAdapter 抽象类旨在处理打印生命周期,该生命周期具有四个主要的回调方法。您必须在打印适配器中实现这些方法才能与打印框架正确交互。

  • onStart() - 在打印过程开始时调用一次。如果您的应用程序有任何需要执行的一次性准备任务,例如获取要打印数据的快照,请在此处执行它们。在适配器中实现此方法不是必需的。
  • onLayout() - 每次用户更改影响输出的打印设置(例如不同的页面大小或页面方向)时调用,从而使您的应用程序有机会计算要打印的页面的布局。至少,此方法必须返回打印文档中预期的页面数。
  • onWrite() - 调用以将打印页面呈现到要打印的文件中。在每次 onLayout() 调用之后,此方法可能会被调用一次或多次。
  • onFinish() - 在打印过程结束时调用一次。如果您的应用程序有任何需要执行的一次性拆卸任务,请在此处执行它们。在适配器中实现此方法不是必需的。

以下部分介绍了如何实现布局和写入方法,这些方法对于打印适配器的功能至关重要。

注意: 这些适配器方法在应用程序的主线程上调用。如果您预计在实现中执行这些方法需要大量时间,请实现它们以便在单独的线程中执行。例如,您可以将布局或打印文档写入工作封装在单独的 AsyncTask 对象中。

计算打印文档信息

PrintDocumentAdapter 类的实现中,您的应用程序必须能够指定它正在创建的文档类型,并根据打印页面大小的信息计算打印作业的总页数。适配器中 onLayout() 方法的实现执行这些计算,并在 PrintDocumentInfo 类中提供有关打印作业预期输出的信息,包括页数和内容类型。以下代码示例显示了 PrintDocumentAdapteronLayout() 方法的基本实现。

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.");
    }
}

onLayout() 方法的执行可以有三种结果:完成、取消或在无法完成布局计算的情况下失败。您必须通过调用 PrintDocumentAdapter.LayoutResultCallback 对象的相应方法来指示其中一个结果。

注意: onLayoutFinished() 方法的布尔参数指示自上次请求以来布局内容是否实际发生更改。正确设置此参数允许打印框架避免不必要地调用 onWrite() 方法,本质上是缓存先前写入的打印文档并提高性能。

onLayout() 的主要工作是根据打印机的属性计算预期输出的页数。您如何计算此数字高度依赖于您的应用程序如何为打印布局页面。以下代码示例显示了一个实现,其中页数由打印方向确定。

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

写入打印文档文件

当需要将打印输出写入文件时,Android 打印框架会调用应用程序 PrintDocumentAdapter 类的 onWrite() 方法。该方法的参数指定应写入哪些页面以及要使用的输出文件。然后,您的方法实现必须将每个请求的内容页面呈现到多页 PDF 文档文件中。此过程完成后,您将调用回调对象的 onWriteFinished() 方法。

注意: Android 打印框架可能会为每次调用 onLayout() 调用 onWrite() 方法一次或多次。因此,当打印内容布局未更改时,将 onLayoutFinished() 方法的布尔参数设置为 false 非常重要,以避免不必要地重写打印文档。

注意: onLayoutFinished() 方法的布尔参数指示自上次请求以来布局内容是否实际发生更改。正确设置此参数允许打印框架避免不必要地调用 onLayout() 方法,本质上是缓存先前写入的打印文档并提高性能。

以下示例使用 PrintedPdfDocument 类创建 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);

    ...
}

此示例将 PDF 页面内容的呈现委托给 drawPage() 方法,这将在下一节中讨论。

与布局一样,onWrite() 方法的执行可以有三种结果:完成、取消或在无法写入内容的情况下失败。您必须通过调用 PrintDocumentAdapter.WriteResultCallback 对象的相应方法来指示其中一个结果。

注意: 渲染要打印的文档可能是一个资源密集型操作。为了避免阻塞应用程序的主用户界面线程,您应该考虑在单独的线程上执行页面呈现和写入操作,例如在 AsyncTask 中。有关使用异步任务等执行线程的更多信息,请参阅 进程和线程

绘制 PDF 页面内容

当您的应用程序打印时,您的应用程序必须生成一个 PDF 文档并将其传递给 Android 打印框架进行打印。您可以为此目的使用任何 PDF 生成库。本课程介绍了如何使用 PrintedPdfDocument 类从您的内容生成 PDF 页面。

PrintedPdfDocument 类使用 Canvas 对象在 PDF 页面上绘制元素,类似于在活动布局上绘制。您可以使用 Canvas 绘制方法在打印页面上绘制元素。以下示例代码演示了如何使用这些方法在 PDF 文档页面上绘制一些简单元素。

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

在使用 Canvas 在 PDF 页面上绘制时,元素以磅为单位指定,即 1/72 英寸。确保您使用此度量单位来指定页面上元素的大小。对于绘制元素的位置,坐标系从页面的左上角的 0,0 开始。

提示: 虽然 Canvas 对象允许您将打印元素放置在 PDF 文档的边缘,但许多打印机无法打印到物理纸张的边缘。在使用此类构建打印文档时,请确保考虑页面的不可打印边缘。