你有沒有想過給你的Android應用新增列印的功能?
在Android4.4之前,Android上沒有專門為列印提供的API,如果要實現列印的話,只能依賴第三方的解決方案,或者自己實現SMB/CIFS這樣的協議,用WIFI、藍芽或者USB直接和印表機通訊。而我們絕大多數人也不會想去直接和印表機通訊,一般都是依靠已經存在的方案,這也是最好的選擇了。其實,這就是Android新增的功能,可以讓列印功能更加簡單和可靠。
雲列印技術
Google Cloud Print Service最先是在2010年4月對外發布的,GCP是一個基於web的印表機,也是一個可以提供列印功能的管理系統,它能讓任何聯網裝置都可以使用GCP服務來連結任何一臺印表機。使用者可以為GCP服務新增任何一臺印表機,設定是很老式印表機、非雲端連線的印表機也是可以的,只需要保證印表機與連線的電腦在web上是共享的,並且裝有Google的Chrome。據作者所說,使用GCP是讓Android4.4以下的系統支援列印功能的最簡單方式。GCP也是我們最希望在Android4.4以上裝置上見到的新功能,所以可以開始學習了。點選這裡檢視資料。
KitKat之前的列印技術
Google雲列印技術給開發者提供了一系列API,只要開發者擁有Google認證賬號就可以提交列印任務和接收列印任務了。可以從這裡下載Google雲列印相關的開發資料https://developers.google.com/cloud-print/。本文不會涉及任何實現的細節,因為它有點長,而且幫助不大。還有一點是,使用者必須正確設定GCP賬號,並和印表機繫結,下面將要介紹兩種實現方法。
Google雲列印APP
Paulo Fernandes是第一個在APP中使用雲列印服務的人,它的Cloud Print應用是第一款使用GCP API來為Android裝置提供列印服務的APP。兩年後,也就是2013年六月的時候,Google退出了它自己的版本,叫做Google Cloud Print。我會用Google Cloud Print作為例子進行講解,因為它更流行一些,不過我自己也覺得大多數雲列印類APP都差不多。首先,我們需要使用PackageManager這個類來保證Android裝置上已經執行了Google Cloud Print服務,可以使用這個函式:
1 2 3 4 5 6 7 8 9 10 |
; html-script: false ] private boolean hasGoogleCloudPrint() { PackageManager pm = getPackageManager(); try { pm.getPackageInfo(“com.google.Android.apps.cloudprint”, 0); return true; } catch(PackageManager.NameNotFoundException e) { return false; } } |
如果裝置上裝了Google Cloud Print服務的話,我們就可以使用一個Intent來開始列印了:
1 2 3 4 5 6 7 8 9 10 |
; html-script: false ] private void printViaGoogleCloudPrintApp(Uri content) { Intent printIntent = new Intent(Intent.ACTION_SEND); printIntent.setPackage(“com.google.Android.apps.cloudprint”); printIntent.setType(“image/*”); printIntent.putExtra(Intent.EXTRA_TITLE, “Print Test Title”); printIntent.putExtra(Intent.EXTRA_STREAM, uri); startActivity(printIntent); } |
或者,還可以使用webview呼叫Google Cloud Print的web介面,這裡省略了這部分,這裡有一個例子,感興趣的可以看看完整示例.
在KitKat系統中進行列印
有了這個列印的API,Android上列印就變得更簡單、更可靠了。這是它的API的更新列表,以包的形式組織:
Android.support.v4.print
- PrintHelper – bitmap列印佇列工具類。
Android.print
- PrintDocumentAdapter – 提供自定義列印文件的基礎類。
- PrintManager – 訪問列印佇列,並提供PrintDocumentAdapter類支援。
Android.print.pdf
- PrintedPdfDocument – 基於特定PrintAttributeshelper建立PDF。
Android.webkit
- WebView.createPrintDocumentAdapter – 為WebView列印內容建立PrintDocumentAdapter。
Android.printservice
- 實現自定義PrintService的容器類。
我們先來看看Android.printservice,這個包裡面有與實現自己的列印服務相關的類,Print Service抽象出了與真實印表機(或者其它中間部件如GCP)之間的通訊,Google Cloud Print APP就在KitKat上實現了這個服務,所以就可以在GCP上建立列印任務了。關於Print Service的具體實現超出了本文的範圍,不過可以關注一下PrintService,它還是挺實用的,它是實現列印所必須的一個類,還有其它需要的相關類稍後就介紹。(需要注意的是:大多數KitKat裝置中預裝了Google Cloud Print,Chrome,Drive,Gallery還有Quickoffice的,都會提供列印的服務)
接下來要說的是PrintHelper,它也在Android.support.v4.print包下,你可能會想,既然這個類放在Android的相容包裡,那麼就可以在老版本的Android上使用列印的功能了。但是很不幸的是,事實不是這樣的,PrintHelper這個類有一個靜態方法systemSupportsPrint(),從本人進行的測試來看,這個方法只有在KitKat裝置中才會返回true。如果在systemSupportPrint()這個方法返回false的裝置上呼叫printBitmap()方法的話,會直接被忽略,不會發生任何事件。現在我們只需要瞭解為什麼PrintHelper這個類會包含在Android的相容包裡面,好像也沒有什麼用,至少目前還沒有什麼用處。
不管怎麼樣,PrintHelper這個類在KitKat中進行列印操作還是很實用的,它提供了簡單,直接的方式來列印圖片。
- 可以指定圖片的縮放模式,FILT或者FIT模式,FILT模式會用給出的圖片填充整個區域,並保持整體比例不變,可能會有裁剪。FIT模式會水平填充或者垂直填充,這取決於文件或者圖片的尺寸,它也會保持整體的比例,但是不會裁剪,所以可能會出現空白區域,除非圖片的比例和要列印的文件的比例正好合適。
- 可以指定顏色模式,COLOR或者MONOCHROME。
還有一個很實用的新增方法叫createPrintDocumentAdapter(),為webview準備的。這個方法會和PrintManager一起使用,讓列印webview的內容更加簡單。
1 2 3 4 |
; html-script: false ] PrintDocumentAdapter printDocumentAdapter = mWebView.createPrintDocumentAdapter(); PrintManager printManger = (PrintManager) getSystemService(Context.PRINT_SERVICE); printManager.print(“Test Print Job”, printDocumentAdapter, null); |
在列印webview的內容時,還需要注意的是:
- 使用者不能指定列印的頁面範圍,所以只能列印整個文件。
- PrintDocumentAdapter在被Webview建立的時候,只能提供當前載入的內容,如果文件沒載入完畢,或者中途又載入了別的文件,那麼可能會導致不可預料的結果,或者PrintDocumentAdapter會載入失敗。
- 一個webview例項一次只能處理一個列印任務。
PrintManager這個類只有兩個公開的方法:getPrintJobs()和print()方法。getPrintJobs()方法會返回一個PrintJob型別的列表,這些PrintJob物件由應用呼叫。PrintJob物件在檢測狀態或者在取消或者重啟列印任務的時候很有用。在列印的時候會建立一個PrintJob物件(帶有標題,PrintDocumentAdapter和PrintAttributes),其中PrintAttributes可以讓你指定一種顏色模式,媒體尺寸,邊距還有解析度,還有要特別注意這些屬性的操作都是由PrintService操作的,使用者只是把任務提交而已。而且很有可能很多屬性與對應的服務是沒有關係的,或者是會被直接忽略掉的。一個簡單的例子就是在選擇“save as PDF”的時候,就會忽略掉顏色模式這個屬性。
最後要看的這個類叫PrintDocumentAdapter。這個類是一個基礎類,繼承它可以建立自定義的佈局和內容。儘管是可以用PrintHelper和Bitmap類來輕鬆對繪製的內容進行列印,但是PrintDocumentAdapter在佈局和屬性上更加靈活,在實現PrintDocumentAdapter的時候,有兩個可選的方法和兩個必須的方法,可選的方法是onStart()和onFinish(),在分配和釋放資源的時候會需要這兩個方法。首先要看的一個必須的方法叫onLayout(),在PrintAttribute改變的時候就會呼叫這個方法,這個方法的主要目的就是為了建立PrintDocumentInfo物件,用於描述所列印的內容,和呼叫LayoutResultCallback物件裡的一個方法(有三個方法)。這也是建立PrintedPdfDocument物件最好的地方,所以就可以往裡面傳遞相應的列印屬性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
; html-script: false ] @Override public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) { // If the CancellationSignal indicates the print job has been cancelled then call // onLayoutCancelled and return as there is nothing else to do if (cancellationSignal.isCanceled()) { callback.onLayoutCanceled(); return; } // If you are using PrintedPdfDocument helper class this is a good place to instantiate it mPdfDocument = new PrintedPdfDocument(getContext(), newAttributes); // Next you will want to determine the number of pages your document will require based // on the specified attributes and your content, whatever that may be int pageCount = determinePageCount(newAttributes); // Finally you will need to create a PrintDocumentInfo object which specifies the content // type and page count. The content type value can either by DOCUMENT or PHOTO // and will potentially be used by the print service but could also be ignored depending on // what print service is being used. The Android developer docs mention that the print // service may use the document type to determine the paper quality and/or other quality // settings though again, it is entirely up to the print service how this value is used. PrintDocumentInfo info = new PrintDocumentInfo.Builder(“Document Title”) .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT) .setPageCount(pageCount) .build(); // If anything else went wrong that prevented us from making our determinations etc if (somethingWentWrong) { callback.onLayoutFailed(“Something went wrong”); } else { // Once everything is complete we make a call to onLayoutFinished with the // PrintDocumentInfo we created and we specify whether the layout changed. // In this example we always specify true, that the layout has changed, but in // general this may not be the case. This method, onLayout, may be called // multiple times and in some cases the changes to the PrintAttributes may not // affect your overall layout - this might be a case where you would want to return // false, it really depends on your content and layout callback.onLayoutFinished(info, true); } } |
另外一個必須的方法叫onWrite(),這個方法在需要使用檔案描述符修改PDF檔案的時候,就會呼叫,特別要注意的是這個方法(PrintDocumentAdapter的方法也一樣)是在主執行緒呼叫的;在這裡提出,因為此方法最好是在後臺執行,因為考慮到我們會做一些檔案IO操作。這個方法的主要目的就是往PDF裡面寫內容,把PDF內容寫到檔案裡,然後呼叫對應的回撥方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
; html-script: false ] @Override public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor destination, CancellationSignal cancellationSignal, WriteResultCallback callback) { // First we need to loop over all of our pages and draw their content for(int index = 0; index < mTotalPages; index ++) { // If at any point while drawing the pages we see that the cancellation signal has // been triggered we make the onWriteCancelled method call, clean-up the // PrintedPdfDocument and return if (cancellationSignal.isCanceled()) { callback.onWriteCancelled(); mPdfDocument.close(); mPdfDocument = null; return; } // It is possible that the requested pageRanges do not include every page, in this // case we need to potentially skip some pages if (!pageRangesContainPage(pageRanges, index)) { continue; } // Use the PrintedPdfDocument object to start a page PdfDocument.Page page = mPdfDocument.startPage(index); Canvas canvas = page.getCanvas(); // … draw some stuff to your page’s canvas… // … you may also want to write some PageRange information here so you can // determine all of the ranges you supplied pages for when you go to call // onWriteFinished mPdfDocument.finishPage(page); } // Next we need to write out the updated PrintedPdfDocument to the specified // destination try { mPdfDocument.writeTo(new FileOutputStream(destination.getFileDescriptor())); } catch(IOException e) { callback.onWriteFailed(e.toString()); return; } finally { // Whether or not the write is successful we need to clean-up the // PrintedPdfDocument object mPdfDocument.close(); mPdfDocument = null; } // It is possible that we did not supply pages for all of the requested PageRanges so we // need to pass onWriteFinished an accurate array of the PageRanges. In most cases it // would be sufficient to pass back the same PageRanges array object that was passed // in to us, pageRanges. PageRange[] completedPages = getCompletedPageRanges(); callback.onWriteFinished(completedPages); } |
總的來說,Android新增的列印框架也沒有加了很多新功能,但是它確實讓APP能夠更簡單、可靠地進行列印操作,大多數APP都提供了列印服務,而且很多都預裝在了KitKat裝置中。