Jetpack Compose + CameraX+ MlKit 实现 二维码扫描(二)

(二)实现二维码扫描框和动画

上一步中我们已经实现了相机的预览功能,了解上一步请移步这里 ,本篇文章中我们实现扫码功能,以下功能基于mlkit 官方demo修改而成,查看官方demo 请移步 github.com/googlesampl...

此功能已经发布到jitpack.io ,如果直接使用根据以下步骤: 引入依赖: settings.gradle.kts

scss 复制代码
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            setUrl("https://jitpack.io")
        }
    }
}

build.gradle.kts

scss 复制代码
implementation("com.github.qiangzengli:ScanBarcode:1.1.4")
// 权限申请库
implementation("com.google.accompanist:accompanist-permissions:0.34.0")

使用示例:

kotlin 复制代码
package com.alan.scan_example

import android.Manifest.permission.CAMERA
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.alan.scan_example.ui.theme.ScanBarcodeTheme
import com.alan.scanbarcode.scan.contract.ScanContract
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ScanBarcodeTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Example()
                }
            }
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Example() {
    var showCamera by remember { mutableStateOf(false) }
    val context = LocalContext.current
    // 权限请求对象
    val permissionState = rememberPermissionState(permission = CAMERA) {
        if (it) {
            showCamera = true
        }
    }
    LaunchedEffect(null) {
        if (permissionState.status.isGranted) {
            showCamera = true
        } else {
            permissionState.launchPermissionRequest()
        }
    }
    Scaffold { innerPadding ->
        val launcher = rememberLauncherForActivityResult(ScanContract()) { launchResult ->
            Toast.makeText(context, launchResult ?: "没有值", Toast.LENGTH_LONG).show()
        }
        Column(Modifier.padding(innerPadding)) {
            TextButton(onClick = {
                launcher.launch(null)
            }) {
                Text("调用扫码页面")
            }
        }

    }
}

预览效果

具体的实现代码

  1. 实现解析类 VisionImageProcessor.kt
kotlin 复制代码
/*
 * Copyright 2020 Google LLC. All rights reserved.
 *
 * 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 com.alan.scanbarcode.scan

import android.graphics.Bitmap
import androidx.camera.core.ImageProxy
import com.google.mlkit.common.MlKitException

/** An interface to process the images with different vision detectors and custom image models.  */
interface VisionImageProcessor {
    /** Processes ImageProxy image data, e.g. used for CameraX live preview case.  */
    @Throws(MlKitException::class)
    fun processImageProxy(image: ImageProxy?)

    /** Processes a bitmap image.   */
    fun processBitmap(bitmap: Bitmap?)

    /** Stops the underlying machine learning model and release resources.  */
    fun stop()
}

VisionProcessorBase.kt

kotlin 复制代码
/*
 * Copyright 2020 Google LLC. All rights reserved.
 *
 * 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 com.alan.scanbarcode.scan

import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import androidx.annotation.GuardedBy
import androidx.camera.core.ImageProxy
import com.google.android.gms.tasks.OnFailureListener
import com.google.android.gms.tasks.OnSuccessListener
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.TaskExecutors
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import java.nio.ByteBuffer

/**
 * Abstract base class for ML Kit frame processors. Subclasses need to implement {@link
 * #onSuccess(T, FrameMetadata, GraphicOverlay)} to define what they want to with the detection
 * results and {@link #detectInImage(VisionImage)} to specify the detector object.
 *
 * @param <T> The type of the detected feature.
 */
abstract class VisionProcessorBase(
    val onSuccessUnit: ((results: List<Barcode>) -> Unit)? = null,
    val onFailureUnit: ((e: Exception) -> Unit)? = null
) : VisionImageProcessor {

    private val executor = ScopedExecutor(TaskExecutors.MAIN_THREAD)

    // Whether this processor is already shut down
    private var isShutdown = false

    // To keep the latest images and its metadata.
    @GuardedBy("this")
    private var latestImage: ByteBuffer? = null

    @GuardedBy("this")
    private var latestImageMetaData: FrameMetadata? = null

    // To keep the images and metadata in process.
    @GuardedBy("this")
    private var processingImage: ByteBuffer? = null

    @GuardedBy("this")
    private var processingMetaData: FrameMetadata? = null

    @Synchronized
    private fun processLatestImage() {
        processingImage = latestImage
        processingMetaData = latestImageMetaData
        latestImage = null
        latestImageMetaData = null
        if (processingImage != null && processingMetaData != null && !isShutdown) {
            processImage(processingImage!!, processingMetaData!!)
        }
    }

    private fun processImage(
        data: ByteBuffer,
        frameMetadata: FrameMetadata,
    ) {
        requestDetectInImage(
            InputImage.fromByteBuffer(
                data,
                frameMetadata.width,
                frameMetadata.height,
                frameMetadata.rotation,
                InputImage.IMAGE_FORMAT_NV21
            ),
        )
            .addOnSuccessListener(executor) { processLatestImage() }
    }


    override fun processImageProxy(image: ImageProxy) {
        if (isShutdown) {
            return
        }

        requestDetectInImage(
            InputImage.fromMediaImage(image.image!!, image.imageInfo.rotationDegrees),
        )
            .addOnCompleteListener { image.close() }
    }

    fun processImageProxy(context: Context, image: ImageProxy) {
        Log.e("CropUtil", "开始裁剪:${System.currentTimeMillis()}")
        val bitmap = CropUtil.cropImageProxyCenter(context, image, 200)
        Log.e("CropUtil", "结束裁剪:${System.currentTimeMillis()}")
        requestDetectInImage(
//            InputImage.fromMediaImage(image.image!!, image.imageInfo.rotationDegrees),
            InputImage.fromBitmap(bitmap, 0),
        )
            .addOnCompleteListener {
                bitmap.recycle()
                image.close()
            }

    }


    // Code for processing single still image
    override fun processBitmap(bitmap: Bitmap?) {
        if (isShutdown) {
            return
        }
        requestDetectInImage(
            InputImage.fromBitmap(bitmap!!, 0),
        ).addOnCompleteListener { bitmap.recycle() }
    }

    private fun requestDetectInImage(
        image: InputImage,
    ): Task<List<Barcode>> {
        return setUpListener(
            detectInImage(image),
        )
    }

    private fun setUpListener(
        task: Task<List<Barcode>>,
    ): Task<List<Barcode>> {

        return task
            .addOnSuccessListener(
                executor,
                OnSuccessListener { results: List<Barcode> ->
                    onSuccessUnit?.invoke(results)
                }
            )
            .addOnFailureListener(
                executor,
                OnFailureListener { e: Exception ->
                    e.printStackTrace()
                    onFailureUnit?.invoke(e)
                }
            )
    }

    override fun stop() {
        executor.shutdown()
        isShutdown = true

    }

    protected abstract fun detectInImage(image: InputImage): Task<List<Barcode>>


}

ScopedExecutor.kt

kotlin 复制代码
/*
 * Copyright 2020 Google LLC. All rights reserved.
 *
 * 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 com.alan.scanbarcode.scan

import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Wraps an existing executor to provide a [.shutdown] method that allows subsequent
 * cancellation of submitted runnables.
 */
class ScopedExecutor(private val executor: Executor) : Executor {
    private val shutdown = AtomicBoolean()

    override fun execute(command: Runnable) {
        // Return early if this object has been shut down.
        if (shutdown.get()) {
            return
        }
        executor.execute inner@ {
            // Check again in case it has been shut down in the meantime.
            if (shutdown.get()) {
                return@inner
            }
            command.run()
        }
    }

    /**
     * After this method is called, no runnables that have been submitted or are subsequently
     * submitted will start to execute, turning this executor into a no-op.
     *
     *
     * Runnables that have already started to execute will continue.
     */
    fun shutdown() {
        shutdown.set(true)
    }
}

FrameMetadata.kt

kotlin 复制代码
package com.alan.scanbarcode.scan

import android.util.Size

/** Describing a frame info. */
class FrameMetadata(val width: Int = 0, val height: Int = 0, val rotation: Int = 0) {
    constructor(size: Size, rotation: Int = 0) : this(size.width, size.height, rotation)

    /** Builder of [FrameMetadata]. */
    class Builder {
        private var width = 0
        private var height = 0
        private var rotation = 0

        fun setWidth(width: Int): Builder {
            this.width = width
            return this
        }

        fun setHeight(height: Int): Builder {
            this.height = height
            return this
        }

        fun setRotation(rotation: Int): Builder {
            this.rotation = rotation
            return this
        }

        fun build(): FrameMetadata {
            return FrameMetadata(width, height)
        }
    }
}

CropUtil.kt

kotlin 复制代码
package com.alan.scanbarcode.scan

import android.content.Context
import android.graphics.*
import androidx.camera.core.ImageProxy
import com.alan.scanbarcode.scan.CropUtil.yuv420888ToNv21
import java.io.ByteArrayOutputStream

// DP转PX
fun Context.dpToPx(dp: Int): Int {
    return (dp * resources.displayMetrics.density).toInt()
}

fun ImageProxy.toBitmap(): Bitmap {
    val image = this
    val nv21 = yuv420888ToNv21(image)
    val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
    val out = ByteArrayOutputStream()
    yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 100, out)
    val imageBytes = out.toByteArray()
    return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}

object CropUtil {


    // 确保裁剪区域在图像范围内
    fun ensureCropRect(imageWidth: Int, imageHeight: Int, cropSize: Int): Rect {
        val centerX = imageWidth / 2
        val centerY = imageHeight / 2
        val halfSize = cropSize / 2

        return Rect(
            (centerX - halfSize).coerceAtLeast(0),
            (centerY - halfSize).coerceAtLeast(0),
            (centerX + halfSize).coerceAtMost(imageWidth),
            (centerY + halfSize).coerceAtMost(imageHeight)
        )
    }

    fun cropImageProxyCenter(context: Context,imageProxy: ImageProxy, sizeDp: Int): Bitmap {
        val rotationDegrees = imageProxy.imageInfo.rotationDegrees
        val matrix = Matrix().apply {
            postRotate(rotationDegrees.toFloat())
        }
        val bitmap = imageProxy.toBitmap()
        val newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
        val cropSizePx = context.dpToPx(sizeDp)
        val width = newBitmap.width
        val height = newBitmap.height
        val cropRect = ensureCropRect(width, height, cropSizePx)

        // 4. 执行裁剪
        return Bitmap.createBitmap(
            newBitmap,
            cropRect.left,
            cropRect.top,
            cropRect.width(),
            cropRect.height(),
        ).also {
            newBitmap.recycle()
            bitmap.recycle()
        }
    }


    fun yuv420888ToNv21(image: ImageProxy): ByteArray {
        val yBuffer = image.planes[0].buffer
        val uBuffer = image.planes[1].buffer
        val vBuffer = image.planes[2].buffer

        val ySize = yBuffer.remaining()
        val uSize = uBuffer.remaining()
        val vSize = vBuffer.remaining()

        val nv21 = ByteArray(ySize + uSize + vSize)

        yBuffer.get(nv21, 0, ySize)

        val chromaRowStride = image.planes[1].rowStride
        val chromaPixelStride = image.planes[1].pixelStride

        var offset = ySize
        for (row in 0 until image.height / 2) {
            for (col in 0 until image.width / 2) {
                val vuPos = row * chromaRowStride + col * chromaPixelStride
                nv21[offset++] = vBuffer.get(vuPos)
                nv21[offset++] = uBuffer.get(vuPos)
            }
        }

        return nv21
    }

}

BarcodeScannerProcessor.kt

kotlin 复制代码
package com.alan.scanbarcode.scan

import com.google.android.gms.tasks.Task
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage

/** Processor for the barcode detector. */
class BarcodeScannerProcessor(
    onSuccess: ((results: List<Barcode>) -> Unit)? = null,
    onFailed: ((e: Exception) -> Unit)? = null
) : VisionProcessorBase(onSuccess, onFailed) {
    private val barcodeScanner: BarcodeScanner =
        BarcodeScanning.getClient(
            BarcodeScannerOptions.Builder()
                .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
                .build()
        )


    override fun stop() {
        super.stop()
        barcodeScanner.close()
    }

    override fun detectInImage(image: InputImage): Task<List<Barcode>> {
        return barcodeScanner.process(image)
    }
}

ScanContract.kt

kotlin 复制代码
package com.alan.scanbarcode.scan.contract

import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import com.alan.scanbarcode.ScanActivity


/**
 * 自定义跳转Activity接收返回值Contract
 * 使用out 指定泛型上界,即协变,等同于Java <? extends Activity>
 */
class ScanContract() :
    ActivityResultContract<Unit?, String?>() {
    override fun createIntent(context: Context, input: Unit?) =
        Intent(context, ScanActivity::class.java)

    override fun parseResult(resultCode: Int, intent: Intent?): String? {
        if (resultCode == Activity.RESULT_OK) return intent?.getStringExtra("barcode")
        return null
    }
}

ScanPage.kt

kotlin 复制代码
package com.alan.scanbarcode.scan

import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.util.Log
import androidx.compose.animation.core.*
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.material.TabRowDefaults.Divider
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.alan.scanbarcode.ScanActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@Composable
fun ScanPage() {
    val context = LocalContext.current
    val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
        vibratorManager.defaultVibrator
    } else {
        @Suppress("DEPRECATION")
        context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
    }

    // 检查设备是否支持震动
    fun hasVibrator(): Boolean {
        return vibrator.hasVibrator()
    }

    // 简单震动
    fun vibrate(durationMillis: Long) {
        if (!hasVibrator()) return
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            vibrator.vibrate(VibrationEffect.createOneShot(durationMillis, VibrationEffect.DEFAULT_AMPLITUDE))
        } else {
            @Suppress("DEPRECATION")
            vibrator.vibrate(durationMillis)
        }
    }

    var offset by remember { mutableStateOf(0f) }
    // 无限重复动画
    val infiniteTransition = rememberInfiniteTransition()
    offset = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 1500, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        )
    ).value
    var currentTime by remember { mutableStateOf(0L) }
    var lastTime by remember { mutableStateOf(0L) }
    // 是否扫描成功
    var isSuccess by remember { mutableStateOf(false) }
    val scope  = rememberCoroutineScope()
    val processor by remember {
        mutableStateOf(
            BarcodeScannerProcessor(
                onSuccess = {
                    if (isSuccess) return@BarcodeScannerProcessor
                    if (it.isNotEmpty()) {
                        isSuccess = true
                        vibrate(200L)
                        (context as ScanActivity).apply {
                            setResult(RESULT_OK, Intent().apply {
                                putExtra("barcode", it.firstOrNull()?.rawValue)
                            })
                            finish()
                        }
                    }

                },
                onFailed = {

                }

            ))
    }
    Scaffold(modifier = Modifier.fillMaxSize()) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            CameraView(
                onAnalyze = {
                    scope.launch (Dispatchers.IO) {
                        Log.d("ScanPage", "onAnalyze")
                        currentTime = System.currentTimeMillis()
                        if (currentTime - lastTime >= 200L) {
                            processor.processImageProxy(context, it)
                            lastTime = currentTime
                        } else {
                            it.close()
                        }

                    }

                },
            )
            Box(
                modifier = Modifier
                    .size(200.dp)
                    .zIndex(1000f)
                    .border(1.dp, color = Color.Blue),
                contentAlignment = Alignment.Center,
            ) {
// 扫描线
                Divider(
                    color = Color.Green.copy(alpha = 0.7f),
                    thickness = 2.dp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .offset(y = 200.dp * offset - 100.dp)
                        .shadow(4.dp, shape = RectangleShape)
                )
            }

        }
    }

}

ScanActivity.kt

kotlin 复制代码
package com.alan.scanbarcode

import android.os.Bundle
import android.view.Window.FEATURE_NO_TITLE
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.view.WindowCompat
import com.alan.scanbarcode.scan.ScanPage

class ScanActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)
        window.statusBarColor = Color.Transparent.toArgb()
        window.navigationBarColor = Color.Transparent.toArgb()
        // 隐藏ActionBar
        requestWindowFeature(FEATURE_NO_TITLE)
        setContent {
            ScanPage()
        }
    }
}
相关推荐
lichong9516 分钟前
【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之video 的各种状态和生命周期调用说明
android·vue.js·macos
app出海创收老李19 分钟前
海外独立创收日记(1)-我是如何从0到1在Google Play获得睡后被动收入的?
android·程序员
lang99988820 分钟前
kodi在Android4.0.4安装播放歌曲显示歌词
android·kodi·歌词插件
yzx99101344 分钟前
构建未来:深度学习、嵌入式与安卓开发的融合创新之路
android·人工智能·深度学习
前行的小黑炭1 小时前
Android :如何快速让布局适配手机和平板?
android·java·kotlin
Yang-Never5 小时前
Kotlin协程 -> Job.join() 完整流程图与核心源码分析
android·开发语言·kotlin·android studio
一笑的小酒馆11 小时前
Android性能优化之截屏时黑屏卡顿问题
android
懒人村杂货铺13 小时前
Android BLE 扫描完整实战
android
TeleostNaCl15 小时前
如何安装 Google 通用的驱动以便使用 ADB 和 Fastboot 调试(Bootloader)设备
android·经验分享·adb·android studio·android-studio·android runtime
fatiaozhang952716 小时前
中国移动浪潮云电脑CD1000-系统全分区备份包-可瑞芯微工具刷机-可救砖
android·网络·电脑·电视盒子·刷机固件·机顶盒刷机