打印自定义文档

对于某些应用,例如绘图应用、页面布局应用以及其他专注于图形输出的应用,创建精美的打印页面是一项重要功能。在这种情况下,打印图片或 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() 方法。

注意:对于每次调用 onLayout(),Android 打印框架可能会调用 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 页面上绘制元素,类似于在 Activity 布局上绘制。您可以使用 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 文档的边缘,但许多打印机无法打印到实体纸的边缘。使用此类构建打印文档时,请务必考虑页面的不可打印边缘。