关于Android打印技术的调研—如何实现PrintAdapter?
2017-08-18 16:22
836 查看
杂谈—例行墨迹
本来还在复(xue)习(xi)数据结构,然后好好的找一个好工作,但谁知还没等你复(yu)习(xi),就赶上了返校做毕设,写论文。于是一路狂风带闪电的毕业了,然后回北京找工作,一路狂风带闪电的投简历面试,最后没钱了,有一家录取我了,我就先来了。
呵呵,没想到是搞医疗的,虽然也是Android但是有些不爽啊,和我想像的做app根本不一样,而且还有入职培训—长达一个月!!!!!!!!!!都是些关于从业观和产品操作之类的培训。。。。。。。。。。OMG!
是不是很奇葩?从入职的时候,笔试有一篇智力测试题,还带正反面的,我就有一丝不祥的感觉了。
没办法,没钱啊。
然后让我调研一下Android打印,因为Android设备通过USB打印生成的图表,有些模糊还有些慢,所以调研一下,有哪些打印方式,快不快,清不清晰。
到今天为止,调研任务完成,搜集的关于Android打印的知识和涉及的技术如下:
我再墨迹两句,不得不说时间过得真快啊,从上次我想复习数据结构,到现在重新写一篇帖子,已经过了这么久,而这段时间,没钱,对工作不满,一会又想开了,一会又钻牛角尖了,也是挺痛苦的。从以前伸手要钱,要现在前途的一塌糊涂。一眨眼,已经过去这么久了,而我的人生,可能也就如此了,起步也就这么回事了。
PrintAdapter的实现根据官方文档实现,但是官方文档竟然写的是伪代码,所以具体不知道怎么实现,而且有些相关类还用了{@hide}注解,暂未开发好,不开放。所以真是不知道怎么办,也没有个大牛带,也搜索不到资料,真的累,心累,真的苦,心苦。
最终确定PrintHelper+HP打印服务+双View生成大Bitmap优化为最佳方案。(为了不看的无聊,一段size4的字体,一段size3的字体,这样才不会疲劳)
一、一些调研的说明。
关于Android打印的调研,查找了View转换为Bitmap的方法,绘制Pdf文件的方法,实现了Android原生api的打印,WebView的打印,以及Bitmap打印不清晰的优化思路等等。View转换为Bitmap的方法很多,效率上没有差别特别大的,只有按照需要来选择合适的。这些方法包括:布局View转换为Bitmap,ListView转换为Bitmap,ScrollView转换为Bitmap,RecycleView转换为Bitmap,界面中未出现的View转换为Bitmap。
手动绘制Pdf文件需要运用PostScript语言,GhostScript环境,需要Java调ps。Java调ps有一个第三方的jar包,Ghost4J,但是这个工具已经有超过一年不更新了,一旦jdk更新,可能会导致jar包产生一些Bug,而这个jar包目前也只是可以在Java上将ps和Pdf互转,写入ps文件PostSript代码的话,还是需要Java原生的文件读写,Java还是比较费事,而且PostScript在学习的过程中发现资料不是很多,以后在调试过程中可能回很费力。
Android原生打印PrintHelper是用的比较多的,最起码在CSDN上比较多。但是PrintHelper在跳转到Android的打印服务时,即便连在一个局域网的打印机,也找不到。只能下载一个打印服务的第三方,这个是因为国情原因,解决不了。目前的打印第三方,在测试的时候,用的HP和SamSung的第三方,而HP是比较好的,他可以跑去国情的因素,直接搜索到,SamSung的话,手机中没有GooglePlay或者GooglePlay起不来的话,就会崩溃。现在Android原生PrintHelper+HP打印已经成功,也可以控制纸张等。
Android的PrintManager+PrintDocumentAdapter,实现过程在网上找不到任何已经成功的Demo,官方文档中给出的Demo是基于伪代码的实现的。而且一些相关的类,在查看源码时发现大量方法均有/* {@hide} /的注解,此注解的意思是,不开放的API。我们称之为Hidden API,之所以被隐藏,是想阻止开发者使用SDK中那些未完成或不稳定的部分(接口或架构)。举个例子,Bluetooth API在API 5(Android 2.0)上才开放;在API 3 和4上都是用@hide属性隐藏了。当这些API被验证和清理后,Google的开发者会移除@hide属性,并让其在API 5官方化。很多地方在API 4 和5之间发生了变化。如果你的程序依赖某些隐藏的API,当其部署到新的平台上时,就有可能陷入困境。而从这些隐藏的注解从4.0一直持续到了现在的8.0,也可以说明一些问题。因此可知,PrintManager+PrintDocument目前并不完善。
WebView的打印是可以实现的,打印HTML文件,可以在文字上比转换为Bitmap的清晰,在图片上,还是需要原来的图片分辨率够大才行。但是WebView的打印又再一次的受到了国情的影响,Google与中国大陆的相爱相杀使得一些Google的比较冷门的技术在大陆得不到支持,例如WebView的打印,当我们的语言设置为中文简体时,WebView的打印失败,PrintService启动失败。将语言调成中文繁体的时候,才会打印成功。
对于Bitmap打印不清晰的原因,也在调研的过程中分析,除了插件原因,其他还可能有打印机原因以及要打印的图片自身的原因。但据同事说,打印机的USB打印与PC上的打印在效果上有差别,因此打印机自身的原因就暂且排除。但是图片自身的原因,还是在怀疑范围内,因为Android设备和PC生成图片的单位是不同的。而项目中观察了一下布局,图表的布局处大小为高240dp,宽是wrapcontent,因此猜想,是生成的Bitmap的“分辨率”本来就不够大,因此打印的效果不好。因此可以写一个大的View将绘制为大Bitmap,打印在A4纸上,这样Bitmap“分辨率”大,比较清晰,测试时,明显清晰了很多。
基于以上这些调研,相关调研的代码,解决方案如下。
二、一些调研的代码、解决方案等。
View转Bitmap:/** * Created Xwq on 2017/8/11. */ public class SimpleUtils { /** * 将 Bitmap 保存到SD卡 * * @param context * @param mybitmap * @param name * @return */ public static boolean saveBitmapToSdCard(Context context, Bitmap mybitmap, String name) { boolean result = false; //创建位图保存目录 String path = Environment.getExternalStorageDirectory() + "/xwq/"; File sd = new File(path); if (!sd.exists()) { sd.mkdir(); } File file = new File(path + name + ".jpg"); FileOutputStream fileOutputStream = null; if (!file.exists()) { try { // 判断SD卡是否存在,并且是否具有读写权限 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { fileOutputStream = new FileOutputStream(file); mybitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream); fileOutputStream.flush(); fileOutputStream.close(); //update gallery Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); Uri uri = Uri.fromFile(file); intent.setData(uri); context.sendBroadcast(intent); Toast.makeText(context, "保存成功", Toast.LENGTH_SHORT).show(); 12a74 result = true; } else { Toast.makeText(context, "不能读取到SD卡", Toast.LENGTH_SHORT).show(); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return result; }
/** * 手动测量摆放View * 对于手动 inflate 或者其他方式代码生成加载的View进行测量,避免该View无尺寸 * * @param v * @param width * @param height */ public static void layoutView(View v, int width, int height) { // validate view.width and view.height v.layout(0, 0, width, height); int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); // validate view.measurewidth and view.measureheight v.measure(measuredWidth, measuredHeight); v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight()); } public static int px2dip(Context context, float pxValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (pxValue / scale + 0.5f); } /** * 获取一个 View 的缓存视图 * (前提是这个View已经渲染完成显示在页面上) * * @param view * @return */ public static Bitmap getCacheBitmapFromView(View view) { final boolean drawingCacheEnabled = true; view.setDrawingCacheEnabled(drawingCacheEnabled); view.buildDrawingCache(drawingCacheEnabled); final Bitmap drawingCache = view.getDrawingCache(); Bitmap bitmap; if (drawingCache != null) { bitmap = Bitmap.createBitmap(drawingCache); view.setDrawingCacheEnabled(false); } else { bitmap = null; } return bitmap; } /** * 对ScrollView进行截图 * * @param scrollView * @return */ public static Bitmap shotScrollView(ScrollView scrollView) { int h = 0; Bitmap bitmap = null; for (int i = 0; i < scrollView.getChildCount(); i++) { h += scrollView.getChildAt(i).getHeight(); scrollView.getChildAt(i).setBackgroundColor(Color.parseColor("#ffffff")); } bitmap = Bitmap.createBitmap(scrollView.getWidth(), h, Bitmap.Config.RGB_565); final Canvas canvas = new Canvas(bitmap); scrollView.draw(canvas); return bitmap; } /** * 对ListView进行截图 */ public static Bitmap shotListView(ListView listview) { ListAdapter adapter = listview.getAdapter(); int itemscount = adapter.getCount(); int allitemsheight = 0; List<Bitmap> bmps = new ArrayList<Bitmap>(); for (int i = 0; i < itemscount; i++) { View childView = adapter.getView(i, null, listview); childView.measure( View.MeasureSpec.makeMeasureSpec(listview.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); childView.setDrawingCacheEnabled(true); childView.buildDrawingCache(); bmps.add(childView.getDrawingCache()); allitemsheight += childView.getMeasuredHeight(); } Bitmap bigbitmap = Bitmap.createBitmap(listview.getMeasuredWidth(), allitemsheight, Bitmap.Config.ARGB_8888); Canvas bigcanvas = new Canvas(bigbitmap); Paint paint = new Paint(); int iHeight = 0; for (int i = 0; i < bmps.size(); i++) { Bitmap bmp = bmps.get(i); bigcanvas.drawBitmap(bmp, 0, iHeight, paint); iHeight += bmp.getHeight(); bmp.recycle(); bmp = null; } return bigbitmap; } /** * 对RecyclerView进行截图 */ public static Bitmap shotRecyclerView(RecyclerView view) { RecyclerView.Adapter adapter = view.getAdapter(); Bitmap bigBitmap = null; if (adapter != null) { int size = adapter.getItemCount(); int height = 0; Paint paint = new Paint(); int iHeight = 0; final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/8th of the available memory for this memory cache. final int cacheSize = maxMemory / 8; LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize); for (int i = 0; i < size; i++) { RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i)); adapter.onBindViewHolder(holder, i); holder.itemView.measure( View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(), holder.itemView.getMeasuredHeight()); holder.itemView.setDrawingCacheEnabled(true); holder.itemView.buildDrawingCache(); Bitmap drawingCache = holder.itemView.getDrawingCache(); if (drawingCache != null) { bitmaCache.put(String.valueOf(i), drawingCache); } height += holder.itemView.getMeasuredHeight(); } bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888); Canvas bigCanvas = new Canvas(bigBitmap); Drawable lBackground = view.getBackground(); if (lBackground instanceof ColorDrawable) { ColorDrawable lColorDrawable = (ColorDrawable) lBackground; int lColor = lColorDrawable.getColor(); bigCanvas.drawColor(lColor); } for (int i = 0; i < size; i++) { Bitmap bitmap = bitmaCache.get(String.valueOf(i)); bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint); iHeight += bitmap.getHeight(); bitmap.recycle(); } } return bigBitmap; } }
布局Layout转Bitmap
private Bitmap convertView2Bitmap(View tempView) { Bitmap bitmap = Bitmap.createBitmap(tempView.getWidth(), tempView.getHeight(), Bitmap.Config.ARGB_8888); bitmap.setHasAlpha(false); Canvas canvas = new Canvas(bitmap); tempView.draw(canvas); return bitmap; } 利用Android绘制时的缓存。 linearLayout.setDrawingCacheEnabled(true); linearLayout.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); linearLayout.layout(0, 0, linearLayout.getMeasuredWidth(), linearLayout.getMeasuredHeight()); bitmap[0] = Bitmap.createBitmap(linearLayout.getDrawingCache()); linearLayout.setDrawingCacheEnabled(false);
Android PrintHelper+HP打印服务。
PrintHelper printHelper = new PrintHelper(this); /** * PrintHelper通过setScaleMode()方法设置模式,现在有两种模式 * SCALE_MODE_FIT:这个打印完整的图片,这样打印纸的边缘可能有空白 * SCALE_MODE_FILL:这个填满所有的打印纸,因此图片的边缘可能打印不出来 */ printHelper.setScaleMode(PrintHelper.SCALE_MODE_FIT); // 打印图片 printHelper.printBitmap("Print Bitmap", ConvertAndSet());
PrintManager+PrintDocumentAdapter:
private PrintManager printManager; private List<PrintJob> printJobList; private PageRange[] pageRanges; private PageRange[] writtenPagesRanges; printManager = (PrintManager) this.getSystemService(Context.PRINT_SERVICE); printJobList = printManager.getPrintJobs(); if (!printJobList.isEmpty()) writtenPagesRanges = printJobList.get(0).getInfo().getPages(); else writtenPagesRanges = new PageRange[0]; android.print.PrintJob printJob = printManager.print("mDocument", new PrintAdapter(new PrintAdapter.getPrintItemCountListener() { @Override public int getPrintItemCount() { return 2; } }, this, writtenPagesRanges), null); PrintJobInfo printJobInfo = printJob.getInfo(); printJobList.add(printJob); PageRange[] pageRanges = printJobInfo.getPages(); if (writtenPagesRanges.length == 0) writtenPagesRanges = pageRanges; else { /** * 在writtenPagesRanges后追加我们的pageRanges * * */ } public class PrintAdapter extends PrintDocumentAdapter { private getPrintItemCountListener getPrintItemCount = null; private PdfDocument pdfDocument = null; private Context context; private int totalPages; private long startTimes, endTimes; private PageRange[] writtenPagesArray;//打印的总数组 private PageRange[] newWrittenPagesArray;//这次打印的数组 public PrintAdapter(getPrintItemCountListener getPrintItemCount, Context context, PageRange[] writtenPagesArray) { this.getPrintItemCount = getPrintItemCount; this.context = context; this.writtenPagesArray = writtenPagesArray; } @Override public void onStart() { super.onStart(); //初始化 startTimes = System.currentTimeMillis(); Log.i("xwq", "startTimes:" + startTimes); } @Override public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) { pdfDocument = new PrintedPdfDocument(context, newAttributes); if (cancellationSignal.isCanceled()) { callback.onLayoutCancelled(); return; } totalPages = computePageCount(newAttributes); if (totalPages > 0) { newWrittenPagesArray = new PageRange[totalPages]; PrintDocumentInfo printDocumentInfo = new PrintDocumentInfo.Builder("print_output.pdf") .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT) .setPageCount(totalPages) .build(); callback.onLayoutFinished(printDocumentInfo, true); } else { callback.onLayoutFailed("Page count calculation failed.<=0"); } } @Override public void onWrite(PageRange[] pages, ParcelFileDescriptor destination, CancellationSignal cancellationSignal, WriteResultCallback callback) { for (int i = 0; i < totalPages; i++) { if (containsPages(pages, i)) { /** * 这次打印的数组中,添加当前i页 */ for (int j = 0; j < newWrittenPagesArray.length; j++) { if (newWrittenPagesArray[j] == null) { newWrittenPagesArray[j] = pages[i]; } } /** *当前i页添加进总数组中,这里写了这个方法,在外面就不用写追加的方法了。 * 这里就打印当前数组了,写死了。 */ } PdfDocument.Page page = pdfDocument.startPage(pdfDocument.getPages().get(i)); if (cancellationSignal.isCanceled()) { callback.onWriteCancelled(); pdfDocument.close(); pdfDocument = null; return; } drawPage(page); pdfDocument.finishPage(page); } try { pdfDocument.writeTo(new FileOutputStream(destination.getFileDescriptor())); } catch (IOException e) { e.printStackTrace(); callback.onWriteFailed(e.toString()); } finally { pdfDocument.close(); pdfDocument = null; } pdfDocument.getPages(); /** * 最后计算一下需要打印的数组,应该是总数组。 * 这里就将当前数组打印了。 */ /*PageRange[] writtenPages = computeWrittenPages(); callback.onWriteFinished(writtenPages);*/ callback.onWriteFinished(newWrittenPagesArray);//将当前打印数组打印 } private PageRange[] computeWrittenPages() { return new PageRange[0]; } private void drawPage(PdfDocument.Page page) { Canvas canvas = page.getCanvas(); int titleBaseLine = 72; int leftMargin = 54; Paint paint = new Paint(); paint.setColor(Color.RED); paint.setTextSize(36); canvas.drawText("Hello Title", leftMargin, titleBaseLine, paint); paint.setTextSize(11); canvas.drawText("Hello paragraph", leftMargin, titleBaseLine + 25, paint); paint.setColor(Color.BLUE); canvas.drawRect(100, 100, 172, 172, paint); } private boolean containsPages(PageRange[] pages, int i) { int mStart, mEnd; boolean isContain = false; for (int j = 0; j < pages.length; j++) { mStart = pages[j].getStart(); mEnd = pages[j].getEnd(); isContain = (i >= mStart) && (i <= mEnd); if (isContain) return isContain; } return isContain; } @Override public void onFinish() { super.onFinish(); endTimes = System.currentTimeMillis(); Log.i("xwq", "endTimes:" + endTimes + " and the time between start and finish is :" + (endTimes - startTimes)); } private int computePageCount(PrintAttributes printAttributes) { int itemsPerPage = 4; PrintAttributes.MediaSize pageSize = printAttributes.getMediaSize(); if (!pageSize.isPortrait()) { itemsPerPage = 6; } if (getPrintItemCount != null) { int printItemCount = getPrintItemCount.getPrintItemCount(); return (int) Math.ceil(printItemCount / itemsPerPage); } else { return 1; } } //定义监听者,用来回调我们打印的条目数,不过到现在也不知道这个条目数是什么 public interface getPrintItemCountListener { public int getPrintItemCount(); }
WebView打印:
public static String getHtmlUrl(Context context, String htmlName) { String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/xwq/"; File sd = new File(path); if (!sd.exists()) { sd.mkdir(); } Log.i("xwq", path); File file = new File(path + htmlName + ".html"); if (file.exists()) { String url = "file:/" + file.getPath(); Log.i("xwq", "url string :" + url.toString()); return url; //return String.ValueOf(Uri.fromFile(file)); } return null; } 对webview进行设置。支持js函数,并在webview加载结束后,在进行操作。在这里操作直接为打印。 private void file2webview() { WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webView.loadUrl(SimpleUtils.getHtmlUrl(this, "xwq")); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { return false; } @Override public void onPageFinished(WebView view, String url) { Log.i("xwq", "webview finished"); createWebPrintJob(webView); } }); } 打印。 private void createWebPrintJob(WebView webView) { // Get a PrintManager instance PrintManager printManager = (PrintManager) this .getSystemService(Context.PRINT_SERVICE); // Get a print adapter instance PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter(); // Create a print job with name and adapter instance String jobName = getString(R.string.app_name) + " Document"; PrintJob printJob = printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build()); /*// Save the job object for later status checking mPrintJobs.add(printJob);*/ }
PrintAdapter的实现只是自己瞎想,不会,求大神指点。
Bitmap不清晰优化。
测试的时候打印了一个html文件,文件里有简单的文本和一张图片, 此图片是网上down的一个图表图片,分辨率为1237*709,效果良好, 后又down了一个741*322的,效果对于741*322来说还不错,可是对比1237*709的话,感觉效果不够好。 因此猜测,项目中的打印效果不好可能是因为在转换bitmap的时候,分辨率不够大。 但是关于bitmap生成大分辨率的方法,找不到,因为如果要像photoshop一样可以图片质量变大的话,需要研究基于图像的算法,根据像素点向周围的扩散等等,这个估计要个博士来写才可能写成,而且photos中的效果也不太好,会有失真现象。 所以bitmap只存在分辨率变小的方法,原因很简单,bitmap是很据view生成的,view的大小限制了bitmap的最大像素点。 最后思考归结于,在view转换为bitmap的时候,view的大小本身就小,所以生成的bitmap一定会分辨率不够, 例如项目中report页面中的linechart,高只有240dp,这样分辨率变得最大你还能大过对于设备的屏幕240dp所对应px吗? 所以给出思路是在生成报告单的同时,生成两张报告单,一张用于用户观看,可与用户交互,大小和项目中原来无异, 另一张隐藏,用户看不到,只为打印而用, 这个隐藏的view的宽高,与其内部字体的大小,和图表中的字体大小线条粗细等等,都设为尽可能地大(具体大小需要实践,才知道多大最好), 反正不给用户看这个,然后生成bitmap的时候,指定生成bitmap的大小,令这个大大的视图画在小的画布里(其实不用,当时这么写的是因为没有考虑到,只需要打印在A4纸上就可以了), 这样,生成的bitmap就会分辨率变大,然后打印。 经测试,效果明显比之前要好。至于在项目中到底怎么样,还需要进一步探究。 隐藏的View可以将布局写在主布局的外部这样实现,不过生成大图片的时候需要注意OOM等现象。
相关文章推荐
- 关于学习android中v4包中PagerAdapter实现引导页效果
- .Net中的打印A4纸如何实现(以C#为例)——VB中的Printer.Print在.Net中如何实现
- Android如何实现毛玻璃效果之Android高级模糊技术
- Android如何实现毛玻璃效果之Android高级模糊技术
- 关于Android开发中实现锚点技术,也是焦点的改变
- android 如何实现连接蓝牙打印机来实现打印功能
- 关于如何完整的调用支付宝集成开发包,实现android端无线快捷支付(详细无比)
- Android实现动态验证码的技术调研与实现
- 关于如何实现javascript for supermap调用arcgis切片的关键技术研究
- [置顶] 关于如何实现Android透明状态栏的总结
- Android 关于MQTT实现单聊和群聊功能,强大的技术
- 关于如何使用js 插件实现打印的功能
- <如何实现1080P延迟低于500ms的实时超清直播传输技术> <关于直播,所有的技术细节都在这里了>
- 关于Android 如何实现mobile data on/off功能
- Android如何实现毛玻璃效果之Android高级模糊技术
- 【Android】如何配合服务端实现防盗链技术
- 关于android BaseAdapter 中如何灵活控制listview 是否显示,是否有事件触发
- 关于android中listview的adapter如何通用的一些看法
- 关于System.Drawing.Print名称空间中给类对打印的管理与实现
- 【Android技术整理】Handler以及Handler如何实现多线程