Google 致力于为黑人社区推动种族平等。查看具体举措

打印自定义文档

对于某些应用,例如绘图应用、页面布局应用以及专注于图形输出的其他应用,创建精美的打印页面是一项重要功能。在这种情况下,打印图片或 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 文档的边缘放置打印元素,但许多打印机无法打印到实际纸张的边缘。当您使用该类构建打印文档时,确保考虑到页面的不可打印边缘。