You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 04:15:54