打印自定义文档

对于某些应用,例如绘图应用、页面布局应用以及其他主要 制作精美的打印页面是一项关键功能。在这种情况下, 可打印图片或 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 打印框架可能会针对每个onWrite()onLayout() 的调用。因此, 设置布尔值参数的 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 文档的边缘。 请务必考虑网页无法打印的边缘 您将通过此类构建打印文档。