描画アプリ、ページ レイアウト アプリなど、グラフィック出力にフォーカスした一部のアプリでは、美しい印刷ページを作成する機能が重要です。この場合、画像または 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 ドキュメントの端に印刷要素を配置できますが、プリンタの多くは用紙の端にある要素を物理的に印刷できません。このクラスを使用して印刷ドキュメントを作成するときは、ページの印刷できない端の部分を考慮してください。