ベータコンピューティングの活動や技術、開発のこだわりなどを紹介するブログです。



AndroidでQRコードとJANコードを扱ってみる

Beta ComputingのAndroid担当のd-ariakeです。
Android案件でバーコードを扱う機会があったので、実装方法を残したいと思います。

行うこと

今回は以下の2点を共有したいと思います。

  • ZXing Android Embedded を使い、QRコードとJANコードを生成する。
  • Glide のカスタムModel Loaderで生成・キャッシュさせる。

ZXing Android Embeddedによるバーコードの生成

ZXing Android EmbeddedZxing をAndroid向けに使いやすくしたライブラリです。
このライブラリを使うと以下のようなコードを書くだけで、簡単にバーコードを生成することができます。

//  QRコードを生成するコード
val hints = mapOf(
    EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.H,
    EncodeHintType.CHARACTER_SET to "Shift_JIS",
    EncodeHintType.QR_VERSION to 12
)
val bitmap: android.graphics.Bitmap =
    BarcodeEncoder().encodeBitmap(contents, BarcodeFormat.QR, width, height, hints)
//  JAN (EAN) コードを生成するコード
val bitmap: android.graphics.Bitmap =
    BarcodeEncoder().encodeBitmap(contents, BarcodeFormat.EAN_13, width, height)

本当に簡単に生成できちゃいます!

余談:
JANコードを生成する際には12桁か13桁の数値文字列を渡す必要があります。
13桁目はチェックサムとなっていて、誤り検出に使われるみたいです。
EAN13Writer.javaUPCEANReader.java のソースを読んだ感じ、12桁のコードを渡すと自動的にチェックサムを計算して13桁として生成してくれるようです。

GlideのカスタムModel Loaderの実装

Androidで画像を扱う際には何かしらの画像管理ライブラリを使うかと思います。
今回は私が一番慣れているGlideを使用して、QRコード・JANコードを生成・キャッシュし、ImageView に表示させるといったことを行いました。

ネット上の画像の場合はそのままURL文字列を渡してしまえばすぐに実現できてしまいます。

Glide.with(this.applicationContext)
    .load("https://画像のリンク.png")
    .placeholder(R.drawable.プレースホルダー)
    .into(this.imageView)

しかし、今回のようにQR・JANコードを生成する場合は自分で ModelLoader を実装する必要があります。
以下に実装手順を載せていきます。

(公式ドキュメント: Glide v4 : Writing a custom ModelLoader)

まず、以下のようにライブラリを追加します。
(必要に応じて apply plugin: 'kotlin-kapt' を追記してください。)

//  https://mvnrepository.com/artifact/com.journeyapps/zxing-android-embedded
implementation("com.journeyapps:zxing-android-embedded:4.1.0")

//  https://mvnrepository.com/artifact/com.github.bumptech.glide/glide
implementation("com.github.bumptech.glide:glide:4.11.0")

//  https://mvnrepository.com/artifact/com.github.bumptech.glide/compiler
kapt("com.github.bumptech.glide:compiler:4.11.0")

次に、Glideの Model に相当する BarcodeRequest を定義します。
sealed class で定義し、Qr 型と Jan 型で表現しました。

package jp.co.betacomputing.barcodeapp

import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel

sealed class BarcodeRequest {

    abstract val content: String

    data class Qr(override val content: String, val level: ErrorCorrectionLevel) : BarcodeRequest()
    data class Jan(override val content: String) : BarcodeRequest()
}

次に ModelLoader<Model (= BarcodeRequest,) Data (= Bitmap)>DataFetcher<Data (= Bitmap)> の実装です。
BarcodeRequestwidthheight とともに Fetcher に渡し、そこで Bitmap を生成しています。

今回は特にネット経由で何かをダウンロードするといったことはないので、getDataSource()DataSource.LOCAL を指定しており、リソースの解放処理も不要なので clearnup() では何もしていません。
(公式のドキュメントのBase64の例でも同じような感じでしたね。)

loadData() で実際のバーコードの生成処理を行います。
オプションを指定して BarcodeEncoder().encodeBitmap() を叩くだけです。
生成に成功したら DataCallback#onDataReady() を叩いて完了を通知します。

package jp.co.betacomputing.barcodeapp

import android.graphics.Bitmap
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.signature.ObjectKey
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.journeyapps.barcodescanner.BarcodeEncoder
import jp.co.betacomputing.barcodeapp.BarcodeRequest as Req

internal class BarcodeModelLoader : ModelLoader<Req, Bitmap> {

    override fun buildLoadData(model: Req, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap>? =
        ModelLoader.LoadData(ObjectKey(model), Fetcher(model, width, height))

    override fun handles(model: Req): Boolean = true

    private class Fetcher(
        private val request: Req,
        private val width: Int,
        private val height: Int
    ) : DataFetcher<Bitmap> {

        override fun getDataClass(): Class<Bitmap> = Bitmap::class.java

        override fun cleanup() = Unit

        override fun getDataSource(): DataSource = DataSource.LOCAL

        override fun cancel() = Unit

        override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
            when (this.request) {
                is Req.Qr -> this.generateQr(this.request, callback)
                is Req.Jan -> this.generateJan(this.request, callback)
            }
        }

        private fun generateQr(request: Req.Qr, callback: DataFetcher.DataCallback<in Bitmap>) {
            try {
                val hints = mapOf(EncodeHintType.ERROR_CORRECTION to request.level)
                val bitmap = BarcodeEncoder()
                    .encodeBitmap(this.request.content, BarcodeFormat.QR_CODE, this.width, this.height, hints)

                callback.onDataReady(bitmap)
            } catch (e: Exception) {
                callback.onLoadFailed(e)
            }
        }

        private fun generateJan(request: Req.Jan, callback: DataFetcher.DataCallback<in Bitmap>) {
            try {
                val bitmap = BarcodeEncoder()
                    .encodeBitmap(request.content, BarcodeFormat.EAN_13, this.width, this.height)

                callback.onDataReady(bitmap)
            } catch (e: Exception) {
                callback.onLoadFailed(e)
            }
        }
    }
}

そして、この BarcodeModelLoader をGlideに登録する部分です。
これはほとんどドキュメント通りです。

package jp.co.betacomputing.barcodeapp

import android.content.Context
import android.graphics.Bitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.module.AppGlideModule

@GlideModule
internal class BarcodeGlideModule : AppGlideModule() {

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        registry.prepend(BarcodeRequest::class.java, Bitmap::class.java, Factory())
    }

    private class Factory : ModelLoaderFactory<BarcodeRequest, Bitmap> {

        override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<BarcodeRequest, Bitmap> =
            BarcodeModelLoader()

        override fun teardown() = Unit
    }
}

最後に、この BarcodeModelLoader を実際に使った簡単なアプリを作りました。
ボタンを押すと、このModel Loader経由でバーコードが生成され、ImageView に設定されます。

f:id:betacomputing3:20200824180053p:plain:w300

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/qrImageView"
        android:layout_width="256dp"
        android:layout_height="256dp"
        android:layout_margin="10dp"
        android:background="#ef5350"
        android:contentDescription="@null" />

    <ImageView
        android:id="@+id/janImageView"
        android:layout_width="256dp"
        android:layout_height="102dp"
        android:layout_margin="10dp"
        android:background="#42a5f5"
        android:contentDescription="@null" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="PUSH ME!"
        tools:ignore="HardcodedText" />

</LinearLayout>
package jp.co.betacomputing.barcodeapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.bumptech.glide.Glide
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        this.setContentView(R.layout.activity_main)
        this.button.setOnClickListener {

            //  QRコード
            Glide.with(this)
                .load(BarcodeRequest.Qr(content = "あいうえお", level = ErrorCorrectionLevel.H))
                .into(this.qrImageView)

            //  JANコード
            Glide.with(this)
                .load(BarcodeRequest.Jan(content = "020000100001"))
                .into(this.janImageView)
        }
    }
}

以上、GlideのカスタムModel Loaderを作って、バーコードを表示させる方法でした。
Glideのキャッシュやロード機能を使いたいけれども、画像の取得部分は自分で実装したいというときは是非やってみてください。