打印自定义文档

对于某些应用程序(例如绘图应用程序、页面布局应用程序和其他侧重于图形输出的应用程序),创建漂亮的打印页面是一项关键功能。在这种情况下,仅仅打印图像或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 文档的边缘,但许多打印机无法打印到物理纸张的边缘。使用此类构建打印文档时,请确保考虑页面的不可打印边缘。