文章大纲

  • 引言
  • 一、通过Android 官方提供的打印库
    • 1、第一步安装打印服务插件
    • 2、判断是否支持打印服务
    • 3、调用v4库对应的api完成打印
      • 3.1、 打印图片
      • 3.2、打印PDF
  • 二、移花接木
  • 三、原始Socket进行网络通信

引言

最近接到领导的一个需求,需要通过Android 端直接控制局域网打印机进行打印,一开始查阅了很多资料包括各大品牌官网开发者文档,最后终于实现了,这篇文章就简单总结下,目前在Android 应用层通过局域网Wi-Fi快速调用家用打印机(首先得支持无线打印的功能,最好还是Mopria联盟的成员及认证机器)进行打印实现方式主要有三种,接下来将一一介绍。

一、通过Android 官方提供的打印库

大概是在Android API 19 之后,Android 在V4兼容包下提供了一个名为android.support.v4.print打印支持包,通过官方的调用包下对应的API是可以快速实现局域网Wi-Fi调用家用打印机完成图片或者文档的打印的,不过呢Google 官方并没有那么友好,提前帮你适配各种打印机的驱动,因此这个库正常工作的前提是需要依赖各品牌官方或者第三方集成商提供的打印服务插件(比如第三方Mopria PrintService、惠普提供的 HP Print Service、佳能提供的 Canon Print Service等等),至于使用何种插件取决于你自己,两者互有优劣,第三方集成的服务在于兼容品牌多,但是有些型号可能没有对应的驱动支持,而各品牌官方的优势则在于可以完美适配对应品牌型号的打印机,缺点就是各厂家之间不能通用,要想使用哪种品牌的就得安装对应的打印服务插件。

1、第一步安装打印服务插件

通常这些所谓的服务插件都是以APK的形式提供的,有条件的话到Google play 官网上去下载,当然国内各大应用商店都有直接输入英文搜索就行,千万不要去那种垃圾的网站去下载什么所谓的完美破解版,都是些挂羊头卖狗肉的垃圾,下载完毕在之后安装,可以通过代码静默安装也可以引导安装,安装完毕之后还需要先到设置界面中开启对应的服务,开启对应服务之后,当我们需要打印时,他们会去帮我完成连接打印机等一系列准备工作(比如说提供搜索Wi-Fi下同一网段的打印机适配驱动等等),我们只需要调用对应的API传入要打印的数据即可。

2、判断是否支持打印服务

直接调用android.support.v4.print.PrintHelper中**systemSupportsPrint()**的方法

Log.e("cmo","是否支持:"+PrintHelper.systemSupportsPrint());

3、调用v4库对应的api完成打印

3.1、 打印图片

    private void photoPrint() {//初始化创建PrintHelper对象PrintHelper photoPrinter = new PrintHelper(this);photoPrinter.setScaleMode(PrintHelper.SCALE_MODE_FIT);Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher_round);//第一个参数为jobName 任意字符串,建议可以使用随机字符串,下同photoPrinter.printBitmap("cmo:photoPrint", bitmap);}

3.2、打印PDF

  • View 转为PDF,Android 给我提供了原生的创建PDF文档的API,通过这些API我们可以把绝大部分的View 转为PDF文档。
package com.crazymo.printer;import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;/*** @author crazy.mo*/
public class MoPrintPdfAdapter extends PrintDocumentAdapter {private String mFilePath;public MoPrintPdfAdapter(String file) {this.mFilePath = file;}@Overridepublic void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal,LayoutResultCallback callback, Bundle extras) {if (cancellationSignal.isCanceled()) {callback.onLayoutCancelled();return;}PrintDocumentInfo info = new PrintDocumentInfo.Builder(getJobName()).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build();callback.onLayoutFinished(info, true);}@Overridepublic void onWrite(PageRange[] pages, ParcelFileDescriptor destination, CancellationSignal cancellationSignal,WriteResultCallback callback) {InputStream input = null;OutputStream output = null;try {input = new FileInputStream(mFilePath);output = new FileOutputStream(destination.getFileDescriptor());byte[] buf = new byte[1024];int bytesRead;while ((bytesRead = input.read(buf)) > 0) {output.write(buf, 0, bytesRead);}callback.onWriteFinished(new PageRange[]{PageRange.ALL_PAGES});} catch (FileNotFoundException e) {} catch (Exception e) {e.printStackTrace();} finally {try {input.close();output.close();} catch (IOException e) {e.printStackTrace();}}}private String getJobName() {try {String[] filePaths = mFilePath.split(File.separator);return filePaths[filePaths.length - 1];} catch (Exception e) {e.printStackTrace();}return String.valueOf(System.currentTimeMillis());}
}

把View 转为PDF,此处需要注意View 必须是已经渲染加载完毕之后,否则无法把内容写入到PDF中,此处为简单Demo,实际项目中建议使用线程池替代这种独立创建线程的方式,另外对于Android 6.0及以上版本需要处理动态权限,下同。

    /*** 把View转为PDF,必须要在View 渲染完毕之后* 1.使用LayoutInflater反射出来的View不行; * 2. 将要转换成pdf的xml view文件include到一个界面中,将其设置成android:visibility=”invisible”就可以实现,不显示,但是能转换成PDF; * 3. 设置成gone不行;* @param view* @param pdfName*/private void createPdfFromView(@NonNull View view, @NonNull final String pdfName ){//1, 建立PdfDocumentfinal PdfDocument document = new PdfDocument();PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(view.getMeasuredWidth()   , view.getMeasuredHeight(), 1)//设置绘制的内容区域,此处我预留了10的内边距.setContentRect(new Rect(10,10,view.getMeasuredWidth()-10,view.getMeasuredHeight()-10)).create();PdfDocument.Page page = document.startPage(pageInfo);view.draw(page.getCanvas());//必须在close 之前调用,通常是在最后一页调用document.finishPage(page);//保存至SD卡new Thread(new Runnable() {@Overridepublic void run() {try {String path = Environment.getExternalStorageDirectory() + File.separator + pdfName;File e = new File(path);if (e.exists()) {e.delete();}document.writeTo(new FileOutputStream(e));document.close();} catch (IOException e) {e.printStackTrace();}}}).start();}
  • 调用PrintManager 的方法执行PDF打印
    /*** 用系统框架打印PDF* @param filePath*/private void doPdfPrint(String filePath) {String jobName = "jobName";PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);MoPrintPdfAdapter myPrintAdapter = new MoPrintPdfAdapter(filePath);// 设置打印参数PrintAttributes attributes = new PrintAttributes.Builder().setMediaSize(PrintAttributes.MediaSize.ISO_A4).setResolution(new PrintAttributes.Resolution("id", Context.PRINT_SERVICE, 480, 320)).setColorMode(PrintAttributes.COLOR_MODE_COLOR).setMinMargins(PrintAttributes.Margins.NO_MARGINS).build();printManager.print(jobName, myPrintAdapter, attributes);}

欲了解更多请参见官方文档

二、移花接木

所谓移花接木其实本质上是一种投机取巧的方式,主要思路就是在自己的APP中调用另一个APP的提供的打印功能,目前比较好用的APP有随行打印 PrintHand(新版本还提供了大量品牌对应的专用驱动及通用驱动,对于一些型号的打印机来说通用驱动也是可以完美支持的)、PrintShare 、品牌官方提供的手机打印APP,其中PrintHand 内部中使用PrintShare的代码,并进行了优化和扩展,所以呢推荐使用PrintHand,如果有条件的话建议到Google Play上去购买下载收费版,千万不要去国内搜索引擎搜索下载所谓的破解版,因为我已经找了很多资源网站上都没有收费破解版的(如果找到了不妨分享大家下),有些甚至是根本不能用,哪些资源网站还能再无耻一点,安装完毕之后,借助第三方APP的方式来实现打印,好处在于可以进行很多个性化的设置和简单便捷对接,但无法主动掌控打印流程,核心思想就是在我们自己的APP中匿名启动另一个APP的Activity

public class ActivityHelper {/***PrintHand 打印ApplicationId*/public static final String APPID_PRINTHAND = "com.dynamixsoftware.printhand";public static final String MAIN_PRINTHAND = APPID_PRINTHAND+".ui.ActivityMain";/***惠普打印ApplicationId*/public static final String APPID_HP="com.hp.printercontrol";public static final String MAIN_HP = APPID_HP+".base.PrinterControlActivity";/*** 通过应用的包名和对应的Activity全类名启动任意一个Activity(可以跨进程)* 如果该Activity非应用入口(入口Activity默认android:exported="true"),则需要再清单文件中添加 android:exported="true"。* Service也需要添加android:exported="true"。允许外部应用调用。* @param pkg 应用的包名即AppcationId* @param cls 要启动的Activity 全类名*/public static void startActivityByComponentName(Context context, String pkg, String cls) {ComponentName comp = new ComponentName(pkg,cls);Intent intent = new Intent();intent.setComponent(comp);intent.setAction("android.intent.action.VIEW");intent.setAction("android.intent.action.SEND");intent.addCategory("android.intent.category.DEFAULT");context.startActivity(intent);}
}

打开PrintHand的打印界面,无论是想要打开哪个APP的界面,你得先安装对应的APP,然后去获取对应的APPLICATIONID和对应Activity的信息,最后通过匿名启动方式启动即可。

ActivityHelper.startActivityByComponentName(this, ActivityHelper.APPID_PRINTHAND, ActivityHelper.MAIN_PRINTHAND);

启动了第三方APP的打印界面之后,就相当于是把打印任务交到别人手上了,至于如何操作是第三方APP的事了。对了,我查阅了惠普打印机开发者官网,发现在Android8.0之后自动集成了惠普远程打印的功能,因为惠普不提供打印的SDK了。

三、原始Socket进行网络通信

这种形式是最本质的实现远程打印的原理,绝大部分第三方APP都是基于Socket通信的封装而已。原来我以为每个厂家应该都会定制了专属于自己的私有的网络通信协议,没想到竟然是最简答的Socket就能通过同一网段进行访问,是最简单的C/S架构,可以把打印机看成S层,所以我们想通过手机去向打印机发出请求,只需要拿到打印机的IP和对应的端口号,使用Sokect去通信即可,因为同一网段的打印机只要接收到网段内的Sokect请求就会接收,如此我们便可以拿到Sokect的输出流OutputStream,于是打印则演变成为了向OutputStream 写入数据,有些打印机还支持输入流InputStream,通过InputStream我们或许还可以拿到打印机的状态

package com.crazymo.moprint.util;import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;/*** @author : Crazy.Mo*/
public class WifiPrinter {private InputStream mInputStream;/*** 通过socket out 流往打印机发送数据*/private OutputStream mOutputStream;private OutputStreamWriter mWriter;private Socket mSocket;private String mEncode;private String mIp;private int mPort;public WifiPrinter(String ip, int port) {mEncode = StandardCharsets.UTF_8.name();this.mIp = ip;this.mPort = port;try {if (mSocket != null) {closeIOAndSocket();} else {mSocket = new Socket();}mSocket.connect(new InetSocketAddress(mIp, mPort));mInputStream = mSocket.getInputStream();mOutputStream = mSocket.getOutputStream();mWriter = new OutputStreamWriter(mOutputStream, mEncode);} catch (Exception e) {Log.e("cmo", "构造WifiPrintHelper2对象" + e.toString());}}/*** */public boolean isConnect() {if (mSocket != null) {if (!mSocket.isClosed() && mSocket.isConnected()) {return true;}}return false;}/*** 关闭IO流和Socket*/public void closeIOAndSocket() {try {if (mInputStream != null) {mInputStream.close();}if (mOutputStream != null) {mOutputStream.close();}if (mWriter != null) {mWriter.close();}if (mSocket != null) {mSocket.close();}} catch (IOException e) {Log.e("cmo", "关闭流异常");}}/*** 打印换行 和打印空白(一个Tab的位置,约4个汉字)** @param lineNum* @param tag     换行"\n" "\t"* @return length 需要打印的空行数* @throws IOException*/public void printLine(final int lineNum, final String tag) {if (mWriter != null) {try {for (int i = 0; i < lineNum; i++) {mWriter.write(tag);}mWriter.flush();} catch (IOException e) {Log.e("cmo", "打印空白字符异常:" + e.getMessage());}}}/*** @param length 需要打印空白的长度,* @throws IOException*/private void printTabSpace(int length) {printLine(length, "\t");}/*** 打印文字** @param text* @throws IOException*/public void printText(final String text) {try {byte[] content = text.getBytes(mEncode);mOutputStream.write(content);mOutputStream.flush();} catch (IOException e) {Log.e("cmo", "打印字符串异常:" + e.getMessage());}}/*** @param pdfPath 全路径*/public void printPDF(final String pdfPath) {File pdf;if (TextUtils.isEmpty(pdfPath)) {return;}pdf = new File(pdfPath);if (!pdf.exists()) {return;}byte[] buf = new byte[1024];int bytesRead;FileInputStream input=null;try {input = new FileInputStream(pdfPath);while ((bytesRead = input.read(buf)) > 0) {mOutputStream.write(buf, 0, bytesRead);}mOutputStream.flush();} catch (IOException e) {Log.e("cmo", "打印图片异常:" + e.getMessage());}try {input.close();} catch (IOException e) {e.printStackTrace();}}/*** 并不一定兼容打印二维码* @param qrData 二维码的内容* @throws IOException*/public void qrCode(final String qrData) {int moduleSize = 8;try {int length = qrData.getBytes(mEncode).length;//打印二维码矩阵mWriter.write(0x1D);mWriter.write("(k");mWriter.write(length + 3);mWriter.write(0);mWriter.write(49);mWriter.write(80);mWriter.write(48);mWriter.write(qrData);mWriter.write(0x1D);mWriter.write("(k");mWriter.write(3);mWriter.write(0);mWriter.write(49);mWriter.write(69);mWriter.write(48);mWriter.write(0x1D);mWriter.write("(k");mWriter.write(3);mWriter.write(0);mWriter.write(49);mWriter.write(67);mWriter.write(moduleSize);mWriter.write(0x1D);mWriter.write("(k");mWriter.write(3);mWriter.write(0);mWriter.write(49);mWriter.write(81);mWriter.write(48);mWriter.flush();} catch (IOException e) {Log.e("cmo", "打印二维码异常:" + e.getMessage());}}/*** 使用:CrazyThreadPool.THREAD_POOL_EXECUTOR.execute(new Runnable(){});*/public static class CrazyThreadPool {private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();private static final int CORE_POOL_SIZE = (CPU_COUNT + 1);private static final int KEEP_ALIVE = 1;private static final int MAXIMUM_POOL_SIZE = ((CPU_COUNT * 2) + 1);private static final BlockingQueue<Runnable> WORKQUEUE = new LinkedBlockingQueue<>(64);private static final ThreadFactory THREADFACTORY = new ThreadFactory() {private final AtomicInteger mCount = new AtomicInteger(1);@Overridepublic Thread newThread(Runnable r) {return new Thread(r, "WifiPrint #" + this.mCount.getAndIncrement());}};public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,TimeUnit.SECONDS, WORKQUEUE, THREADFACTORY);}
}

使用Sokect进行远程打印,此处需要注意一些细节,因为Sokect可以支持长连接,但是如果不处理好,当以下情况发生时Sokect会断开:

  • 直接调用Socket类的close方法

  • Socket类的InputStream和OutputStream有一个关闭(必须是主动调用调用InputStream和OutputStream的 close方法关闭流),网络连接自动关闭

  • 程序退出时网络连接自动关闭

  • Socket对象设为null或未关闭并使用new Socket()建立新对象后,由JVM的垃圾回收器回收为Socket对象分配的内存空间后自动关闭网络连接。

在使用Sokect 方法判断连接状态时,需要注意下两个方法isClosed方法和isConnected()方法:

  • isClosed方法——用来返回当前Sokect是否关闭,关闭则返回true。即不管Sokect对象是否曾经连接成功过,只要处于
    关闭状态,isClosde就返回true。即使是建立一个未连接的Sokect对象,isClose也同样返回true

  • isConnected() ——用于返回sokect曾经是否成功连接过,而不是当前状态。isConnected方法所判断的并不是Sokect对象的当前连接状态,而是Sokect对象是否曾经连接成功过;如果成功连接过,即使现在isClosed返回true,isConnected仍然返回true。

因此要判断当前的Sokect对象是否处于连接状态,必须同时使用isClosed和isConnected方法,即只有当isClosed返回false,isConnected返回true的时候Sokect对象才处于连接状态,再次发送打印请求时可能会发生Sokect通信异常,所以我这里直接使用的是短连接的形式替代,每一次打印请求发送完毕之后就关闭此次的Sokect即对应的流

 WifiPrinter.CrazyThreadPool.THREAD_POOL_EXECUTOR.execute(new Runnable() {@Overridepublic void run() {WifiPrinter wifiPrintHelper= new WifiPrinter("192.168.0.101",9100);wifiPrintHelper.printText("676810029020988879217932789789989879797978798178668");wifiPrintHelper.printLine(1,"\n");wifiPrintHelper.printText("android wifi print!");wifiPrintHelper.qrCode("hello world hahhahahahahahahahh");wifiPrintHelper.closeIOAndSocket();}});

如果大家去运行,就会发现第三种方式虽然比较简单,可控性也比较强,但是对于格式来说就不好控制了,因为我们这里输入的都是原始的字节数据,目前对于Sokect打印方式,我采取的是先把原始的数据转为PDF,再把PDF的数据传入Sokect输出流中,无论是图片、布局、还是网页,都可以转为PDF再传入输出流

WifiPrinter.CrazyThreadPool.THREAD_POOL_EXECUTOR.execute(new Runnable() {@Overridepublic void run() {WifiPrinter wifiPrintHelper = new WifiPrinter("192.168.0.101", 9100);wifiPrintHelper.printPDF(Environment.getExternalStorageDirectory() + File.separator + "table3.pdf");wifiPrintHelper.closeIOAndSocket();}});

如果需要精确优质的打印效果和排版,那么可能得查查阅对应打印机的打印指令,然后传入对应的指令字节数据,比如说惠普打印机的PLC等,这样以来就需要对不同品牌的打印指令进行适配,以上是个人浅见,仅供参考。
PS:源码传送门

Android进阶——Android控制端连接同一网段Wi-Fi家用打印机小结相关推荐

  1. Android进阶-Android自带APIDemo与震动器

    Android进阶-Android自带APIDemo与震动器 API-Demo 在android-sdk\samples\android-14\ApiDemos下有许多Android为他的特性提供的D ...

  2. Android进阶——Android弹窗组件工作机制之Dialog、DialogFragment

    前言 Android在DialogFragment推出后,就已经不推荐继续使用Dialog,可替换为DialogFragment,其实DialogFragment只不过是对增加一层看不到的Fragme ...

  3. Android进阶——Android跨进程通讯机制之Binder、IBinder、Parcel、AIDL

    前言 Binder机制是Android系统提供的跨进程通讯机制,这篇文章开始会从Linux相关的基础概念知识开始介绍,从基础概念知识中引出Binder机制,归纳Binder机制与Linux系统的跨进程 ...

  4. Android进阶——Android视图工作机制之measure、layout、draw

    前言 自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信.其实只要了解了View的工作机制后 ...

  5. Android进阶——网络通信之ip rule,ip route等策略路由小结

    文章大纲 引言 一.策略路由概述 二.策略路由相关理论知识 1.内核配置的缺省路由表 1.1.`0`#表 1.2.`253`#default表 1.3. `254`#main表 1.4.`255`#l ...

  6. Android进阶——Android四大组件启动机制之Activity启动过程

    前言 Activity启动过程涉及到的比较多的知识点有Binder的跨进程通讯,建议先看完Binder的跨进程通讯再来阅读本篇文章,在文章阅读开始,我们先要理解Activity启动模型,再者去理解有关 ...

  7. Android 进阶——Android 系统的基础术语和编译的相关理论小结

    文章大纲 引言 一.Android系统的分区 1./boot 引导分区 2./system 系统分区 3./recovery 恢复分区 刷入RE: 4./data 用户数据区 5./cache 数据缓 ...

  8. 【Android春招每日一练】(十六) 剑指4题+Android进阶

    文章目录 概览 剑指offer 1.61 翻转单词顺序 1.62 左旋转字符串 1.63 滑动窗口的最大值 1.64 队列的最大值 Android进阶 Android布局优化 Android权限处理 ...

  9. 我的Android进阶之旅------经典的大客推荐(排名不分先后)!!

    今天看到一篇文章,收藏了很多大牛的博客,在这里分享一下(转载于:http://blog.csdn.net/wujxiaoz/article/details/8237096) Android中文Wiki ...

最新文章

  1. cmder里ls、pwd、自定义的alias等一系列命令都无法使用
  2. 使用jQuery操作DOM
  3. Node初学者入门,一本全面的NodeJS教程,微小的web框架,能实现文件上传功能以及数据解析功能...
  4. 全球及中国5-氯-2-羟基苯甲酸产业专项调研与投资潜力预测报告2022-2028年
  5. 网管心得:优化网络性能给局域网提速[好文章]
  6. samba服务器查看文件共享,我的笔记Uuntu下Samba服务器共享文件夹在windows7 下查看.doc...
  7. PHP 服务器变量 $_SERVER(转)
  8. DataTable增加行
  9. JVM笔记1:Java内存模型及内存溢出
  10. php 自学提升进阶路线,瓶颈
  11. 极光im php,利用php+curl调用极光IM第三方REST API方法经验
  12. C++ Socket服务器简单代码示例
  13. python爬楼梯问题_使用python算法解决楼梯台阶问题方法详解
  14. ubuntu 中 vi 编辑文件上下左右删除键毫无作用肿么办!(上上下下左右左右BABA)
  15. 搜狗站长html标签验证,教你把企业网站添加到搜狗站长平台
  16. hiberfil.sys文件过大
  17. php面试题(附带答案)
  18. 组态王bitset用法_宇电AI系列仪表和组态王在产品检测装置中的应用
  19. Qt Q_UNUSED使用
  20. 代理/ssh端口转发

热门文章

  1. table、tr、td表格的行、单元格等属性说明
  2. 计算机照片无法打开,提示windows照片查看器无法打开此图片怎么处理
  3. SCM系统有什么好处?
  4. 什么是零拷贝技术(Zero Copy)?
  5. 强大无比!百度文库、音视频下载、商品历史价…一行命令满足你的各种需求...
  6. spidermonkey_Mozilla改进了SpiderMonkey JavaScript引擎中的RegExp支持
  7. SpiderMonkey 入门翻译
  8. etc微信充值显示服务器错误,etc微信
  9. Source Code - JavaScript - 学习优雅的编码
  10. 想选一个项目来做剧本杀和游戏代理加盟怎么选?