Android 如何通过代码绘制小票单据

时间:2021-06-08 18:01:57

2020-02-04

关键字:通过代码绘制POS机小票、快递单小票、收银小票、自定义绘制Bitmap

 

话不多说,直接上效果图:

Android 如何通过代码绘制小票单据

Android 如何通过代码绘制小票单据

 

这种收银小票,由于它的格式排版的多元化,是不可能有什么公用模板可以让我们只是简单地输入一些信息就自动生成并排版好的。它的本质就是一张张的图片。 我们需要将要打印的信息准备好,然后创建一张尺寸合适的空白图片,再在这张图片上像画画一样一点一点地将要打印的信息涂绘上去,最终成画。它的原理就是这么简单且粗暴。本篇文章所述功能的完整源码将于文末贴出。

 

而在 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);

以下是抗锯齿开启与关闭时的小票效果图:

Android 如何通过代码绘制小票单据

当然,这种主观感受的东西还是各位见仁见智了。

 

下面贴出完整源码:

Android 如何通过代码绘制小票单据Android 如何通过代码绘制小票单据
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();
        }
    }
}
小票绘制源码