使用谷歌的开源库zxing.jar实现二维码扫描功能。
zxing.jar实现二维码扫描的教程网上一搜一大堆,不过大都是千篇一律,而且比较复杂,封装的也不太好。
比如,大多数都会有这么一大堆文件(如下图),来回用handler传消息,很难看懂。布局也基本上是固定的,SurfaceView只能全屏,想要改界面还得先看懂,比较费劲。包里边既有Activity,又有View,又用到了R中的资源。自由度太低了。总的来说,感觉封装的很差劲。
经过学习,总结,最终发现,其实zxing.jar只是提供了二维码数据的解码,扫码过程中图片预览用的是摄像头Camera+SurfaceView,和zxing完全没有关系。
实现打开摄像头,并显示摄像头拍到的画面,只需要调用
camera=Camera.open();//打开摄像头 camera.setPreviewDisplay(surfaceView.getHolder());//用SurfaceView显示画面 camera.startPreview();//开始预览
就这么简单,何必写那一大堆类。
扫面二维码过程中,有一个矩形框,实际识别的是矩形框中的图片。矩形框的尺寸和位置其实是我们自己定义的。
我们只需要定义一个Rect对象,然后再定义一个ViewfinderView类来根据Rect对象画出矩形框和一条扫描线。
这么一来,从表面上看,扫描二维码的app已经做好了,但是最核心的扫描功能还没有加上。
我们需要从摄像头捕获的图像中,获取到我们自己定义的Rect对象区域内的部分,交给zxing.jar来进行解码。
实际上,我们不是从SurfaceView中截图,因为1、从SurfaceView中截图并不简单,2、效率会很差。
为了提高效率,Camera有一个setPreviewCallback方法,设置一个PreviewCallback对象,PreviewCallback对象需要实现一个方法:
public void onPreviewFrame(byte[] arg0, Camera arg1)
其中byte[]就是Camera捕获的图片的数据(水平不够,具体格式不了解)。
实际上,我们是在这里把数据进行转换然后交给zxing进行解码的。
具体转换过程可以参考别人写好的代码。
由于解码耗时较长,大约需要几十毫秒,为了界面看起来比较流畅,需要开一个独立的线程用于解码。
另外,byte[]数组得到的图片的尺寸和surfaceview的尺寸一般不会一样,需要了解一下Camera的一些方法,比如:getParameters().getPreviewSize()方法。
大致思路就是如此。并不需要做的那么复杂。
而且这么封装,自由度也高,可以任意布局。
核心类JcQrCodeScanner.java:
package cn.jucheng.jclibs.qrcode; import java.io.IOException; import java.util.concurrent.CountDownLatch; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.Rect; import android.hardware.Camera; import android.hardware.Camera.PreviewCallback; import android.hardware.Camera.Size; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.view.SurfaceView; import com.google.zxing.MultiFormatReader; /** * 二维码扫描。 * <h3>使用方式:</h3> * <h4>开启扫描:</h4> * <pre> * JcQrCodeScanner.init(context); * JcQrCodeScanner.get().hd=hd; * JcQrCodeScanner.get().openDriver(surfaceView); * JcQrCodeScanner.get().startPreview(); * </pre> * <h4>停止扫描:</h4> * <pre> * if(JcQrCodeScanner.get()!=null){ * JcQrCodeScanner.get().stopPreview(); * JcQrCodeScanner.get().closeDriver(); * JcQrCodeScanner.get().destroy(); * } * </pre> * <p>可以通过{@link #setFramingRect}设置扫描区域。</p> * <p>可以通过{@link #getFramingRect}获取扫描区域,用于显示扫描框。扫描框可参考{@link cn.jucheng.jclibs.qrcode.ViewfinderView},当然你也可以自己定义。</p> * <p>可以获取扫描区域的原始图像{@link #captureFramingBitmap},摄像头捕获的原始图像{@link #captureBitmap}</p> * <h4>注意:</h4> * <p>依赖zxing3.2.1.jar。记得导入jar包,注意版本号。</p> * <p>记得在AndroidManifest.xml中添加Camera权限。</p> * <pre><uses-permission android:name="android.permission.CAMERA"/></pre> * * @author 作者:hanyeah * @date 创建时间:2017-2-16 上午10:34:42 */ public class JcQrCodeScanner { private static final String TAG="JcQrCodeScanner"; /**版本号**/ public static final String VERSION = "1.0.1"; private static JcQrCodeScanner jcQrCodeScanner; private Context context; private Camera camera; private SurfaceView surfaceView; private Rect framingRect; private Rect framingRectInPreview; private Boolean previewing=false; private int previewFormat; /**用于传递消息,比如解码成功。**/ public Handler hd; private String result=""; public static final int DECODE_SUCCESS=1; private JcQrCodeScanner(Context context) { // TODO Auto-generated constructor stub this.context=context; decodeThread=new DecodeThread(); decodeThread.start(); } /** * 获取最近一次解码的结果。 * @return */ public String getReault(){ return result; } /** * 初始化实例。 * @param context 使用该类的Activity对象。 */ public static void init(Context context){ if(jcQrCodeScanner==null){ jcQrCodeScanner=new JcQrCodeScanner(context); } } /** * 获取实例。 * <p>获取之前需要先调用init进行初始化。</p> * @return 该类的实例 */ public static JcQrCodeScanner get(){ return jcQrCodeScanner; } /** * 打开摄像头。 * @param surfaceView 用于显示摄像头捕获的图像的SurfaceView对象。 * @return Boolean */ public Boolean openDriver(SurfaceView surfaceView){ this.surfaceView=surfaceView; if(camera==null){ camera=Camera.open(); if(camera==null){ return false; } try { camera.setPreviewDisplay(surfaceView.getHolder()); previewFormat=camera.getParameters().getPreviewFormat(); cameraSize=camera.getParameters().getPreviewSize(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return true; } /** * 关闭摄像头。 */ public void closeDriver(){ if(camera!=null){ camera.release(); camera=null; } } /** * 开始预览,在之前设置的surfaceView中显示摄像头捕捉到的画面。 */ public void startPreview(){ if(camera!=null&&!previewing){ camera.startPreview(); previewing=true; isSucceed=false; camera.setPreviewCallback(previewCallback); } } /** * 停止预览。 */ public void stopPreview(){ if(camera!=null&&previewing){ camera.setPreviewCallback(null); camera.stopPreview(); previewing=false; } } /** * 彻底销毁。 * 解码线程,对象实例都会销毁。 */ public void destroy(){ stopPreview(); closeDriver(); Message quit = Message.obtain(decodeThread.getHandler(), 0); quit.sendToTarget(); try { decodeThread.join(); } catch (InterruptedException e) { e.printStackTrace(); Log.e(TAG,e.getMessage()); } jcQrCodeScanner=null; } /** * 设置Camera的预览尺寸。Camera的预览尺寸并不是连续的,所以实际值可能会和设置的值不符。 * @param width * @param height */ public void setCameraPreviewSize(int width,int height){ if(camera!=null){ Camera.Parameters par=camera.getParameters(); par.setPreviewSize(width, height); par.setPictureSize(width, height); camera.setParameters(par); cameraSize=camera.getParameters().getPreviewSize(); framingRectInPreview=null; Log.d(TAG, "cameraSize="+cameraSize.width+","+cameraSize.height); } } /** * 设置预览框的矩形区域。 * @param rect */ public void setFramingRect(Rect rect){ framingRect=new Rect(rect); framingRectInPreview=null; } /** * 获取预览框的矩形区域。 * <p>实际显示尺寸。</p> * @return Rect */ public Rect getFramingRect(){ if(framingRect==null){ Point p=new Point(surfaceView.getWidth(), surfaceView.getHeight()); framingRect=new Rect(p.x/4, p.y/4, p.x*3/4, p.y*3/4); } return framingRect; } /** * 获取预览框的矩形区域。 * <p>相对于Camera获取的原始图片的尺寸。</p> * @return */ public Rect getFramingRectInPreview(){ if(framingRectInPreview==null){ Point p=new Point(surfaceView.getWidth(), surfaceView.getHeight()); Rect rect = new Rect(getFramingRect()); rect.left = rect.left * cameraSize.width / p.x; rect.right = rect.right * cameraSize.width / p.x; rect.top = rect.top * cameraSize.height / p.y; rect.bottom = rect.bottom * cameraSize.height / p.y; framingRectInPreview = rect; } return framingRectInPreview; } /** * <p>扫描区域截图。</p> * <p>截取的是扫描区域的原始图像,可能和实际显示的大小不一致。</p> * @return Bitmap */ public Bitmap captureFramingBitmap(){ Rect rect = getFramingRectInPreview(); return JcQrCode.capture(data,previewFormat, cameraSize.width, cameraSize.height, rect.left, rect.top, rect.width(), rect.height()); } /** * <p>Camera截图。</p> * <p>截取的是Camera捕获的原始图像。</p> * <p>也可以考虑用camera.takePicture实现。</p> * @return Bitmap */ public Bitmap captureBitmap(){ return JcQrCode.capture(data,previewFormat, cameraSize.width, cameraSize.height, 0,0,cameraSize.width, cameraSize.height); } /** * 保存Camera捕获的用于解码的图像数据。在PreviewCallback中更新。由于解码比较耗时,所以该数据的更新并不及时。 */ private byte[] data; /** * 是否正在解码 */ private Boolean isDecoding=false; /** * 是否解码成功 */ private Boolean isSucceed=false; /** * Camera的预览尺寸,通过camera.getParameters().getPreviewSize()获取。 */ private Size cameraSize; private PreviewCallback previewCallback=new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] arg0, Camera arg1) { // TODO Auto-generated method stub if(isDecoding||isSucceed){ return; } isDecoding=true; data=arg0; decodeThread.getHandler().sendEmptyMessage(1); } }; /** * 解码成功处理。 * <p>解码线程调用。</p> * @param result */ private void decodeSuccess(String result){ Log.d(TAG, "解码成功:"+result); this.result=result; stopPreview(); closeDriver(); if(hd!=null){ Message msg=hd.obtainMessage(DECODE_SUCCESS); msg.obj=result; hd.sendMessage(msg); } } private final DecodeThread decodeThread; /** * 解码线程。 * <p>解码比较耗时(几十毫秒),需要放到子线程中。</p> * @author 作者:hanyeah * @date 创建时间:2017-2-16 下午3:03:19 */ private class DecodeThread extends Thread{ private final MultiFormatReader multiFormatReader; private Handler handler; private final CountDownLatch handlerInitLatch;//CountDownLatch这个要学习一下。 public DecodeThread() { // TODO Auto-generated constructor stub multiFormatReader = new MultiFormatReader(); handlerInitLatch = new CountDownLatch(1); } Handler getHandler() { try { handlerInitLatch.await(); } catch (InterruptedException ie) { ie.printStackTrace(); Log.e(TAG, ie.getMessage()); // continue? } return handler; } /** * 解码 */ private void decode(){ try{ Rect rect = getFramingRectInPreview(); //Log.d(TAG, rect.toString()); //Log.d(TAG, String.format("%d,%d , %d",cameraSize.width, cameraSize.height,data.length)); String result=JcQrCode.decode(data, cameraSize.width, cameraSize.height, rect.left, rect.top, rect.width(), rect.height()); if(result!=""){ //解码成功,停止扫描,发送成功消息。 isSucceed=true; decodeSuccess(result); } isDecoding=false; } catch(Exception e){ e.printStackTrace(); Log.d(TAG, "解码出错:"+e); } } public void run() { Looper.prepare(); //在线程中创建handler。 handler=new Handler(){ public void handleMessage(Message msg) { switch(msg.what){ case 1: decode(); break; case 0: Looper.myLooper().quit(); break; } }; }; handlerInitLatch.countDown(); Looper.loop(); }; }; }
用到了自己封装的一个工具类JcQrCode.java
package cn.jucheng.jclibs.qrcode; import java.io.ByteArrayOutputStream; import java.util.HashMap; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Rect; import android.graphics.YuvImage; import android.util.Log; import com.google.zxing.BarcodeFormat; import com.google.zxing.BinaryBitmap; import com.google.zxing.ChecksumException; import com.google.zxing.DecodeHintType; import com.google.zxing.EncodeHintType; import com.google.zxing.FormatException; import com.google.zxing.LuminanceSource; import com.google.zxing.NotFoundException; import com.google.zxing.PlanarYUVLuminanceSource; import com.google.zxing.RGBLuminanceSource; import com.google.zxing.Result; import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; import com.google.zxing.common.HybridBinarizer; import com.google.zxing.qrcode.QRCodeReader; import com.google.zxing.qrcode.QRCodeWriter; /** * 二维码编码解码。 * @author 作者:hanyeah * @date 创建时间:2017-2-15 上午11:51:27 */ public class JcQrCode { private static final String TAG="JcQrCode"; /** * 字符串编码为二维码图片。 * @param url 要编码的字符串 * @param qrWidth 二维码宽 * @param qrHeight 二维码高 * @return Bitmap 二维码图片 */ public static Bitmap encode(String url,int qrWidth,int qrHeight) { try { //判断URL合法性 if (url == null || "".equals(url) || url.length() < 1) { Log.d(TAG, "二维码参数不合法"); return null; } HashMap<EncodeHintType, String> encodeHints = new HashMap<EncodeHintType, String>(); encodeHints.put(EncodeHintType.CHARACTER_SET, "utf-8"); //图像数据转换,使用了矩阵转换 BitMatrix bitMatrix = new QRCodeWriter().encode(url, BarcodeFormat.QR_CODE, qrWidth, qrHeight, encodeHints); int[] pixels = new int[qrWidth * qrHeight]; //下面这里按照二维码的算法,逐个生成二维码的图片, //两个for循环是图片横列扫描的结果 for (int y = 0; y < qrHeight; y++) { for (int x = 0; x < qrWidth; x++) { if (bitMatrix.get(x, y)) { pixels[y * qrWidth + x] = 0xff000000; } else { pixels[y * qrWidth + x] = 0xffffffff; } } } //生成二维码图片的格式,使用ARGB_8888 Bitmap bitmap = Bitmap.createBitmap(qrWidth, qrHeight, Bitmap.Config.ARGB_8888); bitmap.setPixels(pixels, 0, qrWidth, 0, 0, qrWidth, qrHeight); return bitmap; } catch (WriterException e) { e.printStackTrace(); } return null; } /** * 二维码图片解码为字符串。 * @param bmp 二维码图片 * @return String 二维码图片中的信息,解码失败返回""。 */ public static String decode(Bitmap bmp){ int[] intArray = new int[bmp.getWidth()*bmp.getHeight()]; bmp.getPixels(intArray, 0, bmp.getWidth(), 0, 0, bmp.getWidth(), bmp.getHeight()); LuminanceSource source = new RGBLuminanceSource(bmp.getWidth(), bmp.getHeight(), intArray); BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source)); try { HashMap<DecodeHintType, String> decodeHints = new HashMap<DecodeHintType, String>(); decodeHints.put(DecodeHintType.CHARACTER_SET, "utf-8"); Result result=new QRCodeReader().decode(binaryBitmap,decodeHints); return result.getText(); } catch (NotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ChecksumException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (FormatException e) { // TODO Auto-generated catch block e.printStackTrace(); } return ""; } /** * 图片数据中指定二维码区域进行解码。 * @param data byte[] 图片数据,具体格式不知道。 * @param dataWidth int 图片宽 * @param dataHeight int 图片高 * @param left int * @param top int * @param width int * @param height int * @return String */ public static String decode(byte[] data,int dataWidth,int dataHeight,int left,int top,int width,int height){ PlanarYUVLuminanceSource source=new PlanarYUVLuminanceSource(data, dataWidth, dataHeight, left, top, width, height, false); BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source)); Result result; try { HashMap<DecodeHintType, String> decodeHints = new HashMap<DecodeHintType, String>(); decodeHints.put(DecodeHintType.CHARACTER_SET, "utf-8"); result = new QRCodeReader().decode(binaryBitmap,decodeHints); return result.getText(); } catch (NotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ChecksumException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (FormatException e) { // TODO Auto-generated catch block e.printStackTrace(); } return ""; } /** * 截取指定区域的图片。 * @param data byte[] 源数据源 * @param format int camera.getParameters().getPreviewFormat() * @param dataWidth int 数据宽 * @param dataHeight int 数据高 * @param left int * @param top int * @param width int * @param height int * @return Bitmap */ static Bitmap capture(byte[] data,int format,int dataWidth,int dataHeight,int left,int top,int width,int height){ //参考:http://blog.csdn.net/hipilee/article/details/8629234 YuvImage yuvImg = new YuvImage(data,format,dataWidth,dataHeight,null); ByteArrayOutputStream outputstream = new ByteArrayOutputStream(); Rect rect=new Rect(left,top,left+width,top+height); yuvImg.compressToJpeg(rect, 100, outputstream); Bitmap bitmap = BitmapFactory.decodeByteArray(outputstream.toByteArray(), 0,outputstream.size()); return bitmap; } /** * 截取指定区域的图片。 * 灰度图。 * @param data * @param dataWidth * @param dataHeight * @param left * @param top * @param width * @param height * @return */ static Bitmap renderCroppedGreyscaleBitmap(byte[] data,int dataWidth,int dataHeight,int left,int top,int width,int height) { int[] pixels = new int[width * height]; byte[] yuv = data; int inputOffset = top * dataWidth + left; for (int y = 0; y < height; y++) { int outputOffset = y * width; for (int x = 0; x < width; x++) { int grey = yuv[inputOffset + x] & 0xff; pixels[outputOffset + x] = 0xFF000000 | (grey * 0x00010101); } inputOffset += dataWidth; } Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bitmap.setPixels(pixels, 0, width, 0, 0, width, height); return bitmap; } }
扫描框类ViewfinderView.java
/* * Copyright (C) 2008 ZXing authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.jucheng.jclibs.qrcode; import java.util.Collection; import java.util.HashSet; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View; import com.google.zxing.ResultPoint; /** * 扫描框。 * <p>使用方式:</p> * <p>在启动JcQrCodeScanner之后调用</p> * <pre> * viewfinderView.stopSaomiao=false; * viewfinderView.invalidate(); * </pre> * <p>停止扫描时,调用</p> * <pre> * vf.stopSaomiao=true; * </pre> */ public final class ViewfinderView extends View { private static final String TAG = "ViewfinderView"; /** * 刷新界面的时间间隔(毫秒)。 */ public int animationDelay = 20; /** * 四个绿色边角对应的长度 */ public int cornerLineLength=10; /** * 四个绿色边角对应的宽度 */ public int cornerLineWidth = 3; /** * 四个绿色边角对应的颜色 */ public int cornerLineColor=0xFFFFCE85; /** * 扫描框中的中间线的宽度 */ public int middleLineWidth = 2; /** * 扫描框中的中间线的与扫描框左右的间隙 */ public int middleLinePadding = 5; /** * 中间那条线每次刷新移动的距离 */ public float speed = 5f; /** * 扫描线的位置(y方向坐标) */ public float middleLineLocation=0f; /** * 字体大小 */ private int textSize = 16; /** * 字体颜色 */ private int textColor=Color.WHITE; /** * 字体距离扫描框下面的距离 */ private int textPaddingTop = 30; /** * 显示的文字 */ private String text="扫描中..."; /** * 画笔对象的引用 */ private Paint paint = new Paint(); /** * 二维码区域外的颜色 */ public int maskColor = 0x88000000; /** * 停止扫描 */ public Boolean stopSaomiao=false; /** * */ public int resultPointColor = Color.YELLOW;; private Collection<ResultPoint> possibleResultPoints = new HashSet<ResultPoint>(5); private Collection<ResultPoint> lastPossibleResultPoints; public ViewfinderView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public void onDraw(Canvas canvas) { if (JcQrCodeScanner.get() == null) { return; } Rect frame = JcQrCodeScanner.get().getFramingRect(); if (frame == null) { return; } // 获取屏幕的宽和高 int width = canvas.getWidth(); int height = canvas.getHeight(); paint.setColor(maskColor); // 画出扫描框外面的阴影部分,共四个部分,扫描框的上面到屏幕上面,扫描框的下面到屏幕下面 // 扫描框的左边面到屏幕左边,扫描框的右边到屏幕右边 canvas.drawRect(0, 0, width, frame.top, paint); canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint); canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint); canvas.drawRect(0, frame.bottom + 1, width, height, paint); if(stopSaomiao)return; // 画扫描框边上的角,总共8个部分 paint.setColor(cornerLineColor); canvas.drawRect(frame.left, frame.top, frame.left + cornerLineLength, frame.top + cornerLineWidth, paint); canvas.drawRect(frame.left, frame.top, frame.left + cornerLineWidth, frame.top + cornerLineLength, paint); canvas.drawRect(frame.right - cornerLineLength, frame.top, frame.right, frame.top + cornerLineWidth, paint); canvas.drawRect(frame.right - cornerLineWidth, frame.top, frame.right, frame.top + cornerLineLength, paint); canvas.drawRect(frame.left, frame.bottom - cornerLineWidth, frame.left + cornerLineLength, frame.bottom, paint); canvas.drawRect(frame.left, frame.bottom - cornerLineLength, frame.left + cornerLineWidth, frame.bottom, paint); canvas.drawRect(frame.right - cornerLineLength, frame.bottom - cornerLineWidth, frame.right, frame.bottom, paint); canvas.drawRect(frame.right - cornerLineWidth, frame.bottom - cornerLineLength, frame.right, frame.bottom, paint); // 绘制中间的线,每次刷新界面,中间的线往下移动SPEEN_DISTANCE middleLineLocation += speed; if (middleLineLocation >= frame.bottom||middleLineLocation<frame.top) { middleLineLocation = frame.top; } canvas.drawRect(frame.left + middleLinePadding, middleLineLocation - middleLineWidth / 2, frame.right - middleLinePadding, middleLineLocation + middleLineWidth / 2, paint); //画小黄点 Collection<ResultPoint> currentPossible = possibleResultPoints; Collection<ResultPoint> currentLast = lastPossibleResultPoints; if (currentPossible.isEmpty()) { lastPossibleResultPoints = null; } else { possibleResultPoints = new HashSet<ResultPoint>(5); lastPossibleResultPoints = currentPossible; paint.setColor(resultPointColor); for (ResultPoint point : currentPossible) { canvas.drawCircle(frame.left + point.getX(), frame.top + point.getY(), 6.0f, paint); } } if (currentLast != null) { paint.setColor(resultPointColor); for (ResultPoint point : currentLast) { canvas.drawCircle(frame.left + point.getX(), frame.top + point.getY(), 3.0f, paint); } } // 画扫描框下面的字 drawText(canvas,frame); // 只刷新扫描框的内容,其他地方不刷新,刷新区域没有用到 postInvalidateDelayed(animationDelay, frame.left, frame.top, frame.right, frame.bottom); } /** * 画文字 * @param canvas * @param frame */ protected void drawText(Canvas canvas,Rect frame){ paint.setColor(textColor); paint.setTextSize(textSize); paint.setTypeface(Typeface.create("System", Typeface.BOLD)); canvas.drawText(text, frame.left, (float) (frame.bottom + (float) textPaddingTop), paint); } /** * 添加小黄点。 * 小黄点是什么意思,还不了解。 * @param point */ public void addPossibleResultPoint(ResultPoint point) { possibleResultPoints.add(point); } }
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。