Androidのアプリ開発関連のブログ

スキャン機能はだいたい出来上がった

制作中のQRリーダーは、スキャンしてテキストの表示する機能は大体できた。

QRの詳細情報の表示もしたかったが、Zxingのコード追ってみたが、やはり取得できなそう。
byte配列は取れそうなんで、それを解析処理と同じような処理作って詳細情報の取得も考えたが、かなりめんどいのと、マスクパターンに関してはかなり無理ありそうな感じに見えた。
というわけで、詳細情報の表示は無しの方向。

0.5秒間隔でプレビュー画像を解析して、スキャン成功したらAlertDialogでテキストの表示をするだけ。
広告は結果表示時だけにすることも考えたが、AlertDialogだと少なくとも普通にはAdMobは表示できない気配なんでスキャン画面に表示した。
メイン画面で広告つけちゃうと他のアプリと比べて一目でわかる優位性が無い気がするが、さほど邪魔ではないと思う。
結果表示は当初は別画面にすることも考えたが、AlertDialog使うのが簡単で再スキャンもしやすいUIにできると思うんでAlertDialogにした。

あと、画像ファイルに関連付けして画像ファイルを解析できるようにしたいと思ってる。

ZxingのライセンスはApache License 2.0ってやつで、ライセンス文書のコピーを添付するだけでいいみたい。
ぬるくて使いやすいですね。

QRリーダー制作中

QRリーダー制作中。


まずCameraのPreviewが止まっちゃう場合があってハマったが、
SurfaceHolder.CallbackのsurfaceCreatedでstartPreview()するとそうなるっぽい。
Camera.PreviewCallbackをsetPreviewCallback以外のsetPreviewCallbackWithBufferなんかも試したり、Previewを処理時に意図的に止めたりしてもダメだったが、surfaceCreatedではなくsurfaceChangedでstartPreview()したら問題なくなった。

ついでに、コールバックにTimer試したりもしたんだが、
setPreviewCallbackではなくsetOneShotPreviewCallbackだとコールバックが一回しか発生しない。
プレビューの間隔は端末依存で、手元のだと5fpsが最低ぽかった。
1秒1回も処理できればいいと思うんで、1回だけのsetOneShotPreviewCallbackでコールバックを設定して、
処理完了後にTimerで再度setOneShotPreviewCallbackを設定って方法で1秒間隔に変更した。


あと、カメラの向きとかアスペクト比なんかも考えなきゃと思ったんだが、端末を回転させると落ちることが判明した。
Camera.open()してる状態で再度open()が発生すると落ちるぽいんで、onCreateでopenではなく、onResumeでopenしてonPauseでreleaseにした。
releaseすると、release状態でプレビューの更新とかが発生すると例外が発生するんで、tryで回避した。

向きの変更はcamera.setDisplayOrientation(int)で変更できる(API14以降?)んだが、計算式がめんどくさい。
カメラの前背も考慮する必要がある様でよくわかってないが、リファレンスにサンプルコードが載ってるんで真似した。

アスペクト比は調整すると余白ができちゃうし、調整しなくても良いかなと思うことにした。


あと結果表示領域付けるだけでいいや。
できればQRコードの詳細情報を表示できるようにしたいんだが、ZxingだとDataMatrixの詳細は取得できそうなんだがQRの詳細は取得できなそうな・・・

ライブラリ・プロジェクトの作成

antでAndroidアプリを作る際、コード共通化のためにアプリを一部分離してライブラリ化したり、オープンソースのライブラリを組み込む方法。


android create lib-project -n プロジェクト名 -t ターゲット -p パス -k パッケージ名
アプリのプロジェクトを作る場合は、android create projecですが、 android create lib-projectにするだけですね。
-tで指定するターゲットは、android list targets」で確認できるid
lib-projectを作成した場合はsrc以下に何もできないので、srcに.javaファイルをぶち込むだけ。


アプリ側で
ライブラリを使用する場合は、アプリ側のproject.propertiesに、
android.library.reference.1=相対パス
と1行。
2個以上のライブラリを使用する場合は、android.library.reference.2


普通にantでビルドする場合はjavacのsourceオプションが1.5になって、オープンソースのライブラリ使ったりする場合にエラーが出るかもしれないが、
lib-project側のant.propertiesに、
java.source=7
java.target=7
とかするとsourceオプションが指定できる。

また1ヶ月空いてしまったが・・・

結局また1ヶ月何も作らなかったが、再開する。
定番だが、QRリーダー作ってみようかと思ってたわけで、先月調べたところSurfaceViewというアニメーション等の更新が必要なものを表示するViewがあり、カメラのプレビューにはそれ使う。
というわけで、まずはSurfaceViewにカメラの表示。


AndroidManifest.xml
<uses-feature android:name="android.hardware.camera"/>
<uses-permission android:name="android.permission.CAMERA"/>
この2行を、<manifest>の下(<application>の前に書いた)に書いとく。
<uses-permission>でアプリがカメラを使えるようにして、<uses-feature>を書いておくとアプリ配信時にカメラ無し端末はGooglePlayに表示されなくなる。


APIリファレンスだとカメラはandroid.hardware.Cameraが非推薦でandroid.hardware.camera2を使うように書かれているが、camera2はAPIレベル21。
API 21はAndroid 5.0かな?流石に要件厳しすぎるんでandroid.hardware.Cameraの方使うことに。


MainActivity.java
import android.app.Activity;
import android.os.Bundle;
import android.view.SurfaceView;
import android.view.SurfaceHolder;
import android.hardware.Camera;

public class MainActivity extends Activity{
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final Camera camera;
        if(Camera.getNumberOfCameras()==1){
            camera=Camera.open();
        }else{
            Camera.CameraInfo ci=new Camera.CameraInfo();
            Camera.getCameraInfo(0,ci);
            if(ci.facing==ci.CAMERA_FACING_BACK){
                camera=Camera.open(0);
            }else{
                camera=Camera.open(1);
            }
        }
        SurfaceView sv=(SurfaceView)findViewById(R.id.sv);
        sv.getHolder().addCallback(new SurfaceHolder.Callback(){
            public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){
           
            }
            public void surfaceCreated(SurfaceHolder holder){
                try{
                    camera.setPreviewDisplay(holder);
                    camera.startPreview();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
            public void surfaceDestroyed(SurfaceHolder holder){
           
            }
        });
    }
}

40行ほどになってしまったが、半分は背面カメラの判別コード。
最初のお試しにはカメラの判別はしなくていいと思うんで、もっと簡単。

layoutファイルでSurfaceViewを定義。
コード中でidでSurfaceViewを取得して、getHolder()でSurfaceHolderを取得。
addCallbackでコールバックを設定。
addCallbackの引数はSurfaceHolder.Callback一つで、SurfaceHolder.Callbackはイベントリスナ的なやつかな?surfaceChanged、surfaceCreated、surfaceDestroyedの3つを定義する。
今回はお試しなんで、surfaceCreatedだけ定義した。これがSurfaceView作成時に1回だけ呼びだされて、内容変更時にsurfaceChanged、破棄時にsurfaceDestroyedが呼び出される感じかな。

Camera.open()で1番目のカメラが起動、カメラ番号を引数に渡すこともできCamera.open(1)だと2番目になるのかな?
Camera.getNumberOfCameras()で端末のカメラ数を取得できる。

SurfaceView作成時に呼び出されるsurfaceCreatedでカメラのプレビューを開始してSurfaceViewに描画する。
起動中のカメラのsetPreviewDisplay(holder)でSurfaceHolderを設定。
startPreview()するだけ。
tryで例外処理する必要があるけど、簡単だね。


今回はカメラが複数ある場合は背面カメラを使い、一つの場合はそれを起動。としたかったわけだが、
Callback内(サブクラス内)でカメラの取得が必要そうなんで、final修飾子つけた。
finalの時はその行で初期化しないとダメなもんだと思ってたが、確実に定義されるなら次以降のif文等で定義することもできるんだね。
対象カメラの判別をしてから定義する必要があるんで、final変数をif文内で初期化にした。
カメラが複数の場合はforで全カメラを判別すべきな気もするが、for内でfinal変数の初期化はできない?やりかたわからん。
端末にカメラが3個以上付いているというのはありえないよね?2個あるなら1番か2番のカメラのどちらかが確実に背面だと思うんで、forじゃなくてif文にした。

カメラの前背面の判別は、Camera.CameraInfo()で判別できるんだが、なんかわかりにくい。
static void Camera.getCameraInfo(int cameraId, Camera.CameraInfo cameraInfo)
↑これ
Cameraのstaticメソッドを使うんだが、直接CameraInfoを返してくれる仕様ならわかりやすいが、voidなんだよね。
空のCameraInfoを自分で作成してgetCameraInfoの引数として渡す。そうすると渡したCameraInfoで情報を得ることができるって仕様。
背面なら、取得したCameraInfoのfacingが、Camera.CameraInfo.CAMERA_FACING_BACKになる。
前面なら、Camera.CameraInfo.CAMERA_FACING_FRONT。



そんなわけで、カメラを扱うのはできた。
QRの解析にはZxingって定番ぽいライブラリ使うつもりだが、プレビュー中のカメラから自動で定期的にビットマップを取得してZxingで処理する感じだね。
やる気出せばとりあえずのものは1日でできると思うが、やる気が出るか・・・