您的位置:首页 > 移动开发 > Android开发

关于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等现象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息