列印自訂文件

對部分應用程式 (例如繪圖應用程式、頁面版面配置應用程式,以及其他著重於圖像輸出的應用程式) 來說,建立精美的列印頁面是一大關鍵。在這種情況下,只能列印圖片或 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 物件中。

Compute 列印文件資訊

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 頁面中繪製元素,這與在活動版面配置中繪圖類似。您可以使用 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 文件的邊緣,但許多印表機無法列印實體紙的邊緣。使用此類別建立列印文件時,請務必考量到網頁無法列印的邊緣。