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

基于CameraX与ML Kit:如何连接BarcodeAnalyzer与QrActivity实现二维码识别

解决CameraX + ML Kit二维码扫描的类连接与实例化问题

看起来你现在的核心问题是没有把BarcodeAnalyzer正确绑定到CameraX的ImageAnalysis用例,同时Analyzer内部也有几个关键问题(比如未初始化Context、未处理识别结果、未关闭ImageProxy)导致功能失效。我来一步步帮你解决:


第一步:修复BarcodeAnalyzer类

我们需要给Analyzer添加构造函数传入Context,定义回调接口把扫描结果传给Activity,同时修复ImageProxy的关闭逻辑:

package ge.softservice.nfcwithactivties

import android.annotation.SuppressLint
import android.content.Context
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.firebase.ml.vision.FirebaseVision
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetectorOptions
import com.google.firebase.ml.vision.common.FirebaseVisionImage
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata

// 定义回调接口,把扫描结果传递给Activity处理
interface BarcodeScanListener {
    fun onBarcodeScanned(barcode: FirebaseVisionBarcode)
    fun onScanFailed(error: Exception)
}

class BarcodeAnalyzer(
    private val context: Context,
    private val scanListener: BarcodeScanListener
) : ImageAnalysis.Analyzer {

    private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
        0 -> FirebaseVisionImageMetadata.ROTATION_0
        90 -> FirebaseVisionImageMetadata.ROTATION_90
        180 -> FirebaseVisionImageMetadata.ROTATION_180
        270 -> FirebaseVisionImageMetadata.ROTATION_270
        else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
    }

    @SuppressLint("UnsafeExperimentalUsageError")
    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image ?: run {
            imageProxy.close() // 无有效图像直接关闭,避免阻塞CameraX
            return
        }
        val imageRotation = degreesToFirebaseRotation(imageProxy.imageInfo.rotationDegrees)
        val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)

        val options = FirebaseVisionBarcodeDetectorOptions.Builder()
            .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
            .build()

        val detector = FirebaseVision.getInstance().getVisionBarcodeDetector(options)

        detector.detectInImage(image)
            .addOnSuccessListener { barcodes ->
                if (barcodes.isNotEmpty()) {
                    // 将扫描结果通过回调传给Activity
                    scanListener.onBarcodeScanned(barcodes[0])
                }
                imageProxy.close() // 必须关闭ImageProxy,否则CameraX会停止输出帧
            }
            .addOnFailureListener { exception ->
                scanListener.onScanFailed(exception)
                imageProxy.close() // 失败场景也要关闭
            }
    }
}

关键修复点:

  • 通过构造函数传入Context和BarcodeScanListener,解决未初始化问题
  • 必须在异步回调末尾调用imageProxy.close(),避免CameraX阻塞
  • 用回调接口实现Analyzer与Activity的解耦,让Activity处理UI逻辑
  • 增加了图像为空的边界处理

第二步:修改QrActivity,绑定ImageAnalysis用例

现在需要在startCamera()方法中创建ImageAnalysis实例,绑定我们的BarcodeAnalyzer,并且实现回调接口处理扫描结果:

package ge.softservice.nfcwithactivties

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_qr.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class QrActivity : AppCompatActivity(), BarcodeScanListener {
    private var preview: Preview? = null
    private var imageAnalyzer: ImageAnalysis? = null
    private var camera: Camera? = null
    private lateinit var cameraExecutor: ExecutorService

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(
                    this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT
                ).show()
                finish()
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_qr)

        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this,
                REQUIRED_PERMISSIONS,
                REQUEST_CODE_PERMISSIONS
            )
        }
        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener(Runnable {
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // 初始化Preview用例
            preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewFinder.createSurfaceProvider())
                }

            // 初始化ImageAnalysis用例并绑定BarcodeAnalyzer
            imageAnalyzer = ImageAnalysis.Builder()
                // 选择只保留最新帧的策略,适合实时扫描
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
                .also {
                    // 实例化Analyzer,传入Context和当前Activity作为回调接收者
                    it.setAnalyzer(cameraExecutor, BarcodeAnalyzer(this, this))
                }

            // 选择后置摄像头
            val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build()

            try {
                cameraProvider.unbindAll()
                // 绑定Preview和ImageAnalysis两个用例到Camera
                camera = cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageAnalyzer
                )
            } catch (exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    // 实现回调方法,处理扫描成功的结果
    override fun onBarcodeScanned(barcode: FirebaseVisionBarcode) {
        val barcodeContent = barcode.rawValue ?: "二维码无内容"
        // 切换到主线程显示Toast,避免后台线程操作UI报错
        runOnUiThread {
            Toast.makeText(this, "扫描到:$barcodeContent", Toast.LENGTH_LONG).show()
            // 可选:扫描成功后关闭页面或执行其他逻辑
            // finish()
        }
    }

    // 实现回调方法,处理扫描失败的情况
    override fun onScanFailed(error: Exception) {
        runOnUiThread {
            Toast.makeText(this, "扫描失败:${error.message}", Toast.LENGTH_SHORT).show()
        }
        Log.e(TAG, "Scan failed", error)
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    companion object {
        private const val TAG = "QrScanActivity"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
    }
}

关键修改点:

  • 让QrActivity实现BarcodeScanListener接口,接收扫描结果
  • 在startCamera()中创建并绑定了ImageAnalysis用例,这是你之前缺失的核心步骤
  • 使用cameraExecutor后台线程处理图像分析,符合CameraX的线程要求
  • 在回调中用runOnUiThread()确保UI操作在主线程执行

为什么之前的代码不工作?

  1. 未绑定ImageAnalysis用例:你原来的startCamera()只绑定了Preview,BarcodeAnalyzer根本没有被CameraX调用
  2. Context未初始化:原Analyzer的lateinit var context没有赋值,实际执行时会触发空指针(只是因为Analyzer没被调用所以没报错)
  3. 未关闭ImageProxy:CameraX要求每次分析后必须关闭ImageProxy,否则会停止输出新帧,导致扫描卡住
  4. 线程问题:ML Kit的回调在后台线程,直接调用Toast会触发UI线程异常

现在按照上面的代码修改后,就能正常实现二维码扫描功能了。

内容的提问来源于stack exchange,提问作者X Y

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 22:23:09