对于一些应用程序,例如绘图应用、页面布局应用以及其他专注于图形输出的应用,创建精美的打印页面是一项关键功能。在这种情况下,仅仅打印图片或 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
抽象类用于处理打印生命周期,该生命周期包含四个主要的 callback 方法。您必须在打印适配器中实现这些方法,才能正确地与打印框架交互
onStart()
- 在打印过程开始时调用一次。如果您的应用有任何一次性准备任务要执行,例如获取要打印的数据快照,请在此处执行。在适配器中实现此方法不是必需的。onLayout()
- 每当用户更改影响输出的打印设置(例如不同的页面尺寸或页面方向)时调用。这为您的应用提供计算要打印页面布局的机会。此方法至少必须返回打印文档中预期的页数。onWrite()
- 调用此方法将打印页面渲染到要打印的文件中。每次调用onLayout()
后,此方法可能会被调用一次或多次。onFinish()
- 在打印过程结束时调用一次。如果您的应用有任何一次性清理任务要执行,请在此处执行。在适配器中实现此方法不是必需的。
以下部分描述如何实现布局和写入方法,这些方法对于打印适配器的功能至关重要。
注意:这些适配器方法在您的应用主线程上调用。如果您预计这些方法在实现中需要花费大量时间执行,请将其实现为在单独的线程中执行。例如,您可以将布局或打印文档写入工作封装在单独的 AsyncTask
对象中。
计算打印文档信息
在 PrintDocumentAdapter
类的实现中,您的应用必须能够指定正在创建的文档类型,并根据打印页面尺寸信息计算打印作业的总页数。适配器中 onLayout()
方法的实现会执行这些计算,并在 PrintDocumentInfo
类中提供有关打印作业预期输出的信息,包括页数和内容类型。以下代码示例展示了 PrintDocumentAdapter
的 onLayout()
方法的基本实现
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 文档文件中。此过程完成后,您调用 callback 对象的 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 页面。
Canvas
类使用 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 页面上绘制时,元素以磅(point)为单位指定,1 磅等于 1/72 英寸。确保使用此度量单位指定页面上元素的大小。对于绘制元素的位置,坐标系的原点 (0,0) 在页面的左上角。
提示:虽然 Canvas
对象允许您将打印元素放置在 PDF 文档的边缘,但许多打印机无法打印到物理纸张的边缘。使用此类构建打印文档时,请确保考虑页面的不可打印边缘。