Andorid基于ZXing实现二维码生成&扫描

最近遇上了扫描条码的需求,在查找资料过程中不是那么地顺利,做个笔记,记录下这两篇文章,前人栽树后人乘凉。
Andorid基于ZXing实现二维码生成&扫描
Android基于MLKit实现条形码扫码
本篇文章Demo下载

ZXing介绍

说到二维码,大量的资料都会提到ZXing,具体见ZXing,这是一个用Java语言实现的1D/2D 条形码图像处理库。涉及专业知识不多做介绍,这篇文章只讲使用。

二维码生成

引入ZXing核心库:

implementation 'com.google.zxing:core:3.5.1'

创建二维码位图,写了一个工具类,可以直接使用:

object QrCodeUtil {
    /**
     * 创建二维码位图 (支持自定义配置和自定义样式)
     * @param content 字符串内容
     * @param width 位图宽度,要求>=0(单位:px)
     * @param height 位图高度,要求>=0(单位:px)
     * @param character_set 字符集/字符转码格式 (支持格式:{@link CharacterSetECI })。传null时,zxing源码默认使用 "ISO-8859-1"
     * @param error_correction 容错级别 (支持级别:{@link ErrorCorrectionLevel })。传null时,zxing源码默认使用 "L"
     * @param margin 空白边距 (可修改,要求:整型且>=0), 传null时,zxing源码默认使用"4"。
     * @param color_black 黑色色块的自定义颜色值
     * @param color_white 白色色块的自定义颜色值
     * @return
     */
    fun createQRCodeBitmap(
        content: String,
        width: Int,
        height: Int,
        character_set: String = "UTF-8",
        error_correction: String = "H",
        margin: String = "1",
        @ColorInt color_black: Int = Color.BLACK,
        @ColorInt color_white: Int = Color.WHITE,
    ): Bitmap? {
        /** 1.参数合法性判断  */
        if (width < 0 || height < 0) { // 宽和高都需要>=0
            return null
        }
        try {
            /** 2.设置二维码相关配置,生成BitMatrix(位矩阵)对象  */
            val hints: Hashtable<EncodeHintType, String> = Hashtable()
            if (character_set.isNotEmpty()) {
                hints[EncodeHintType.CHARACTER_SET] = character_set // 字符转码格式设置
            }
            if (error_correction.isNotEmpty()) {
                hints[EncodeHintType.ERROR_CORRECTION] = error_correction // 容错级别设置
            }
            if (margin.isNotEmpty()) {
                hints[EncodeHintType.MARGIN] = margin // 空白边距设置
            }
            val bitMatrix =
                QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)

            /** 3.创建像素数组,并根据BitMatrix(位矩阵)对象为数组元素赋颜色值  */
            val pixels = IntArray(width * height)
            for (y in 0 until height) {
                for (x in 0 until width) {
                    if (bitMatrix[x, y]) {
                        pixels[y * width + x] = color_black // 黑色色块像素设置
                    } else {
                        pixels[y * width + x] = color_white // 白色色块像素设置
                    }
                }
            }
            /** 4.创建Bitmap对象,根据像素数组设置Bitmap每个像素点的颜色值,之后返回Bitmap对象  */
            val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
            bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
            return bitmap
        } catch (e: WriterException) {
            e.printStackTrace()
        }
        return null
    }
}

使用到了ZXing核心功能的就是这句
QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints),
传入内容、样式、宽高以及配置。

在配置EncodeHintType.CHARACTER_SET字符格式时,我们使用了"UTF-8",这是为了兼容中文,ZXing源码默认使用的是"ISO-8859-1",而"ISO-8859-1"本身是不支持中文的。

看一下使用效果:

val imageView = findViewById<ImageView>(R.id.iv)
val bitmap = createQRCodeBitmap("你好,初次见面,请多指教!", 480, 480)
imageView.setImageBitmap(bitmap)
生成二维码

生成带logo小图的二维码

很多时候见到的二维码中间都会带有一个logo小图,我们也来实现一下这样子的效果。

其实就是把两个bitmap绘制在一块,在原有的方法上补充即可,添加两个参数:

object QrCodeUtil {
    /**
     * 创建二维码位图 (支持自定义配置和自定义样式)
     * @param content 字符串内容
     * @param width 位图宽度,要求>=0(单位:px)
     * @param height 位图高度,要求>=0(单位:px)
     * @param character_set 字符集/字符转码格式 (支持格式:{@link CharacterSetECI })。传null时,zxing源码默认使用 "ISO-8859-1"
     * @param error_correction 容错级别 (支持级别:{@link ErrorCorrectionLevel })。传null时,zxing源码默认使用 "L"
     * @param margin 空白边距 (可修改,要求:整型且>=0), 传null时,zxing源码默认使用"4"。
     * @param color_black 黑色色块的自定义颜色值
     * @param color_white 白色色块的自定义颜色值
     * @param logoBitmap logo小图片
     * @param logoPercent logo小图片在二维码图片中的占比大小,范围[0F,1F],超出范围->默认使用0.2F。
     * @return
     */
    fun createQRCodeBitmap(
        content: String,
        width: Int,
        height: Int,
        character_set: String = "UTF-8",
        error_correction: String = "H",
        margin: String = "1",
        @ColorInt color_black: Int = Color.BLACK,
        @ColorInt color_white: Int = Color.WHITE,
        logoBitmap: Bitmap? = null,
        logoPercent: Float = 0f
    ): Bitmap? {
        /** 1.参数合法性判断  */
        if (width < 0 || height < 0) { // 宽和高都需要>=0
            return null
        }
        try {
            /** 2.设置二维码相关配置,生成BitMatrix(位矩阵)对象  */
            val hints: Hashtable<EncodeHintType, String> = Hashtable()
            if (character_set.isNotEmpty()) {
                hints[EncodeHintType.CHARACTER_SET] = character_set // 字符转码格式设置
            }
            if (error_correction.isNotEmpty()) {
                hints[EncodeHintType.ERROR_CORRECTION] = error_correction // 容错级别设置
            }
            if (margin.isNotEmpty()) {
                hints[EncodeHintType.MARGIN] = margin // 空白边距设置
            }
            val bitMatrix =
                QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)

            /** 3.创建像素数组,并根据BitMatrix(位矩阵)对象为数组元素赋颜色值  */
            val pixels = IntArray(width * height)
            for (y in 0 until height) {
                for (x in 0 until width) {
                    if (bitMatrix[x, y]) {
                        pixels[y * width + x] = color_black // 黑色色块像素设置
                    } else {
                        pixels[y * width + x] = color_white // 白色色块像素设置
                    }
                }
            }
            /** 4.创建Bitmap对象,根据像素数组设置Bitmap每个像素点的颜色值,之后返回Bitmap对象  */
            val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
            bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
            /** 5.为二维码添加logo小图标 */
            if (logoBitmap != null) {
                return addLogo(bitmap, logoBitmap, logoPercent)
            }
            return bitmap
        } catch (e: WriterException) {
            e.printStackTrace()
        }
        return null
    }

    private fun addLogo(srcBitmap: Bitmap?, logoBitmap: Bitmap?, logoPercent: Float): Bitmap? {
        /** 1.参数合法性判断  */
        if (srcBitmap == null || logoBitmap == null) {
            return null
        }
        var percent = logoPercent
        if (logoPercent < 0F || logoPercent > 1F) {
            percent = 0.2F
        }
        /** 2. 获取原图片和Logo图片各自的宽、高值 */
        val srcWidth = srcBitmap.width
        val srcHeight = srcBitmap.height
        val logoWidth = logoBitmap.width
        val logoHeight = logoBitmap.height

        /** 3. 计算画布缩放的宽高比 */
        val scaleWidth = srcWidth * percent / logoWidth
        val scaleHeight = srcHeight * percent / logoHeight

        /** 4. 使用Canvas绘制,合成图片 */
        val bitmap = Bitmap.createBitmap(srcWidth, srcHeight, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        canvas.drawBitmap(srcBitmap, 0f, 0f, null)
        canvas.scale(scaleWidth, scaleHeight, (srcWidth / 2).toFloat(), (srcHeight / 2).toFloat())
        canvas.drawBitmap(logoBitmap, srcWidth * 1f / 2 - logoWidth / 2, srcHeight * 1f / 2 - logoHeight / 2, null)
        return bitmap
    }
}

看一下使用效果:

        val imageViewLogo = findViewById<ImageView>(R.id.iv_logo)
        val logo = BitmapFactory.decodeResource(resources, R.drawable.cat)
        val bitmapLogo = createQRCodeBitmap(
            content = "你好,初次见面,请多指教!",
            width = 480,
            height = 480,
            logoBitmap = logo,
            logoPercent = 0.3f
        )
        imageViewLogo.setImageBitmap(bitmapLogo)
带logo二维码

二维码扫描

借助开源库 ZXing Android Embedded 实现二维码扫描。

ZXing Android Embedded 是用于Android的条形码扫描库,使用ZXing进行解码。

更多的使用可以下载源码工程跑下样例查看,包括设置前后摄像头、设置扫描超时时间等,该篇文章就只介绍最基本的二维码扫描使用。

引入库:

implementation 'com.journeyapps:zxing-android-embedded:4.3.0'

使用相机扫描二维码

跳转到扫描页面后会自动开始扫描,扫描到结果后会将结果返回,onActivityResult废弃之后,使用Activity Result API获取页面回传数据,不了解Activity Result API的可以查看这篇文章Android Activity Result API使用

    private val barcodeLauncher = registerForActivityResult(
        ScanContract()
    ) { result: ScanIntentResult ->
        if (result.contents == null) {
            val originalIntent = result.originalIntent
            if (originalIntent == null) {
                Toast.makeText(this@MainActivity, "Cancelled", Toast.LENGTH_LONG).show()
            } else if (originalIntent.hasExtra(Intents.Scan.MISSING_CAMERA_PERMISSION)) {
                Toast.makeText(this@MainActivity, "Cancelled due to missing camera permission", Toast.LENGTH_LONG)
                    .show()
            }
        } else {
            Toast.makeText(this@MainActivity, "Scanned: " + result.contents, Toast.LENGTH_LONG).show()
        }
    }
        findViewById<Button>(R.id.bt).setOnClickListener {
            barcodeLauncher.launch(ScanOptions())
        }

这是使用默认的扫描页面,使用方法很简单,但是更多的情况下,我们都需要自定义扫描页面样式。

自定义CustomScannerActivity,启动扫描时设置CaptureActivity即可:

        findViewById<Button>(R.id.bt2).setOnClickListener {
            val options = ScanOptions().setOrientationLocked(false).setCaptureActivity(
                CustomScannerActivity::class.java
            )
            barcodeLauncher.launch(options)
        }

具体的在自定义扫描页面中如何配置扫描view,可以参考本篇文章Demo下载或者ZXing Android Embedded 源码工程,里面有样例可以参考,本篇文章也是参考的里面的样例。

从相册中识别二维码图片

在样例中并没有找到从相册中识别二维码图片的方法,最终在issue中发现有提到同样的问题以及解答

打开相册获取图片:

    private fun openGallery() {
        val intent = Intent()
        intent.type = "image/*"
        intent.action = Intent.ACTION_GET_CONTENT
        openGalleryRequest.launch(Intent.createChooser(intent, "识别相册二维码图片"))
    }
    private val openGalleryRequest =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == RESULT_OK) {
                it.data?.data?.let { uri -> handleImage(uri) }
            }
        }

解析图片二维码:

    private fun handleImage(uri: Uri) {
        try {
            val image = MediaStore.Images.Media.getBitmap(this.contentResolver, uri)

            val intArray = IntArray(image.width * image.height)
            image.getPixels(intArray, 0, image.width, 0, 0, image.width, image.height)

            val source = RGBLuminanceSource(image.width, image.height, intArray)
            val reader = MixedDecoder(MultiFormatReader())
            var result = reader.decode(source)
            if (result == null) {
                result = reader.decode(source)
            }
            Toast.makeText(this@MainActivity, "Scanned: ${result?.text}", Toast.LENGTH_LONG).show()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

上述方法从相册中识别二维码图片,发现存在识别失败的问题,尤其是商品条形码,使用相机扫描商品条形码是可以正常扫描识别出来的,但是将商品条形码拍照保存进相册,使用从相册中识别二维码图片方法,却出现识别失败的情况。

为此,又去查找了其他的资料,见下一篇文章Android基于MLKit实现条形码扫码

参考文档:
https://www.jianshu.com/p/b275e818de6a
https://www.jianshu.com/p/c75f16de1b2c
https://www.jianshu.com/p/b85812b6f7c1
https://github.com/journeyapps/zxing-android-embedded/discussions/685

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容