2013年11月17日日曜日

Android 4.4 詳解 Printing Framework 後編

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 1週間ってアッという間ですね。「Android 4.4 KitKat 冬コミ原稿リレーを開催」の11/9担当のsys1yagiです。

 Printing Frameworkの後編という事で、PrintServiceについて解説します。印刷の出力部分を実装する話は「Android 4.4 詳解 Printing Framework 前編」を参照して下さい。

 サンプルコードとしてDropboxにPDFをアップロードするDropboxPrintServiceを作りました。ソースはコチラ→DropboxPrintService

PrintServiceとは


 PrintServiceはプリンタの検出と、印刷ジョブの処理を行うサービスです。ネットワークプリンタのドライバの様な役割をする他、PDFを任意の場所に出力するサービスとしても実装できます。

PrintServiceの追加や設定


 PrintServiceの実装を含んだアプリケーションをインストールすると、SettingsのPrinting画面にサービスが追加されます(図01)。ユーザはそれぞれのPrintServiceのOn/Offを切り替えたり、各PrintServiceが提供する設定画面を起動したりできます。


図01 [PrintServiceの詳細画面]

PrintServiceの実行フロー


 PrintServiceはシステムが管理するPrintManagerと連携しながら動作します。PrintServiceの実行フローを[図02]に示します。



[図02 PrintServiceの実行フロー]

  1. PrintManagerが印刷ダイアログを表示する
  2. システムからプリンタの検出セッションの開始を要求される(onCreatePrinterDiscoverySessionメソッドの呼び出し)
  3. PrinterDiscoverySessionクラスのインスタンスを返却する
  4. PrinterDiscoverySessionクラス内でプリンタの追加、削除、更新を行う
  5. ユーザがプリンタの選択、設定を行い、印刷を開始する
  6. onPrintJobQueuedメソッドにPrintJobが渡される
  7. PrintJobの情報を元に印刷処理を行う
 これらのフローのうち、3,4,5の処理をPrintService側で実装することになります。

PrintServiceを作る


 では早速PrintServiceを実装してみましょう。PrintServiceはServiceを継承した抽象クラスです。[リスト01]に示す通り3つの抽象メソッドを持っています。

リスト01 [PrintServiceの持つ抽象メソッド]

public class SamplePrintService extends PrintService {

  @Override
  protected PrinterDiscoverySession onCreatePrinterDiscoverySession() {
    // プリンタの検索セッション用オブジェクトを返す
    return null;
  }

  @Override
  protected void onPrintJobQueued(PrintJob paramPrintJob) {
    // 印刷処理をする
  }

  @Override
  protected void onRequestCancelPrintJob(PrintJob paramPrintJob) {
    // キャンセルリクエスト
  }

}

onCreatePrinterDiscoverySession()


 システムがプリンタの検出を開始したい時、PrintServiceのonCreatePrinterDiscoverySessionメソッドが呼び出されます。この時PrintService側はPrinterDiscoverySessionのインスタンスを返却する必要があります。

 PrinterDiscoverySessionはプリンタの検出をする間、システムとPrintService間のやりとりをカプセル化するクラスです。PrinterDiscoverySessionクラスの各コールバックメソッドが呼び出されるので、プリンタの追加や更新、削除を行います。

 PrinterDiscoverySessionクラスは沢山の抽象メソッドを持っています。[表01]にPrinterDiscoverySessionクラスの抽象メソッドの解説を示します。

表01 [PrinterDiscoverySessionの抽象メソッド]

項目 解説
void onStartPrinterDiscovery(List<PrinterId> printers) プリンタの検出を開始します。printersには検出済みのプリンタ情報が入っています。printersは補助的な情報です。このセッションで検出するプリンタとは関係ないので、printersに含まれるからといってプリンタ情報を追加しなくてよいわけではありません。プリンタの追加、更新はaddPrintersメソッドで行います。削除はremovePrintersメソッドで行います。
void onStopPrinterDiscovery() プリンタ検出を停止します
void onStartPrinterStateTracking(PrinterId printerId) プリンタの状態の追跡を開始するように求めるコールバックです。印刷ダイアログでプリンタが選択された時に呼び出されます。プリンタの状態が変化した時はすみやかにシステムに通知しなければなりません
void onStopPrinterStateTracking(PrinterId printerId) プリンタの状態の追跡が終了した事を通知するコールバックです。
void onValidatePrinters(List<PrinterId> printerIds) 渡されたPrinterIdのリストが有効であるかの検証を求めるコールバックです。有効な場合はaddPrintersメソッドでプリンタ情報を更新する必要があります
void onDestroy() システムからこのセッションが不要であると判断された時に呼び出されます


 [リスト02]にPrinterDiscoverySessionの実装例を示します。この例は実際にDropboxPrintServiceで利用しています。onStartPrinterDiscoveryメソッドでプリンタを追加している以外は何もしていません。

 プリンタ情報はPrinterInfo.Builderを使って作成します。印刷可能な用紙サイズや、色、解像度などの設定を行えます。addPrintersメソッドでPrinterInfoのリストを渡せば、印刷ダイアログ上にプリンタ情報が表示されます。[リスト02]ではonStartPrinterDiscoveryメソッド内でPrinterInfoを作成し、addPrintersメソッドを呼び出していますが、通常はスレッドを起動してプリンタの検出を行い、Handler等を使ってプリンタ情報を追加する事になるでしょう。

リスト02 [PrinterDiscoverySessionの実装例]

public class DropboxPrinterDiscoverySession
  extends PrinterDiscoverySession {

  private final static String TAG = 
    DropboxPrinterDiscoverySession.class.getSimpleName();
  private final static String PRINTER_ID = "dropbox.print.service";

  private PrintService mPrintService;

  public DropboxPrinterDiscoverySession(PrintService printServie){
    mPrintService = printServie;
  }

  @Override
  public void onStartPrinterDiscovery(List<PrinterId> printers) {
    Log.d(TAG, "onStartPrinterDiscovery()");
    for (PrinterId id : printers) {
      Log.d(TAG, "printerId:" + id.getLocalId());
    }
    List<PrinterInfo> addPrinters = new ArrayList<PrinterInfo>();
    PrinterId printerId = mPrintService.generatePrinterId(PRINTER_ID);

    PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId,
        "Dropbox Printer", PrinterInfo.STATUS_IDLE);

    PrinterCapabilitiesInfo.Builder capBuilder =
      new PrinterCapabilitiesInfo.Builder(printerId);
    capBuilder.addMediaSize(PrintAttributes.MediaSize.ISO_A4, true);
    capBuilder.addMediaSize(PrintAttributes.MediaSize.ISO_B5, false);
    capBuilder.addResolution(new PrintAttributes.Resolution(
        "Default", "default resolution", 600, 600), true);
    capBuilder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
        | PrintAttributes.COLOR_MODE_MONOCHROME,
        PrintAttributes.COLOR_MODE_COLOR);

    builder.setCapabilities(capBuilder.build());
    addPrinters.add(builder.build());
    addPrinters(addPrinters);
  }

  @Override
  public void onStopPrinterDiscovery() {
    Log.d(TAG, "onStopPrinterDiscovery()");
  }

  @Override
  public void onValidatePrinters(List<PrinterId> printerIds) {
    Log.d(TAG, "onValidatePrinters()");
    for (PrinterId id : printerIds) {
      Log.d(TAG, "printerId:" + id.getLocalId());
    }
  }

  @Override
  public void onStartPrinterStateTracking(PrinterId printerId) {
    Log.d(TAG,
        "onStartPrinterStateTracking(printerId: "
            + printerId.getLocalId() + ")");
  }

  @Override
  public void onStopPrinterStateTracking(PrinterId printerId) {
    Log.d(TAG,
        "onStopPrinterStateTracking(printerId: "
            + printerId.getLocalId() + ")");
  }

  @Override
  public void onDestroy() {
    Log.d(TAG, "onDestroy()");
  }
}

onPrintJobQueued(PrintJob paramPrintJob)


 検出したプリンタに対して、ユーザが印刷を開始すると、onPrintJobQueuedメソッドにPrintJobが渡されます。PrintJobにはプリンタIDの他、印刷サイズや色情報、PDFファイルへのFileDescriptorなどが含まれています。印刷データの取り出しはFileDescriptorを使って行います。

 PrintJobは開始、完了、失敗、キャンセルなどのステータスを持っています。印刷の状態に応じてセットする必要があります。

 [リスト03]はPrintJobからPDFを取り出して保存する例です。onPrintJobQueuedメソッド上で全ての処理を行なっていますが、通常はAsyncTask等を使う事になるでしょう。ただし、PrintJobの操作はUIスレッド上で行わなければなりません。PDFデータのFileDescriptorも同様です。PDFデータをネットワーク上に送信する場合は、一時ファイル等を作成した上でAsyncTaskを利用する事になります。

リスト03 [PDFを保存する]

@Override
protected void onPrintJobQueued(final PrintJob printJob) {

  printJob.start();

  File tmp = new File(getExternalCacheDir(), printJob.getDocument()
      .getInfo().getName());

  FileOutputStream fos = null;
  FileInputStream fin = null;
  try {
    fos = new FileOutputStream(tmp);
    fin = new FileInputStream(printJob.getDocument()
        .getData().getFileDescriptor());
    int c;
    byte[] bytes = new byte[1024];

    while ((c = fin.read(bytes)) != -1) {
      fos.write(bytes, 0, c);
    }
    fos.close();
    fin.close();
  } catch (Exception e) {
    e.printStackTrace();
    printJob.fail(e.getMessage());
    return;
  } finally {
    if(fos != null){try{fos.close();}catch(Exception e){}}
    if(fin != null){try{fin.close();}catch(Exception e){}}
  }

  printJob.complete();

}

 PrintJobはPrintService内で自動的にキューイングされているので、改めて自分でキューのハンドリングをする必要はありません。アクティブなPrintJobはgetActivePrintJobsメソッドで取り出せます。

onRequestCancelPrintJob(PrintJob paramPrintJob)


 onRequestCancelPrintJobメソッドはユーザが印刷をキャンセルした時に呼び出されます。PrintJobが渡されるので、対応する処理を停止する必要があります。PrintJobクラスのisCancelledメソッドで確認する事もできます(リスト04)。

リスト04 [isCancelledメソッドを使う例]

protected void onPostExecute(PrintStatus result) {
    if(printJob.isCancelled()) {
        toast("キャンセルされました");
    } else if (result.isError()) {
        printJob.fail(result.getMessage());
    } else {
        printJob.complete();
    }
    toast(result.getMessage());
};

AndroidManifest.xmlに追加する


 作成したPrintServiceをAndroidManifest.xmlに定義する事で、 アプリケーションのインストール時にPrintServiceをシステムに追加できます(リスト05)。

リスト05 [AndroidManifest.xmlにPrintServiceを追加する]

<service
  android:name="app.package.name..YourPrintService"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
  <intent-filter>
    <action android:name="andraoid.printservice.PrintService" />
  </intent-filter>
</service>

設定画面を作る


 多くの場合、ネットワーク越しのプリンタに接続する為の設定がPrintServiceに必要となるでしょう。AndroidManifest.xmlでPrintServiceを定義する際にメタデータの設定を行えます。

 [リスト06]はPrintServiceで使う設定画面の定義例です。設定用のActivityを定義しています。XMLファイルは"res/xml"配下に作
成してください。

リスト06 [res/xml/your_printservice.xml]

<print-service
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:description="@string/description"
  android:settingsActivity="app.package.name.YourSettingActivity" />

 作成したXMLファイルをAndroidManifest.xmlのメタデータで指定します(リスト07)。これでPrintServiceの詳細画面から設定画面へ遷移できます。設定画面のActivityも忘れずAndroidManifest.xmlに定義しましょう。

リスト07 [AndroidManifest.xmlの設定]

<service
  android:name="app.package.name.YourPrintService"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
  <intent-filter>
    <action android:name="andraoid.printservice.PrintService" />
  </intent-filter>

  <meta-data
    android:name="android.printservice"
    android:resource="@xml/your_printservice" >
  </meta-data>
</service>

<activity
  android:name="app.package.name.YourSettingActivity"
  android:exported="true"
  android:label="@string/title_activity_setting"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
</activity>

DropboxPrintServiceを作りました


 本エントリを書くに当たり、調査、習作としてDropboxにPDFを送信するPrintServiceを作成しました。使い方としては以下の通りです。

  • ソースはコチラ→DropboxPrintService
    • ソースの方にはAPIキーは含めていませんので、ソースからビルドして使う場合はDropboxでAPIキーを取得して下さい。
  • apkはコチラ→DropboxPrintService.apk


Dropboxにログインする


 DropboxPrintServiceをインストールしたら、まずはDropboxにログインする必要があります。SettingsのPrinting画面でDropboxPrintServiceを選択し、詳細画面を開いてDropboxPrintServiceをONにして下さい。下部のメニューから設定画面を開けるのでDropboxにログインして下さい。



[図03 Dropboxにログインする]

印刷する


 あとは普通に印刷するだけです。



[図04 Dropbox Printerを使う]

 PDFがDropboxにアップロードされます。



[図05 DropboxにアップロードされたPDF]

おわりに


 本エントリではPrintServiceの役割とざっくりした実装を解説しました。プリンタの追加、削除や、プリントジョブのキャンセル等をしっかり作りこむには色々と考慮点が多そうですが、基本的な事は本エントリの情報で実現できると思います。

 「印刷先がプリンタである必要はない」という点はとてもおもしろく、アイデア次第で便利なサービスを作れる気がします。Kitkatが普及するのはまだ先ですが、今後面白いPrintServiceが登場する事を期待しています。

2013年11月10日日曜日

Android 4.4 詳解 Printing Framework 前編

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 俺がPrinting Frameworkだ!という事で、「Android 4.4 KitKat 冬コミ原稿リレーを開催」の11/9担当のsys1yagiです。

 本記事では、Android 4.4で追加されたPrinting Frameworkで出来ることや、アプリケーションでPrinting Frameworkを使う方法などを解説します。内容はPrinting Contentをベースにしています。のちほどPrinting Contentの日本語訳も公開する予定です。

 今回の解説に登場するコードはGithubで公開しています。→PrintingFrameworkSample

 エミュレータとGalaxy Nexusに焼いたAndroid 4.4で動作確認しています。N5マジほしい。

 Printing Frameworkの概要


 Printing Frameworkは、Android端末からWi-FiやBluetoothやその他のサービスを介して接続したプリンタを使って、文書の印刷を可能にするフレームワークです。Kitkatを搭載した端末で以下の事が実現出来るようになります。

  • アプリケーションからPDFを出力して印刷する
  • PrintServiceを作って、プリントジョブを処理出来る

 注目すべきは「PDFを出力する」という点でしょう。アプリケーションでCanvasを使ってPDFのレンダリングが可能になります。

 PrintServiceは、アプリケーションが発行したプリントジョブを受け取って印刷処理を行うサービスです。PrintServiceを実装する事で、仮想のプリンタを作成したり、プリンタドライバとして振る舞ったりできます。標準ではStorage access framework(これもKitkatで追加されたAPIです)を通してPDFファイルをローカルストレージに保存するサービスが搭載されています。

 Printing Frameworkによる印刷処理の構成を以下に示します。


図01 [Printing Frameworkによる印刷処理の構成]

 印刷を行うには、アプリケーションでPrintDocumentAdapterを用意し、PrintManagerに対して印刷のリクエストを行います。PrintManagerは受け取ったPrintDocumentAdapterでPDFの作成を行い、印刷オプションのダイアログを表示します。印刷オプションを選択して印刷を実行すると、PrintJobがPrintServiceにキューイングされ、印刷処理が開始されます。これらのフローのうち、PDFを生成するPrintDocumentAdapterと、印刷処理を行うPrintServiceを任意に開発できます。

アプリケーションから印刷する


 アプリケーションからPrinting Frameworkを使って印刷をしてみましょう。Printing Frameworkは以下の3種類のフォーマットをサポートしています。それぞれのフォーマットの印刷方法を解説します。

  • 画像
  • HTML
  • カスタム

画像を印刷する


 Printing Frameworkの追加に伴って、support-v4にPrintHelperという画像の印刷処理を補助するクラスが追加されています(Support Libraryのrevision 19を参照)。PrintHelperクラスを使えば画像を印刷するためのPrintJobの作成や、PrintManagerの呼び出しを全て委譲でき、簡単に印刷処理を実現できます。

 PrintHelperではスケールモードとカラーモードの印刷オプションを設定できます。それぞれ2種類ずつオプションを持ちます。

表01 [PrintHelperのスケールモード]

項目 説明
SCALE_MODE_FIT ページの印刷領域に画像全体を表示するように納めます
SCALE_MODE_FILL ページの印刷領域の全体に画像をスケールします。この設定を選択すると、画像の上部と下部、または左右のエッジの一部が印刷されないことを意味します。このオプションがデフォルトとなります

表02 [PrintHelperのカラーモード]

項目 説明
COLOR_MODE_COLOR カラーで印刷します。このオプションがデフォルトとなります
COLOR_MODE_MONOCHROME グレースケールで印刷します


 以下のコードは、PrintHelperクラスを使って画像を印刷する例です。

リスト01 [PrintHelperで画像を印刷する]

private void printImage(String fileName, Bitmap bitmap) {
  if (PrintHelper.systemSupportsPrint()) {

    PrintHelper printHelper = new PrintHelper(context);
    printHelper.setColorMode(PrintHelper.COLOR_MODE_COLOR);
    printHelper.setScaleMode(PrintHelper.SCALE_MODE_FIT);
    printHelper.printBitmap(fileName, bitmap);

  } else {
    Toast.makeText(this, 
      "この端末では印刷をサポートしていません", 
      Toast.LENGTH_SHORT).show();
  }
}

 PrintHelperクラスのsystemSupportsPrintメソッドで端末が印刷をサポートしているか確認できます(と言っても内部ではBuild.VERSION.SDK_INT >= 19の結果を返却しているだけです)。printBitmapメソッドでファイル名とBitmapを渡したあとは何もする必要はありません。PrintJobが作成され、PrintManagerに対してリクエストが送られます。



図02 [印刷ダイアログの起動]

 PrintManagerに対してリクエストを送ると、図02の様に印刷ダイアログが起動されます。印刷ダイアログではプリンタの選択やコピー数、ページサイズ、カラー、印刷の向きなどの設定が行えます。



図03 [PDFを保存する]

 図03は「Save as PDF」でPDFを保存する例です。印刷を実行すると、保存先を選択する画面が起動されるます。ここではDownloadsにPDFを保存しています。



図04 [保存したPDFを開く]

 保存したPDFを開くと無事画像が出力されている事を確認できます。

HTMLを印刷する


 画像の印刷ではPrintHelperクラスを利用しましたが、画像以外を印刷する場合は自分でPrintManagerクラスに印刷のリクエストを送る必要があります。

 PrintManagerクラスに印刷リクエストを送るには、PDFのレンダリングを行うPrintDocumentAdapterクラスが必要となります。API Level 19でWebViewに追加されたcreatePrintDocumentAdapterメソッドを使うと、HTMLをレンダリングするPrintDocumentAdapterクラスを得られます。

 以下のコードはWebViewのcreatePrintDocumentAdapterメソッドでPrintDocumentAdapterクラスを取得し、PrintManagerクラスに印刷のリクエストを送る例です。

リスト02 [WebViewを使って印刷する]

private void printHtml(String fileName, WebView webView) {
  if (PrintHelper.systemSupportsPrint()) {

    PrintDocumentAdapter adapter = webView.createPrintDocumentAdapter();
    PrintManager printManager = 
      (PrintManager) getSystemService(Context.PRINT_SERVICE);
    printManager.print(fileName, adapter, null);

  } else {
    Toast.makeText(this,
        "この端末では印刷をサポートしていません",
        Toast.LENGTH_SHORT).show();
  }
}

 WebViewに予めWebページを表示しておき、上記コードを実行すると、以下の様に印刷が開始されます。



図05 [WebViewの内容を印刷する]

 WebページのPDFを保存できます。改ページもきちんとされています。



図06 [Webページを保存したPDF]

カスタムドキュメントを印刷する


 カスタムドキュメントを印刷する場合はPrintDocumentAdapterクラスを自分で実装します。PrintDocumentAdapterクラスは2つの抽象メソッドを持つ抽象クラスです。以下の様にサブクラスで抽象メソッドを実装する必要があります。

リスト03 [PrintDocumentAdapterクラスを継承する]

public class CustomDocumentPrintAdapter
  extends PrintDocumentAdapter {    

  @Override
  public void onLayout(PrintAttributes oldAttributes,
      PrintAttributes newAttributes,
      CancellationSignal cancellationSignal,
      LayoutResultCallback callback, Bundle extras) {

      //印刷オプション変更時に呼び出される。

  }

@Override
  public void onWrite(PageRange[] pages,
      ParcelFileDescriptor destination,
      CancellationSignal cancellationSignal,
      WriteResultCallback callback) {

      //PDF生成の処理をする

  };

 onLayoutメソッドは印刷ダイアログで、ページサイズや色などの印刷オプションを変更した時に呼び出されるメソッドです。LayoutResultCallbackクラスを使ってレイアウト変更に対するコールバックを行います。LayoutResultCallbackクラスのメソッドを以下に示します。

表03 [LayoutResultCallbackクラスのメソッド]

項目 説明
void onLayoutCancelled() ユーザあるいはアプリケーションからキャンセルを受けた事を通知します。キャンセルの状態はCancellationSignalクラスのisCanceledメソッドで確認できます
void onLayoutFailed(CharSequence error) レイアウト変更に対応できない場合にエラーを通知できます
void onLayoutFinished(PrintDocumentInfo info, boolean changed) レイアウト変更が完了した事を通知します。印刷情報を含んだPrintDocumentInfoクラスを作成して渡す必要があります。


 onWriteメソッドはページのレンダリング時に呼び出されます。PageRangeクラスは印刷する範囲が指定されています。PageRangeクラスの範囲に従ってParcelFileDescriptorクラスに対してPDFを書き出します。レンダリングの結果はWriteResultCallbackクラスを使って通知します。WriteResultCallbackクラスのメソッドを以下に示します。

表04 [WriteResultCallbackクラスのメソッド]

項目 説明
void onWriteCancelled() ユーザあるいはアプリケーションからキャンセルを受けた事を通知します。キャンセルの状態はCancellationSignalクラスのisCanceledメソッドで確認できます
void onWriteFailed(CharSequence error) PDFのレンダリングに失敗した場合にエラーを通知できます
void onWriteFinished(PageRange[] pages) PDFのレンダリングが完了した事を通知します


 以下のコードはPrintDocumentAdapterのサブクラスの実装例です。画像、タイトル、本文をコンストラクタで受け取り、印刷します。

リスト04 [CustomDocumentPrintAdapterの実装例]

public class CustomDocumentPrintAdapter extends PrintDocumentAdapter {

  private Paint mPaint = new Paint();
  private Context mContext;
  private Bitmap mBitmap;
  private String mTitle;
  private String mMessage;

  PrintedPdfDocument mPdfDocument;

  public CustomDocumentPrintAdapter(Context context, Bitmap bitmap,
    String title, String message) {
    mContext = context;
    mBitmap = bitmap;
    mTitle = title;
    mMessage = message;
  }

  @Override
  public void onLayout(PrintAttributes oldAttributes,
      PrintAttributes newAttributes,
      CancellationSignal cancellationSignal,
      LayoutResultCallback callback, Bundle extras) {

    mPdfDocument = new PrintedPdfDocument(mContext, newAttributes);

    if (cancellationSignal.isCanceled()) {
      callback.onLayoutCancelled();
      return;
    }
    int pages = 1;
    //newAttributes.getColorMode();
    //newAttributes.getMediaSize().getHeightMils();  //単位は1/1000インチ
    //newAttributes.getMediaSize().getWidthMils();   //単位は1/1000インチ

    PrintDocumentInfo info = new PrintDocumentInfo.Builder("androids.pdf")
        .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
        .setPageCount(pages).build();
    callback.onLayoutFinished(info, true);
  }

 PDFのレンダリングはPrintedPdfDocumentクラスを使います。onLayoutメソッドのタイミングでPrintedPdfDocumentクラスを初期化しています。本来は印刷サイズに応じてページ数を計算するべきですが、ここでは割愛して1ページで固定しています。PrintDocumentInfoクラスを作成し、LayoutResultCallbackクラスのonLayoutFinishedメソッドに渡しています。

 次にonWriteメソッドを実装します。以下のコードではPrintedPdfDocumentクラスのstartPageメソッドでPdfDocument.Pageクラスを作成し、ページのレンダリングを行なっています。PdfDocument.PageクラスのgetCanvasメソッドでCanvasクラスを取得して任意にレンダリング処理ができます。PDFのレンダリングが完了したらParcelFileDescriptorに対してParcelFileDescriptorのwriteToメソッドでPDFの書き込みを行います。最後にWriteResultCallbackクラスのonWriteFinishedメソッドを呼び出せばレンダリングが完了します。

リスト05 [onWriteメソッドを実装する]

@Override
  public void onWrite(PageRange[] pages,
      ParcelFileDescriptor destination,
      CancellationSignal cancellationSignal, WriteResultCallback callback) {
    if (mPdfDocument == null) {
      return;
    }

    PdfDocument.Page page = mPdfDocument.startPage(0);
    if (cancellationSignal.isCanceled()) {
      callback.onWriteCancelled();
      mPdfDocument.close();
      mPdfDocument = null;
      return;
    }
    onDraw(page.getCanvas());
    mPdfDocument.finishPage(page);

    try {
      mPdfDocument.writeTo(new FileOutputStream(destination
          .getFileDescriptor()));
    } catch (IOException e) {
      callback.onWriteFailed(e.toString());
      return;
    } finally {
      mPdfDocument.close();
      mPdfDocument = null;
    }
    callback.onWriteFinished(pages);
  };

  public void onDraw(Canvas canvas) {
    //レンダリング処理
    //72で1インチとなる    
    //省略
  }
}


 作成したCustomDocumentPrintAdapterクラスを使ってみましょう。CustomDocumentPrintAdapterクラスを自分でインスタンス化する点以外はHTMLの印刷と変わりません。

リスト06 [CustomDocumentPrintAdapterを使う]

private void doPrint(){
  CustomDocumentPrintAdapter adaper = 
    new CustomDocumentPrintAdapter(context,
      BitmapFactory.decodeResource(getResources(),
      R.drawable.androids), "Hello Kitkat!!", getContext()
      .getString(R.string.long_message));

  printWithAdapter("custom.pdf", adapter);
}

private void printWithAdapter(String jobName,
  PrintDocumentAdapter adapter) {

  if (PrintHelper.systemSupportsPrint()) {

    PrintManager printManager = 
      (PrintManager) getSystemService(Context.PRINT_SERVICE);
    printManager.print(jobName, adapter, null);

  } else {
    Toast.makeText(this,
      "この端末では印刷をサポートしていません",
      Toast.LENGTH_SHORT).show();
  }
}

 PrintDocumentAdapterはCanvasを使ってPDFのレンダリングを行うので、インタフェースを工夫すればViewで利用する事ができます。ただしViewのCanvasとPDFのCanvasではスケールが異なるので注意して下さい。



図07 [PrintDocumentAdapterをViewで使う]

 印刷結果は以下の通りとなります。若干文字の大きさやレンダリング位置が異なります。



図08 [CustomDocumentPrintAdapterでレンダリングしたPDF]

おわりに


 PDFをレンダリング出来るようになったのはとてもエキサイティングですね。ただPDFの読み込みはサポートしていないので、エミュレータなどPDFを表示するアプリが入っていない環境でSace as PDFすると中身が確認できない点がつらいです。

 後編ではPrintServiceの仕組みと実装方法を解説します。DropboxにPDFを出力するDropboxPrintServiceを作ったのでそちらも合わせて解説&公開したいと思います。