2020-02-04
關鍵字:通過代碼繪制POS機小票、快遞單小票、收銀小票、自定義繪制Bitmap
話不多說,直接上效果圖:
這種收銀小票,由於它的格式排版的多元化,是不可能有什么公用模板可以讓我們只是簡單地輸入一些信息就自動生成並排版好的。它的本質就是一張張的圖片。 我們需要將要打印的信息准備好,然后創建一張尺寸合適的空白圖片,再在這張圖片上像畫畫一樣一點一點地將要打印的信息塗繪上去,最終成畫。它的原理就是這么簡單且粗暴。本篇文章所述功能的完整源碼將於文末貼出。
而在 Android 中,我們往往會通過創建空白 Bitmap 再結合以 Canvas 和 Paint 來實現塗繪工作。
這篇文章就來記述一下像上面貼出的那種小票圖片的繪制過程。
在繪圖之前我們首先肯定得先准備好畫布、畫筆。畫筆我們得准備好各種顏色的、各種上字號的、各種粗細程度的,我們日常畫畫的時候往往都會准備一大堆畫筆。但在軟件編程中,其實我們本着節約內存的目的,只需要創建一支畫筆對象就可以了。在后續需要用到不同類型的畫筆時再實時切換畫筆屬性也可以的。再一個就是畫布,在繪畫之前我們必須確定好畫布的大小,在現實生活中,中途更換畫布尺寸幾乎是不可能的。在軟件編程中中途更換畫布也比較麻煩,所以我們往往會在繪畫前就確定要畫布的大小。
由於繪制這副小票圖片需要根據文字的大小等屬性信息來輔助確認畫布尺寸。因此,首先定義好畫筆:
//通用畫筆。 Paint commonPaint = new Paint(); commonPaint.setColor(Color.BLACK); commonPaint.setAntiAlias(true); //運單信息列表貨物重量畫筆。 Paint goodsWeightPaint = new Paint(); goodsWeightPaint.setColor(Color.BLACK); goodsWeightPaint.setTextSize(14); goodsWeightPaint.setTypeface(Typeface.DEFAULT_BOLD); goodsWeightPaint.setAntiAlias(true);
嚴格來講,只需要定義一支畫筆實例即可。但筆者為了方便,還是定義了兩支。第二支畫筆中專用畫筆。
其次是定義基礎尺寸:
final int BOTTOM_WHITE_PADDING = 80; //打印紙底部的留白,為了方便打印完后直接撕打印紙而做的留白。 final int BORDER_PADDING = 2; //留一點邊距。 final int IMG_WIDTH = 380; // max 380pix final int HEADER_TEXT_LEFT_PADDING = 20; // 小票信息頭左邊距。 final int HEADER_TEXT_MAX_LENGTH_PER_LINE = IMG_WIDTH - HEADER_TEXT_LEFT_PADDING - HEADER_TEXT_LEFT_PADDING; //小票信息頭一行文本最大顯示長度。 final int WAYBILL_LIST_LEFT_PADDING = 10; // 小票內容列表左邊距。
上面的基礎尺寸定義中還差一個畫布高度沒有定義。畫布高度是動態確定的,要根據小票內容的多寡來確定。筆者這張小票的內容是物流訂單信息,確定畫布的高度的重點就是計算小票信息頭的總高度以及訂單內容總高度。筆者的小票信息頭中記載了有收件地址與轉發地址。由於地址的長度是不固定的,因此要考慮到文字超長而換行的情況:
//轉發地址高度計算。 int forwardAddrHeight = 0; if(pi.getForwardAddr() == null || pi.getForwardAddr().isEmpty()) { Logger.d(TAG, "No forward address found."); }else{ commonPaint.setTextSize(15); int faw = getTextWidth(commonPaint, pi.getForwardAddr()); if(faw > HEADER_TEXT_MAX_LENGTH_PER_LINE){ //轉發地址一行放不下。 forwardAddrHeight = (int) Math.ceil(((float)faw / (float)HEADER_TEXT_MAX_LENGTH_PER_LINE)); forwardAddrHeight = forwardAddrHeight > 3 ? 3 : forwardAddrHeight; //最大不超過 3 行。 Logger.d(TAG, "Forward address can be divide into " + forwardAddrHeight + " lines."); }else{ //轉發地址比較短,一行就能放下了。 Logger.d(TAG, "Forward address only need 1 line to display."); forwardAddrHeight = 1; } commonPaint.setTextSize(15); forwardAddrHeight = (int) (forwardAddrHeight * (commonPaint.descent() - commonPaint.ascent())); forwardAddrHeight += 10; Logger.d(TAG, "Forward address display height:" + forwardAddrHeight); } //收貨地址高度計算。 int revAddrHeight = 0; if(pi.getRevAddr() == null || pi.getRevAddr().isEmpty()) { Logger.d(TAG, "No receive address found."); }else{ int faw = getTextWidth(commonPaint, pi.getRevAddr()); if(faw > HEADER_TEXT_MAX_LENGTH_PER_LINE){ //收件地址一行放不下。 revAddrHeight = (int) Math.ceil(((float)faw / (float)HEADER_TEXT_MAX_LENGTH_PER_LINE)); revAddrHeight = revAddrHeight > 3 ? 3 : revAddrHeight; //最大不超過 3 行。 Logger.d(TAG, "Receive address can be divide into " + revAddrHeight + " lines."); }else{ //收件地址比較短,一行就能放下了。 Logger.d(TAG, "Receive address only need 1 line to display."); revAddrHeight = 1; } commonPaint.setTextSize(15); revAddrHeight = (int) (revAddrHeight * (commonPaint.descent() - commonPaint.ascent())); revAddrHeight += 10; Logger.d(TAG, "Receive address display height:" + revAddrHeight); } commonPaint.setTextSize(14); //化身為運單列表畫筆,以供計算小票高度。 final int IMG_HEIGHT = 220 //基本信息頭固定高度。 + (int)((commonPaint.descent() - commonPaint.ascent() + 10) * poders.size()) //運單列表所需要的高度。 + BOTTOM_WHITE_PADDING //尾部留白,以方便用戶打印后直接撕取。 + forwardAddrHeight //轉發地址高度。 + revAddrHeight; //收件地址高度。
上述代碼標粗加紅的方法的實現如下:
private int getTextWidth(Paint paint, String txt){ Logger.v(TAG, "getTextWidth()"); if(paint != null && txt != null) { return (int) Math.ceil(paint.measureText(txt)); } return 0; } //getTextWidth() -- end
最后就是創建對應尺寸的空白 Bitmap 畫布了:
//創建小票空白位圖,以供后續填充內容。 Bitmap bitmap = Bitmap.createBitmap(IMG_WIDTH, IMG_HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawColor(Color.WHITE);
接下來就可以開始繪制小票內容了。
在繪制之前,筆者創建了“一把游標”,用來記錄當前繪制高度。因為筆者是按照內容從上至下繪制的。外邊框最后繪制。
int printHeight = BORDER_PADDING; // padding top.
剛開始時標尺定位到邊界線處,即小票外邊框的上界處。
其次就是繪制內容了,這塊其實沒什么好講的,就是利用 Paint 來操作 Canvas 而已:
//第一行數據。 commonPaint.setTextSize(15); printHeight += 5 + commonPaint.descent() - commonPaint.ascent(); if(ScannerApplication.getInstance().getLanguageHelper().isThaiLanguage()) { canvas.drawText(context.getString(R.string.activity_main_cdjh_print_line1), 35, printHeight, commonPaint); canvas.drawText(pi.getPrintTime(), 230, printHeight, commonPaint); }else{ canvas.drawText(context.getString(R.string.activity_main_cdjh_print_line1), 60, printHeight, commonPaint); canvas.drawText(pi.getPrintTime(), 200, printHeight, commonPaint); } //第二行數據。收件人信息 commonPaint.setTextSize(20); commonPaint.setTypeface(Typeface.DEFAULT_BOLD); printHeight += 45; canvas.drawText(pi.getUsername(), HEADER_TEXT_LEFT_PADDING, printHeight, commonPaint); //第三行數據。提貨方式。 commonPaint.setTextSize(17); commonPaint.setTypeface(Typeface.DEFAULT); printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(pi.getRevType(), HEADER_TEXT_LEFT_PADDING, printHeight, commonPaint);
每繪制一處內容,按需設置一下畫筆屬性並調整一下“游標”的高度。Canvas 處的位置就靠自己微調來確定就好了。
條形碼筆者是直接使用 zxing 提供的功能來將文本轉換成條形碼圖片,再將條形碼圖片加載到畫布上去的:
//條形碼。 Bitmap barcodebm = makeBarcodeBitmap(pi.getOrderNo(), 260, 70); canvas.drawBitmap(barcodebm, 140, printHeight - 60, commonPaint);
makeBarcodeBitmap() 方法封裝的就是操作 zxing 的接口將文本生成條形碼 Bitmap 的代碼,具體請同學參閱文末的完整源碼即可。
這里需要額外提一點。強烈不建議通過 Android 控件來設置文本內容,將控件直接生成 Bitmap 來繪制在小票畫布上。例如:筆者發現有些同學喜歡用 TextView 控件來顯示文字,並通過 TextView 實例的 getDrawingCache() 來獲取 Bitmap 從而實現文本在畫布上的快速繪制。這種方式兼容性非常差。因為控件的內容是受系統控制的,在設置中可以設置顯示字體的大小,有什么小號、普通、大號、超大號等。控件中的文字字號是會隨着系統顯示字號的不同而改變的。這就會導致你用控件繪制的文本內容在你的設備上看起來很合適,但換到其它人的設備就出問題了。因此,為了保證一致性,至少在繪制小票文本時要統一使用 Canvas 的 drawText() 方法,並經由 Paint 的 setTextSize() 來控制文字尺寸。
然后還有一個需要注意的地方就是超長文本換行。
Canvas 的 drawText() 方法默認是不會為你的超長文本自動換行的。我們需要自行實現。筆者自己實現了這么一個算法,不說這個算法好不好,至少它能用:
//有收件地址,要智能顯示。 commonPaint.setTextSize(15); printHeight += commonPaint.descent() - commonPaint.ascent(); printHeight = printMultiLinesText(canvas, commonPaint, pi.getRevAddr(), printHeight, HEADER_TEXT_MAX_LENGTH_PER_LINE, HEADER_TEXT_LEFT_PADDING); /** * 根據文本長度自動將文本分行打印在小票上。 * @return 最新的打印高度。 * */ private int printMultiLinesText(Canvas canvas, Paint commonPaint, String txt, int printHeight, int maxLenPerLine, int leftPadding){ Logger.v(TAG, "printMultiLinesText()"); //計算文本長度。 int textWidth = getTextWidth(commonPaint, txt); Logger.i(TAG, "textWidth:" + textWidth + ",maxLenPerLine:" + maxLenPerLine); if(textWidth > maxLenPerLine){ //文本超過一行,裁剪后顯示。最大允許顯示三行。 //最長三行。超過的部分不顯示。 int lines = (int) Math.ceil((float)textWidth / (float)maxLenPerLine); Logger.d(TAG, "how many line can be divide? " + lines); if(lines == 2){ //從尾部逐漸縮小字符串。 int cut = 2; // 2個字符 2 個字符的裁剪。 while(true){ int slen = getTextWidth(commonPaint, txt.substring(0, txt.length() - cut)); if(slen <= maxLenPerLine){ break; } cut += 2; } //打印第一行文本。 canvas.drawText(txt.substring(0, txt.length() - cut), leftPadding, printHeight, commonPaint); //打印第二行文本。 printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(txt.substring(txt.length() - cut), leftPadding, printHeight, commonPaint); }else if(lines > 2){ int cut = 2; while(true){ int slen = getTextWidth(commonPaint, txt.substring(0, txt.length() - cut)); if(slen <= maxLenPerLine){ break; } cut += 2; } //打印第一行。 canvas.drawText(txt.substring(0, txt.length() - cut), leftPadding, printHeight, commonPaint); //打印第二行。 printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(txt.substring(txt.length() - cut, 2 * (txt.length() - cut)), leftPadding, printHeight, commonPaint); //打印第三行。 printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(txt.substring(2 * (txt.length() - cut)), leftPadding, printHeight, commonPaint); }else if(lines == 1){ Logger.w(TAG, "Single line forward address cannot be run here."); canvas.drawText(txt, leftPadding, printHeight, commonPaint); }else{ Logger.e(TAG, "Invalid lines:" + lines); } }else{ //只有一行,直接顯示。 canvas.drawText(txt, leftPadding, printHeight, commonPaint); } return printHeight; } // printMultiLinesText() -- end.
上面這個算法的邏輯也算簡單:直接計算文本的長度是否超出畫布允許的寬度,若超了,則從尾部裁掉 2 個字符再次測量,直至文本長度適合為止。那些被裁出去的文本將會被打印到下一行。
在所有小票內容都繪制完成后就是外邊框的繪制了,這個就簡單了,Canvas 的 drawLine() 直接搞定:
//四周邊界。 commonPaint.setStrokeWidth(1); commonPaint.setStyle(Paint.Style.STROKE); int bwhite = (int) (BOTTOM_WHITE_PADDING / 1.5f); //四周邊框只圍住打印內容,底部的留白不要圍以方便用戶撕打印紙。 canvas.drawLine(BORDER_PADDING, BORDER_PADDING, IMG_WIDTH - BORDER_PADDING, BORDER_PADDING, commonPaint); canvas.drawLine(IMG_WIDTH - BORDER_PADDING, BORDER_PADDING, IMG_WIDTH - BORDER_PADDING, IMG_HEIGHT - BORDER_PADDING - bwhite, commonPaint); canvas.drawLine(IMG_WIDTH - BORDER_PADDING, IMG_HEIGHT - BORDER_PADDING - bwhite, BORDER_PADDING, IMG_HEIGHT - BORDER_PADDING - bwhite, commonPaint); canvas.drawLine(BORDER_PADDING, IMG_HEIGHT - BORDER_PADDING - bwhite, BORDER_PADDING, BORDER_PADDING, commonPaint);
如此,文首示例圖片樣式的小票就完成了。
最后來加點餐,我們來看看畫筆 Paint 的抗鋸齒效果。筆者是建議不開啟抗鋸齒功能的,因為不開抗鋸齒生成的圖片看上去更像真實的打印小票。畫筆的抗鋸齒設置如下所示,就一行代碼:
//訂單信息頭畫筆。 Paint commonPaint = new Paint(); commonPaint.setColor(Color.BLACK); commonPaint.setAntiAlias(true);
以下是抗鋸齒開啟與關閉時的小票效果圖:
當然,這種主觀感受的東西還是各位見仁見智了。
下面貼出完整源碼:

package com.jarwen.scanner.scanner; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.os.Environment; import android.widget.ImageView; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.jarwen.scanner.R; import com.jarwen.scanner.ScannerApplication; import com.jarwen.scanner.data.model.PrintInfo; import com.jarwen.scanner.util.Hardware; import com.jarwen.scanner.util.Logger; import com.jarwen.scanner.util.ToastManager; import java.io.File; import java.io.FileOutputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; public abstract class Scanner{ private static final String TAG = "Scanner"; protected Context context; protected ToastManager tm; protected Scanner(Context context, ToastManager tm){ this.context = context; this.tm = tm; } /* * 把訂單信息排版成打印紙形式。 * chorm,2020-01-27 16:54 / 2020-01-29 19:14 * */ protected Bitmap makePrintImage(PrintInfo pi){ Logger.v(TAG, "makePrintImage()"); try{ List<PrintInfo.Order> poders = pi.getWaybills(); if(poders == null || poders.size() == 0) { tm.toast(false, context.getString(R.string.activity_main_cdjh_print_no_waybill)); return null; } //訂單信息頭畫筆。 Paint commonPaint = new Paint(); commonPaint.setColor(Color.BLACK); commonPaint.setAntiAlias(true); //運單信息列表貨物重量畫筆。 Paint goodsWeightPaint = new Paint(); goodsWeightPaint.setColor(Color.BLACK); goodsWeightPaint.setTextSize(14); goodsWeightPaint.setTypeface(Typeface.DEFAULT_BOLD); goodsWeightPaint.setAntiAlias(true); final int BOTTOM_WHITE_PADDING = 80; //打印紙底部的留白,為了方便打印完后直接撕打印紙而做的留白。 final int BORDER_PADDING = 2; //留一點邊距。 final int IMG_WIDTH = 380; // max 380pix final int HEADER_TEXT_LEFT_PADDING = 20; final int HEADER_TEXT_MAX_LENGTH_PER_LINE = IMG_WIDTH - HEADER_TEXT_LEFT_PADDING - HEADER_TEXT_LEFT_PADDING; //小票信息頭一行文本最大顯示長度。 final int WAYBILL_LIST_LEFT_PADDING = 10; //轉發地址高度計算。 int forwardAddrHeight = 0; if(pi.getForwardAddr() == null || pi.getForwardAddr().isEmpty()) { Logger.d(TAG, "No forward address found."); }else{ commonPaint.setTextSize(15); int faw = getTextWidth(commonPaint, pi.getForwardAddr()); if(faw > HEADER_TEXT_MAX_LENGTH_PER_LINE){ //轉發地址一行放不下。 forwardAddrHeight = (int) Math.ceil(((float)faw / (float)HEADER_TEXT_MAX_LENGTH_PER_LINE)); forwardAddrHeight = forwardAddrHeight > 3 ? 3 : forwardAddrHeight; //最大不超過 3 行。 Logger.d(TAG, "Forward address can be divide into " + forwardAddrHeight + " lines."); }else{ //轉發地址比較短,一行就能放下了。 Logger.d(TAG, "Forward address only need 1 line to display."); forwardAddrHeight = 1; } commonPaint.setTextSize(15); forwardAddrHeight = (int) (forwardAddrHeight * (commonPaint.descent() - commonPaint.ascent())); forwardAddrHeight += 10; Logger.d(TAG, "Forward address display height:" + forwardAddrHeight); } //收貨地址高度計算。 int revAddrHeight = 0; if(pi.getRevAddr() == null || pi.getRevAddr().isEmpty()) { Logger.d(TAG, "No receive address found."); }else{ int faw = getTextWidth(commonPaint, pi.getRevAddr()); if(faw > HEADER_TEXT_MAX_LENGTH_PER_LINE){ //收件地址一行放不下。 revAddrHeight = (int) Math.ceil(((float)faw / (float)HEADER_TEXT_MAX_LENGTH_PER_LINE)); revAddrHeight = revAddrHeight > 3 ? 3 : revAddrHeight; //最大不超過 3 行。 Logger.d(TAG, "Receive address can be divide into " + revAddrHeight + " lines."); }else{ //收件地址比較短,一行就能放下了。 Logger.d(TAG, "Receive address only need 1 line to display."); revAddrHeight = 1; } commonPaint.setTextSize(15); revAddrHeight = (int) (revAddrHeight * (commonPaint.descent() - commonPaint.ascent())); revAddrHeight += 10; Logger.d(TAG, "Receive address display height:" + revAddrHeight); } commonPaint.setTextSize(14); //化身為運單列表畫筆,以供計算小票高度。 final int IMG_HEIGHT = 220 //基本信息頭固定高度。 + (int)((commonPaint.descent() - commonPaint.ascent() + 10) * poders.size()) //運單列表所需要的高度。 + BOTTOM_WHITE_PADDING //尾部留白,以方便用戶打印后直接撕取。 + forwardAddrHeight //轉發地址高度。 + revAddrHeight; //收件地址高度。 //創建小票空白位圖,以供后續填充內容。 Bitmap bitmap = Bitmap.createBitmap(IMG_WIDTH, IMG_HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawColor(Color.WHITE); int printHeight = BORDER_PADDING; // padding top. //第一行數據。 commonPaint.setTextSize(15); printHeight += 5 + commonPaint.descent() - commonPaint.ascent(); if(ScannerApplication.getInstance().getLanguageHelper().isThaiLanguage()) { canvas.drawText(context.getString(R.string.activity_main_cdjh_print_line1), 35, printHeight, commonPaint); canvas.drawText(pi.getPrintTime(), 230, printHeight, commonPaint); }else{ canvas.drawText(context.getString(R.string.activity_main_cdjh_print_line1), 60, printHeight, commonPaint); canvas.drawText(pi.getPrintTime(), 200, printHeight, commonPaint); } //第二行數據。收件人信息 commonPaint.setTextSize(20); commonPaint.setTypeface(Typeface.DEFAULT_BOLD); printHeight += 45; canvas.drawText(pi.getUsername(), HEADER_TEXT_LEFT_PADDING, printHeight, commonPaint); //第三行數據。提貨方式。 commonPaint.setTextSize(17); commonPaint.setTypeface(Typeface.DEFAULT); printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(pi.getRevType(), HEADER_TEXT_LEFT_PADDING, printHeight, commonPaint); //條形碼。 Bitmap barcodebm = makeBarcodeBitmap(pi.getOrderNo(), 260, 70); canvas.drawBitmap(barcodebm, 140, printHeight - 60, commonPaint); //條形碼文字的繪制。chorm,2020-01-29 21:15 commonPaint.setTextSize(25); int barcodeX = 140 + 130 - (int)(commonPaint.measureText(pi.getOrderNo()) / 2.0f); canvas.drawText(pi.getOrderNo(), barcodeX, printHeight + 5 + commonPaint.descent() - commonPaint.ascent(), commonPaint); //第四行數據。轉發地址,可能有多行數據。 printHeight += 40; //跳過條形碼的高度。 if(pi.getForwardAddr() == null || pi.getForwardAddr().isEmpty()){ //沒有轉發地址。不顯示也不占據空間。 }else{ //有轉發地址,要顯示。 commonPaint.setTextSize(15); printHeight += commonPaint.descent() - commonPaint.ascent(); printHeight = printMultiLinesText(canvas, commonPaint, pi.getForwardAddr(), printHeight, HEADER_TEXT_MAX_LENGTH_PER_LINE, HEADER_TEXT_LEFT_PADDING); } //第五行數據,收件人姓名。 commonPaint.setTextSize(18); printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(String.format(Locale.US, "%s %s", pi.getReceiver(), pi.getPhone()), HEADER_TEXT_LEFT_PADDING, printHeight, commonPaint); //第六行數據,收貨地址。 printHeight += 10; if(pi.getRevAddr() == null || pi.getRevAddr().isEmpty()){ //沒有收件地址,不顯示也不占據空間。 }else{ //有收件地址,要智能顯示。 commonPaint.setTextSize(15); printHeight += commonPaint.descent() - commonPaint.ascent(); printHeight = printMultiLinesText(canvas, commonPaint, pi.getRevAddr(), printHeight, HEADER_TEXT_MAX_LENGTH_PER_LINE, HEADER_TEXT_LEFT_PADDING); } //第七行,分隔線。 printHeight += 10; commonPaint.setStyle(Paint.Style.STROKE); commonPaint.setStrokeWidth(1); canvas.drawLine(10, printHeight, IMG_WIDTH - 10, printHeight, commonPaint); //運單數據列表區。 commonPaint.setTextSize(14); commonPaint.setTypeface(Typeface.DEFAULT); commonPaint.setStyle(Paint.Style.FILL); for(PrintInfo.Order po:poders){ printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); //第 1 列。 canvas.drawText(po.lot, WAYBILL_LIST_LEFT_PADDING, printHeight, commonPaint); //第 2 列。 canvas.drawText(po.waybillNo, 110, printHeight, commonPaint); //第 3 列。 canvas.drawText(po.weight + "kg", 260, printHeight, goodsWeightPaint); //第 4 列。 canvas.drawText(po.goodsType, 320, printHeight, commonPaint); //第 5 列。 canvas.drawText(String.valueOf(pi.getWaybills().get(0).amount), 350, printHeight, commonPaint); } //四周邊界。 commonPaint.setStrokeWidth(1); commonPaint.setStyle(Paint.Style.STROKE); int bwhite = (int) (BOTTOM_WHITE_PADDING / 1.5f); //四周邊框只圍住打印內容,底部的留白不要圍以方便用戶撕打印紙。 canvas.drawLine(BORDER_PADDING, BORDER_PADDING, IMG_WIDTH - BORDER_PADDING, BORDER_PADDING, commonPaint); canvas.drawLine(IMG_WIDTH - BORDER_PADDING, BORDER_PADDING, IMG_WIDTH - BORDER_PADDING, IMG_HEIGHT - BORDER_PADDING - bwhite, commonPaint); canvas.drawLine(IMG_WIDTH - BORDER_PADDING, IMG_HEIGHT - BORDER_PADDING - bwhite, BORDER_PADDING, IMG_HEIGHT - BORDER_PADDING - bwhite, commonPaint); canvas.drawLine(BORDER_PADDING, IMG_HEIGHT - BORDER_PADDING - bwhite, BORDER_PADDING, BORDER_PADDING, commonPaint); AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setPositiveButton("OK", null); ImageView iv = new ImageView(context); iv.setScaleType(ImageView.ScaleType.FIT_CENTER); iv.setImageBitmap(bitmap); builder.setView(iv); builder.setCancelable(false); builder.create().show(); return bitmap; }catch(Exception e){ e.printStackTrace(); } return null; } // makePrintImage() -- end private Bitmap makeBarcodeBitmap(String contents, int desiredWidth, int desiredHeight) throws Exception { Logger.v(TAG, "makeBarcodeBitmap()"); //條形碼圖片 final int WHITE = 0xFFFFFFFF; final int BLACK = 0xFF000000; HashMap<EncodeHintType, String> hints = new HashMap<>(2); String encoding = "UTF-8"; hints.put(EncodeHintType.CHARACTER_SET, encoding); MultiFormatWriter writer = new MultiFormatWriter(); BitMatrix result = writer.encode(contents, BarcodeFormat.CODE_128, desiredWidth, desiredHeight, hints); int width = result.getWidth(); int height = result.getHeight(); int[] pixels = new int[width * height]; // All are 0, or black, by default for (int y = 0; y < height; y++) { int offset = y * width; for (int x = 0; x < width; x++) { pixels[offset + x] = result.get(x, y) ? BLACK : WHITE; } } Bitmap barcode = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); barcode.setPixels(pixels, 0, width, 0, 0, width, height); return barcode; } private int getTextWidth(Paint paint, String txt){ Logger.v(TAG, "getTextWidth()"); if(paint != null && txt != null) { return (int) Math.ceil(paint.measureText(txt)); } return 0; } //getTextWidth() -- end /** * 根據文本長度自動將文本分行打印在小票上。 * @return 最新的打印高度。 * */ private int printMultiLinesText(Canvas canvas, Paint commonPaint, String txt, int printHeight, int maxLenPerLine, int leftPadding){ Logger.v(TAG, "printMultiLinesText()"); //計算文本長度。 int textWidth = getTextWidth(commonPaint, txt); Logger.i(TAG, "textWidth:" + textWidth + ",maxLenPerLine:" + maxLenPerLine); if(textWidth > maxLenPerLine){ //文本超過一行,裁剪后顯示。最大允許顯示三行。 //最長三行。超過的部分不顯示。 int lines = (int) Math.ceil((float)textWidth / (float)maxLenPerLine); Logger.d(TAG, "how many line can be divide? " + lines); if(lines == 2){ //從尾部逐漸縮小字符串。 int cut = 2; // 2個字符 2 個字符的裁剪。 while(true){ int slen = getTextWidth(commonPaint, txt.substring(0, txt.length() - cut)); if(slen <= maxLenPerLine){ break; } cut += 2; } //打印第一行文本。 canvas.drawText(txt.substring(0, txt.length() - cut), leftPadding, printHeight, commonPaint); //打印第二行文本。 printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(txt.substring(txt.length() - cut), leftPadding, printHeight, commonPaint); }else if(lines > 2){ int cut = 2; while(true){ int slen = getTextWidth(commonPaint, txt.substring(0, txt.length() - cut)); if(slen <= maxLenPerLine){ break; } cut += 2; } //打印第一行。 canvas.drawText(txt.substring(0, txt.length() - cut), leftPadding, printHeight, commonPaint); //打印第二行。 printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(txt.substring(txt.length() - cut, 2 * (txt.length() - cut)), leftPadding, printHeight, commonPaint); //打印第三行。 printHeight += 10 + commonPaint.descent() - commonPaint.ascent(); canvas.drawText(txt.substring(2 * (txt.length() - cut)), leftPadding, printHeight, commonPaint); }else if(lines == 1){ Logger.w(TAG, "Single line forward address cannot be run here."); canvas.drawText(txt, leftPadding, printHeight, commonPaint); }else{ Logger.e(TAG, "Invalid lines:" + lines); } }else{ //只有一行,直接顯示。 canvas.drawText(txt, leftPadding, printHeight, commonPaint); } return printHeight; } // printMultiLinesText() -- end. /** * 將打印小票保存到 /sdcard。 * chorm, 2020-01-29 19:21 * */ protected void saveTicket(Bitmap bitmap){ Logger.v(TAG, "saveTicket()"); try{ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US); FileOutputStream fos = new FileOutputStream(new File(String.format("%s%s%s%s", Environment.getExternalStorageDirectory().getAbsolutePath(), "/cns_ticket", sdf.format(new Date()), ".png"))); bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); fos.flush(); fos.close(); tm.toast("小票已保存至SD卡"); }catch(Exception e){ e.printStackTrace(); } } }