引言
使用 flutter 進(jìn)行 pos 軟件開發(fā)勇蝙,諸多的業(yè)務(wù)場景都涉及到了 小票、標(biāo)簽 打印综液】羁基于通用化,跨平臺的設(shè)計(jì)要求谬莹,小編整理了這篇文章檩奠。
核心設(shè)計(jì)思想
跨平臺通用化方案我們主要解決以下三點(diǎn):
票據(jù)樣式: 拒絕硬編碼,直接使用 flutter-widget 進(jìn)行樣式開發(fā)附帽,更加直觀靈活
打印指令集: 不嵌入廠商的打印SDK埠戳,適配一碼多用,無后續(xù)接入開發(fā)消耗
傳輸方式: 指令集傳輸方式可擴(kuò)展蕉扮,底層代碼無需變動
整體方案流程
票據(jù)樣式使用 flutter-widget 進(jìn)行開發(fā)整胃,打印策略使用光柵位圖的統(tǒng)一標(biāo)準(zhǔn)進(jìn)行指令集中轉(zhuǎn),所有的數(shù)據(jù)轉(zhuǎn)換在 dart 層完成喳钟,網(wǎng)絡(luò)打印機(jī)使用 dart-socket 進(jìn)行傳輸屁使,usb 傳輸封裝各平臺的 write 方法在岂。
整個(gè)工具庫做統(tǒng)一數(shù)據(jù)中轉(zhuǎn),各平臺無需考慮數(shù)據(jù)層的處理蛮寂,只處理數(shù)據(jù)傳輸蔽午。
步驟一: 將 widget 轉(zhuǎn)換打印圖層
使用 flutter-widget 開發(fā)票據(jù)樣式,數(shù)據(jù)填充 widget 后酬蹋,加入隊(duì)列轉(zhuǎn) Uint8List 圖像數(shù)據(jù)
功能實(shí)現(xiàn)可直接使用我們的開源庫 print_image_generate_tool及老,提供能力將 widget 視圖轉(zhuǎn)換成 Uint8List 數(shù)據(jù),內(nèi)部維護(hù)隊(duì)列范抓,按加入順序生成返回圖像數(shù)據(jù)骄恶。
使用方式:
首先,初始化打印圖層
- 在頁面根節(jié)點(diǎn)下將打印圖層 PrintImageGenerateWidget 初始化
MaterialApp(
onGenerateTitle: (context) => '打印測試',
home: Scaffold(
body: PrintImageGenerateWidget(
contentBuilder: (context) {
return const HomePage();
},
onPictureGenerated: _onPictureGenerated, //用于接收 widget 轉(zhuǎn) Uint8List
),
),
)
//打印圖層生成成功
Future<void> _onPictureGenerated(PicGenerateResult data) async {
//widget生成的圖像的字節(jié)結(jié)果
final imageBytes = data.data;
//打印票據(jù)類型(標(biāo)簽尉咕、小票)
final printTypeEnum = printTask.printTypeEnum;
//... 打印邏輯下面補(bǔ)充
}
- 將 widget 生成圖層數(shù)據(jù)叠蝇,注意: 傳入的 tempWidget 必須實(shí)現(xiàn)或繼承父類 ATempWidget
///生成打印的模板 Widget 需要繼承這個(gè)類
mixin ATempWidget {
//生成圖片的縮放倍數(shù)
double get pixelRatio => 1;
//需要生成的票據(jù)像素寬度
int get pixelPagerWidth;
//需要生成的票據(jù)像素高度
int get pixelPagerHeight => -1;
}
示例1:創(chuàng)建一個(gè) 寬45mm,高70mm 的標(biāo)簽?zāi)0澹?/h5>
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
// ignore: depend_on_referenced_packages
import 'package:print_image_generate_tool/print_image_generate_tool.dart';
// 標(biāo)簽樣式 demo
class LabelTemp extends StatelessWidget with ATempWidget {
final String data;
const LabelTemp(this.data, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Text(data),
);
}
@override
int get pixelPagerWidth => 360; //1mm對應(yīng)8像素年缎,45mm 對應(yīng)像素寬度為 360
@override
int get pixelPagerHeight => 560; //1mm對應(yīng)8像素悔捶,70mm 對應(yīng)像素高度為 560
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
// ignore: depend_on_referenced_packages
import 'package:print_image_generate_tool/print_image_generate_tool.dart';
// 標(biāo)簽樣式 demo
class LabelTemp extends StatelessWidget with ATempWidget {
final String data;
const LabelTemp(this.data, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Text(data),
);
}
@override
int get pixelPagerWidth => 360; //1mm對應(yīng)8像素年缎,45mm 對應(yīng)像素寬度為 360
@override
int get pixelPagerHeight => 560; //1mm對應(yīng)8像素悔捶,70mm 對應(yīng)像素高度為 560
}
業(yè)務(wù)方觸發(fā)打印任務(wù)時(shí),使用以下代碼將票據(jù)模板轉(zhuǎn)成圖層數(shù)據(jù)
// 生成打印圖層任務(wù)单芜,指定任務(wù)類型為標(biāo)簽
PictureGeneratorProvider.instance.addPicGeneratorTask(
PicGenerateTask<PrinterInfo>(
tempWidget: LabelTemp('標(biāo)簽內(nèi)容') as ATempWidget,
printTypeEnum: PrintTypeEnum.label, //標(biāo)識是標(biāo)簽
params: printerInfo,
),
);
示例2:創(chuàng)建一個(gè) 寬80mm 的小票模板:
// 小票樣式 demo
class ReceiptTemp extends StatelessWidget with ATempWidget {
final String data;
const ReceiptTemp(this.data, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Text(data),
);
}
@override
int get pixelPagerWidth => 550;
// 小票不限制高度蜕该,pixelPagerHeight = -1 就行,無需重寫
}
業(yè)務(wù)方觸發(fā)打印任務(wù)時(shí)洲鸠,使用以下代碼將票據(jù)模板轉(zhuǎn)成圖層數(shù)據(jù)
// 生成打印圖層任務(wù)堂淡,指定任務(wù)類型為小票
PictureGeneratorProvider.instance.addPicGeneratorTask(
PicGenerateTask<PrinterInfo>(
tempWidget: ReceiptTemp('小票內(nèi)容') as ATempWidget,
printTypeEnum: PrintTypeEnum.receipt,
params: printerInfo,
),
);
步驟二:打印圖層轉(zhuǎn)打印指令集
將 widget 轉(zhuǎn)換打印圖層后,我們需要將 Uint8List 數(shù)據(jù)轉(zhuǎn)換為打印機(jī)可識別的數(shù)據(jù)類型扒腕,ESC 對應(yīng)小票機(jī)绢淀,TSC 對應(yīng)標(biāo)簽機(jī)。
使用 flutter_printer_plus 開源庫進(jìn)行功能實(shí)現(xiàn):
// 轉(zhuǎn) TSC 字節(jié)瘾腰,imageBytes 類型為 Uint8List
var printData = await PrinterCommandTool.generatePrintCmd(
imgData: imageBytes,
printType: PrintTypeEnum.label,
);
// 轉(zhuǎn) ESC 字節(jié)皆的,imageBytes 類型為 Uint8List
var printData = await PrinterCommandTool.generatePrintCmd(
imgData: imageBytes,
printType: PrintTypeEnum.receipt,
);
步驟三:目標(biāo)設(shè)備發(fā)送數(shù)據(jù)
可以自行擴(kuò)展實(shí)現(xiàn) write 方法,也可以使用 flutter_printer_plus 內(nèi)提供的方式蹋盆。目前已提供的實(shí)現(xiàn)如下:
- USB 傳輸 (USB打印機(jī))
flutter_printer_plus 提供獲取當(dāng)前已連接的打印機(jī)列表费薄,列表內(nèi)每一個(gè)元素類型為 usbDevice。
// usb 打印
final conn = UsbConn(usbDevice);
conn.writeMultiBytes(printData, 1024 * 3);
- IP 傳輸(網(wǎng)口打印機(jī))
example 內(nèi)提供獲取局域網(wǎng)內(nèi)可用打印機(jī)樣例
// IP 打印
final conn = NetConn(ip);
conn.writeMultiBytes(printData);
結(jié)合以上三個(gè)步驟栖雾,我們在步驟一內(nèi)接收 Uint8List 后可以這么寫:
//打印圖層生成成功
Future<void> _onPictureGenerated(PicGenerateResult data) async {
final imageBytes = data.data;
final printTask = data.taskItem;
//獲取指定的目標(biāo)打印機(jī)
final printerInfo = printTask.params as PrinterInfo;
//打印票據(jù)類型(標(biāo)簽楞抡、小票)
final printTypeEnum = printTask.printTypeEnum;
if (imageBytes != null) {
// Uint8List 轉(zhuǎn)小票或標(biāo)簽指令集
var printData = await PrinterCommandTool.generatePrintCmd(
imgData: imageBytes,
printType: printTypeEnum,
);
// 發(fā)送打印指令
if (printerInfo.isUsbPrinter) {
// usb 打印
final conn = UsbConn(printerInfo.usbDevice!);
conn.writeMultiBytes(printData, 1024 * 3);
} else if (printerInfo.isNetPrinter) {
// 網(wǎng)絡(luò) 打印
final conn = NetConn(printerInfo.ip!);
conn.writeMultiBytes(printData);
}
}
}
完整流程級具體實(shí)現(xiàn)邏輯可參考 example 示例 。
建議使用者將上層進(jìn)行封裝(維護(hù)隊(duì)列)析藕,打印圖層生成成功后先將圖像保存本地召廷,等待上一個(gè)打印任務(wù)結(jié)束后再從隊(duì)列中獲取本地圖片進(jìn)行下一個(gè)打印任務(wù),避免造成內(nèi)存抖動。
附上 demo 打印的小票樣式
疑難點(diǎn)記錄
1. 打印的票據(jù)柱恤,一半正常数初,一半顯示亂碼
原因:數(shù)據(jù)太大導(dǎo)致打印機(jī)內(nèi)存溢出輸出亂碼找爱。
處理:對圖片進(jìn)行分割處理梗顺,分成n個(gè)小段進(jìn)行打印 長圖切割實(shí)現(xiàn)方式參考
2. 打印機(jī)打印票據(jù),出票緩慢车摄,聲音卡頓
原因:打印的位圖寬度太大寺谤。
處理:將位圖寬度進(jìn)行縮小,問題解決吮播。以 80mm 寬度小票為例变屁,通常 1mm 等于 8個(gè)像素,因?yàn)閷挾冗m配在實(shí)際打印中有偏差意狠,不宜設(shè)置為完全吻合因此粟关,適合小票打印機(jī)打印的圖片像素尺寸應(yīng)為:558px、372px环戈。
開源工具庫
print_image_generate_tool:提供 widget 轉(zhuǎn)圖像數(shù)據(jù)(Uint8List)能力闷板;
flutter_printer_plus:提供圖像數(shù)據(jù)(Uint8List) 轉(zhuǎn) TSC 、ESC能力院塞,提供 ip遮晚、usb 打印支持;
esc_utils:小票打印機(jī)數(shù)據(jù)轉(zhuǎn)換工具拦止,提供圖像數(shù)據(jù)(Uint8List) 轉(zhuǎn) ESC 能力县遣;
tsc_utils:標(biāo)簽打印機(jī)數(shù)據(jù)轉(zhuǎn)換工具,提供圖像數(shù)據(jù)(Uint8List) 轉(zhuǎn) TSC 能力汹族;
android_usb_printer:flutter android 端插件萧求,提供 usb 打印機(jī) 搜索、寫入 等能力顶瞒;