如何为基于Google Mobile Vision API的条码扫描器设置识别框?
好的,我来帮你实现这个仅识别框内条码的功能,核心其实就是两步:先在UI上添加扫描框,然后对Google Mobile Vision检测到的条码做坐标转换和过滤。下面是具体的实现步骤和代码:
实现步骤
1. 修改XML布局,添加扫描框UI
原来的LinearLayout没法叠加视图,我们换成FrameLayout,这样可以把扫描框直接放在相机预览的上方。这里我用自定义View来绘制扫描框,比普通View更灵活(后续还能轻松加扫描线动画)。
修改后的布局代码:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScanBarcodeActivity"> <!-- 相机预览SurfaceView --> <SurfaceView android:id="@+id/cameraPreview" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- 自定义扫描识别框,居中显示 --> <com.yourpackage.ScanBoxView android:id="@+id/scanBox" android:layout_width="300dp" android:layout_height="300dp" android:layout_gravity="center" /> </FrameLayout>
然后创建自定义的ScanBoxView类,用来绘制扫描框并提供坐标获取方法:
import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; public class ScanBoxView extends View { private Paint borderPaint; private Rect scanRect; public ScanBoxView(Context context) { super(context); init(); } public ScanBoxView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { borderPaint = new Paint(); borderPaint.setColor(Color.WHITE); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(4f); borderPaint.setAntiAlias(true); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 获取当前View的矩形区域作为扫描框范围 scanRect = new Rect(0, 0, getWidth(), getHeight()); canvas.drawRect(scanRect, borderPaint); // 如果需要扫描线动画,这里可以添加相关逻辑,比如绘制移动的横线 } // 获取扫描框在屏幕上的绝对坐标(用于后续判断条码位置) public Rect getScanBoxAbsoluteRect() { int[] location = new int[2]; getLocationOnScreen(location); return new Rect( location[0], location[1], location[0] + getWidth(), location[1] + getHeight() ); } }
2. 处理坐标转换与条码过滤
Google Mobile Vision返回的条码坐标是基于相机预览帧的(通常是横屏方向,和手机屏幕竖屏方向不一致),所以我们需要把条码的坐标转换成屏幕坐标,再判断是否落在扫描框内。
下面是ScanBarcodeActivity的完整代码,包含相机初始化、条码检测和过滤逻辑:
import android.Manifest; import android.content.pm.PackageManager; import android.graphics.Rect; import android.os.Bundle; import android.util.SparseArray; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import com.google.android.gms.vision.CameraSource; import com.google.android.gms.vision.Detector; import com.google.android.gms.vision.barcode.Barcode; import com.google.android.gms.vision.barcode.BarcodeDetector; import java.io.IOException; public class ScanBarcodeActivity extends AppCompatActivity { private SurfaceView cameraPreview; private CameraSource cameraSource; private ScanBoxView scanBox; private BarcodeDetector barcodeDetector; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scan_barcode); cameraPreview = findViewById(R.id.cameraPreview); scanBox = findViewById(R.id.scanBox); // 初始化条码检测器,支持所有条码格式 barcodeDetector = new BarcodeDetector.Builder(this) .setBarcodeFormats(Barcode.ALL_FORMATS) .build(); // 设置条码处理器,过滤框内的条码 barcodeDetector.setProcessor(new Detector.Processor<Barcode>() { @Override public void release() { // 释放资源 } @Override public void receiveDetections(Detector.Detections<Barcode> detections) { SparseArray<Barcode> barcodes = detections.getDetectedItems(); if (barcodes.size() > 0) { for (int i = 0; i < barcodes.size(); i++) { Barcode barcode = barcodes.valueAt(i); // 判断当前条码是否在扫描框内 if (isBarcodeInScanBox(barcode)) { // 处理识别结果(在UI线程执行) runOnUiThread(() -> { Toast.makeText(ScanBarcodeActivity.this, barcode.displayValue, Toast.LENGTH_SHORT).show(); // 可选:识别到后停止扫描,避免重复识别 stopScan(); }); break; } } } } }); // 初始化相机源,设置预览尺寸、自动对焦和帧率 cameraSource = new CameraSource.Builder(this, barcodeDetector) .setRequestedPreviewSize(1280, 720) .setAutoFocusEnabled(true) .setRequestedFps(30.0f) .build(); // 启动相机预览 cameraPreview.getHolder().addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { try { if (ActivityCompat.checkSelfPermission(ScanBarcodeActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { // 请求相机权限 ActivityCompat.requestPermissions(ScanBarcodeActivity.this, new String[]{Manifest.permission.CAMERA}, 100); return; } cameraSource.start(holder); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(SurfaceHolder holder) { cameraSource.stop(); } }); } // 判断条码是否在扫描框内的核心方法 private boolean isBarcodeInScanBox(Barcode barcode) { Rect scanBoxRect = scanBox.getScanBoxAbsoluteRect(); Rect barcodeScreenRect = convertBarcodeRectToScreenRect(barcode); // 这里可以选择两种判断逻辑: // 1. scanBoxRect.contains(barcodeScreenRect):条码完全在框内才识别 // 2. Rect.intersects(scanBoxRect, barcodeScreenRect):条码和框有交集就识别 return scanBoxRect.contains(barcodeScreenRect); } // 将条码的预览帧坐标转换为屏幕坐标 private Rect convertBarcodeRectToScreenRect(Barcode barcode) { Camera.Size previewSize = cameraSource.getPreviewSize(); if (previewSize == null || barcode.getBoundingBox() == null) { return new Rect(); } int screenWidth = getResources().getDisplayMetrics().widthPixels; int screenHeight = getResources().getDisplayMetrics().heightPixels; // 相机预览是横屏,屏幕是竖屏,需要旋转并缩放坐标 float scaleX = (float) screenWidth / previewSize.height; float scaleY = (float) screenHeight / previewSize.width; Rect barcodePreviewRect = barcode.getBoundingBox(); // 旋转90度后的坐标转换 int left = (int) (barcodePreviewRect.top * scaleX); int top = (int) ((previewSize.width - barcodePreviewRect.right) * scaleY); int right = (int) (barcodePreviewRect.bottom * scaleX); int bottom = (int) ((previewSize.width - barcodePreviewRect.left) * scaleY); return new Rect(left, top, right, bottom); } // 停止扫描并释放资源 private void stopScan() { if (cameraSource != null) { cameraSource.stop(); } if (barcodeDetector != null) { barcodeDetector.release(); } } @Override protected void onDestroy() { super.onDestroy(); stopScan(); } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 100) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { try { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { return; } cameraSource.start(cameraPreview.getHolder()); } catch (IOException e) { e.printStackTrace(); } } else { Toast.makeText(this, "需要相机权限才能扫描条码", Toast.LENGTH_SHORT).show(); finish(); } } } }
关键注意点
- 权限处理:代码里已经包含了相机权限的请求逻辑,务必确保用户授权后再启动相机预览。
- 坐标适配:不同手机的相机传感器方向可能不同(比如部分机型是270度),如果需要兼容所有设备,可以通过
CameraManager获取相机的传感器方向,调整坐标旋转的计算逻辑。 - 识别逻辑灵活调整:根据你的需求,选择条码完全在框内才识别,还是只要有交集就识别,修改
isBarcodeInScanBox方法里的判断逻辑即可。 - 性能优化:识别到符合条件的条码后,建议停止扫描,避免重复识别;如果需要持续扫描,可以记录已识别的条码ID,过滤重复结果。
内容的提问来源于stack exchange,提问作者A.R.B.N
相关产品推荐
相关产品推荐

