关于Android打印技术的调研—如何实现PrintAdapter?

来源:互联网 发布:cocos2dx程序员 编辑:程序博客网 时间:2024/06/11 03:26

杂谈—例行墨迹

本来还在复(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();                    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等现象。
原创粉丝点击