カスタム ドキュメントの印刷

描画アプリ、ページ レイアウト アプリ、グラフィック出力に重点を置いたその他のアプリなど、一部のアプリケーションでは、美しい印刷ページを作成することが重要な機能です。この場合、画像や 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 抽象クラスは、印刷ライフサイクルを処理するように設計されています。このライフサイクルには、4 つの主要なコールバック メソッドがあります。印刷フレームワークと適切にやり取りするには、印刷アダプターにこれらのメソッドを実装する必要があります。

  • onStart() - 印刷プロセスの開始時に 1 回呼び出されます。印刷するデータのスナップショットの取得など、1 回限りの準備タスクをアプリで実行する場合は、ここで実行します。このメソッドをアダプターに実装する必要はありません。
  • onLayout() - ページサイズやページの向きの変更など、出力に影響する印刷設定をユーザーが変更するたびに呼び出され、印刷対象ページのレイアウトをアプリが計算できるようになります。このメソッドは、少なくとも印刷ドキュメントで想定されるページ数を返す必要があります。
  • onWrite() - 印刷されたページを印刷対象のファイルにレンダリングするために呼び出されます。このメソッドは、onLayout() が呼び出されるたびに 1 回以上呼び出すことができます。
  • onFinish() - 印刷プロセスの終了時に 1 回呼び出されます。アプリに実行する 1 回限りの破棄タスクがある場合は、ここで実行します。アダプターでこのメソッドを実装する必要はありません。

以降のセクションでは、印刷アダプターの機能に不可欠な layout メソッドと write メソッドの実装方法について説明します。

注: これらのアダプター メソッドは、アプリのメインスレッドで呼び出されます。実装でのこれらのメソッドの実行にかなりの時間がかかることが予想される場合は、別のスレッド内で実行するように実装してください。たとえば、レイアウトや印刷ドキュメントの書き込み作業を別々の 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() メソッドの実行結果には、完了、キャンセル、レイアウトの計算を完了できない場合の失敗の 3 つがあります。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 の印刷フレームワークでは、onLayout() の呼び出しごとに onWrite() メソッドを 1 回以上呼び出すことがあります。このため、印刷コンテンツのレイアウトが変更されていない場合には、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() メソッドの実行結果には、完了、キャンセル、コンテンツを書き込めない場合の失敗の 3 つがあります。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 ドキュメントの端に印刷要素を配置できますが、多くのプリンタでは物理的な用紙の端に印刷できません。このクラスを使用して印刷ドキュメントを作成する場合は、ページの印刷できない端を考慮してください。