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を作ったのでそちらも合わせて解説&公開したいと思います。

1 件のコメント:

  1. Very good information.Is this print framework application will support in older versions of android?

    返信削除