準備
ScalaでAndroidアプリ開発環境を構築するという記事の実践編(?)。
前回の記事を参考に、Scala用Androidプロジェクトを作成しておきます。
ZXingライブラリを追加
上記のURLから"ZXing-2.1.zip"をダウンロードします。
(へたに、ZXing-2.2.zipをダウンロードすると、必要なjarファイルが含まれていないので注意。)
解凍して、下記のファイルをプロジェクト/libsに追加してやります。
core/core.jar
javase/javase.jar
AndroidManifest.xmlでカメラの使用を許可する
Androidでカメラを使うにはマニフェストファイルで宣言しておく必要があります。
<uses-sdk (略) />
の後ろに、以下の宣言を追加しましょう。
<uses-permission android:name="android.permission.CAMERA" />
SurfaceViewを用意
res/layout/activity_main.xmlを書き換えて、カメラのプレビューを表示するためのSurfaceViewを用意します。
<RelativeLayout 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=".MainActivity" >
<SurfaceView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
/>
</RelativeLayout>
LuminanceSourceクラスを継承したクラスを作成
JavaでZXingを使う場合、サンプルにあるPlanarYUVLuminanceSource.javaをコピーして使うのが定番(?)っぽいですが、ここでは、Scalaで書き直したものを使用します。
package com.example.scalaqr
import com.google.zxing.LuminanceSource
import android.graphics.Bitmap
final class YUVLuminanceSource(val yuvData: Array[Byte], val dataWidth:Int, val dataHeight: Int,
val left: Int, val top: Int, val width: Int, val height: Int) extends LuminanceSource(width, height)
{
if (left + width > dataWidth || top + height > dataHeight) {
throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
}
override def getRow(y: Int, row: Array[Byte]): Array[Byte] = {
if (y < 0 || y >= getHeight()) {
throw new IllegalArgumentException("Requested row is outside the image: " + y)
}
val width = getWidth()
var vRow = row
if (vRow == null || vRow.length < width) {
vRow = new Array[Byte](width)
}
val offset = (y + top) * dataWidth + left
System.arraycopy(yuvData, offset, vRow, 0, width)
vRow
}
override def getMatrix(): Array[Byte] = {
val width = getWidth()
val height = getHeight()
// If the caller asks for the entire underlying image, save the copy and give them the
// original data. The docs specifically warn that result.length must be ignored.
if (width == dataWidth && height == dataHeight) {
return yuvData
}
val area = width * height
val matrix = new Array[Byte](area)
var inputOffset = top * dataWidth + left
// If the width matches the full width of the underlying data, perform a single copy.
if (width == dataWidth) {
System.arraycopy(yuvData, inputOffset, matrix, 0, area)
return matrix
}
// Otherwise copy one cropped row at a time.
val yuv = yuvData;
for(y <- 0 to height) {
//for (int y = 0; y < height; y++) {
val outputOffset = y * width
System.arraycopy(yuv, inputOffset, matrix, outputOffset, width)
inputOffset += dataWidth
}
matrix
}
override def isCropSupported(): Boolean = true
}
必要がなさそうなメソッドは省いていますので、もしも必要になった場合には各自で実装してください。
MainActivityをScalaで書く
src/MainActivity.javaを削除して、新たにsrc/MainActivity.scalaクラスを作成します。
package com.example.scalaqr
import scala.collection.JavaConversions._
import scala.util.control.Breaks.{ break, breakable }
import _root_.android.app.Activity
import _root_.android.os.Bundle
import android.os.Bundle
import android.app.Activity
import android.view.Menu
import android.content.Context
import android.util.Log
import android.graphics.Point
import android.hardware.Camera
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.View.OnClickListener
import android.view.WindowManager
import android.widget.Toast
import android.widget.Button
import com.google.zxing.BinaryBitmap
import com.google.zxing.MultiFormatReader
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer
class MainActivity extends Activity {
private val TAG = "ScalaQR"
private val MIN_PREVIEW_PIXCELS = 320 * 240
private val MAX_PREVIEW_PIXCELS = 800 * 480
private var mSurfaceView: SurfaceView = null
override protected def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mSurfaceView = findViewById(R.id.preview_view).asInstanceOf[SurfaceView]
mSurfaceView.setOnClickListener(mSurfaceHolderCallback)
val holder = mSurfaceView.getHolder()
holder.addCallback(mSurfaceHolderCallback)
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
}
override def onCreateOptionsMenu(menu: Menu): Boolean = {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu)
true
}
protected def isPortrait: Boolean =
getResources().getConfiguration().orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT
//
// SurfaceHolderCallback methods
//
private val mSurfaceHolderCallback:SurfaceHolder.Callback
with OnClickListener
with Camera.AutoFocusCallback
= new SurfaceHolder.Callback()
with OnClickListener
with Camera.AutoFocusCallback
{
private var mSurfaceHolder: SurfaceHolder = null
private var mCamera: Camera = null
//
// OnClickListener
//
override def onClick(v: View) {
if(v == mSurfaceView){
// オートフォーカスON
mCamera.autoFocus(mSurfaceHolderCallback)
}
}
//
// 生成されたとき
//
override def surfaceCreated(holder: SurfaceHolder) {
mSurfaceHolder = holder
try {
// プレビューをセットする
mCamera = Camera.open()
mCamera.setPreviewDisplay(holder)
mCamera.setPreviewCallback(mPreviewCallback)
} catch {
case e: Exception => e.printStackTrace()
}
}
//
// 変更されたとき
//
override def surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
val parameters = mCamera.getParameters()
val p = pointFromCameraParameters(mCamera)
parameters.setPreviewSize(p.x, p.y)
if (isPortrait) {
mCamera.setDisplayOrientation(90)
} else {
mCamera.setDisplayOrientation(0)
}
// width, heightを変更する
mCamera.setParameters(parameters)
mCamera.startPreview()
}
//
// 破棄されたとき
//
override def surfaceDestroyed(holder: SurfaceHolder) {
mCamera.release()
}
//
// Camera.AutoFocusCallback methods
//
override def onAutoFocus(success: Boolean, camera: Camera) {
if (success) // 現在のプレビューをデータに変換
camera.setOneShotPreviewCallback(mPreviewCallback)
}
private def pointFromCameraParameters(camera: Camera): Point = {
val parameters = camera.getParameters()
val manager: WindowManager =
getApplication().getSystemService(Context.WINDOW_SERVICE).asInstanceOf[WindowManager]
val display = manager.getDefaultDisplay()
val width = display.getWidth()
val height = display.getHeight()
val screenSize = new Point(width, height)
//Log.d(TAG, "screenSize = " + screenSize.toString())
val previewSize = findPreviewSize(parameters, screenSize, false)
//Log.d(TAG, "previewSize = " + previewSize.toString())
previewSize
}
private def findPreviewSize(parameters: Camera#Parameters, screenSize: Point, portrait: Boolean): Point = {
var previewSize: Point = null
val list = parameters.getSupportedPreviewSizes()
breakable {
for (
supportPreviewSize <- list
if supportPreviewSize.width * supportPreviewSize.height >= MIN_PREVIEW_PIXCELS
if supportPreviewSize.width * supportPreviewSize.height <= MAX_PREVIEW_PIXCELS
) {
val supportedWidth = if (portrait) supportPreviewSize.height else supportPreviewSize.width
val supportedHeight = if (portrait) supportPreviewSize.width else supportPreviewSize.height
previewSize = new Point(supportedWidth, supportedHeight)
break
}
}
if (previewSize == null) {
val defaultPreviewSize = parameters.getPreviewSize()
previewSize = new Point(defaultPreviewSize.width, defaultPreviewSize.height)
}
//Log.d(TAG, "found previewPoint = " + previewSize.toString())
previewSize
}
}
//
// オートフォーカスでピントがあったときに呼ばれる(?)
// QRコードを解析する
//
private val mPreviewCallback = new Camera.PreviewCallback() {
//
// Camera.PreviewCallback methods
//
override def onPreviewFrame(data: Array[Byte], camera: Camera) {
//
// バーコード読み取り
//
// 読み込む範囲
val previewWidth = camera.getParameters().getPreviewSize().width
val previewHeight = camera.getParameters().getPreviewSize().height
// プレビューデータから BinaryBitmap を生成
val source = new YUVLuminanceSource(
data, previewWidth, previewHeight, 0, 0, previewWidth, previewHeight)
val bitmap = new BinaryBitmap(new HybridBinarizer(source))
// バーコードを読み込む
val reader = new MultiFormatReader()
var result: Result = null
try {
result = reader.decode(bitmap)
} catch {
case e: Exception => Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show()
}
if (result == null) {
Toast.makeText(getApplicationContext(), "can not scan QR code.", Toast.LENGTH_LONG).show()
} else {
val text = result.getText()
Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show()
}
}
}
}
問題がなければ、これで動くはずです。
起動できたら、画面を一度タップするとQRコードの読み取りを始めます。
うまく動かない場合には、プロジェクトの作成から確認しましょう。
特に、前回の記事のなかの「右クリック->"Add AndroidProguardScala Nature"」を忘れがちなので注意。
補足
画面が表示されるのに、QRコードが読み取れないときには、
うまく動いてないように見えても実は動いていることがあります。
ピントが合うのがめちゃくちゃ遅いです。
実用性は皆無ですが、一応こんなことも出来るということで…。