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