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

描画アプリ、ページ レイアウト アプリなど、グラフィック出力にフォーカスした一部のアプリでは、美しい印刷ページを作成する機能が重要です。この場合、画像または 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 回限りの破棄タスクがアプリにある場合は、ここで実行します。アダプターでこのメソッドを実装する必要はありません。

次のセクションでは、レイアウト メソッドおよび書き込みメソッドを実装する方法について説明します。これらのメソッドは印刷アダプターの機能には非常に重要です。

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