第3回 「ジェスチャの認識」
前回は、カメラ画面の上にAR画面を載せて画像を表示するプログラムを作成しました。メインの AimnShoot クラスはビューを追加する数行が増えただけで、画像表示は ARView クラスが担当するという機能分担でした。なお、カメラからプレビュー画面を送ってくれる CamView クラスは第1回から何の変更もありませんでしたが、これは今後の講義の中でも一切変更はないので、とりあえず忘れてしまってもかまいません。
以上をまとめると、
- ・AimnShoot
- 初期化、全体制御、情報交換を行う
- ・CamView
- 表示用 (後には解析用) のプレビュー画像をメインに送る
- ・ARView
- AR画面を表示する
という図式になります。この基本構造は今後も変わりません。
さて、前回までのアプリは、入力を受け付けないので、動かしてもあまり楽しいものではありませんでした。今回は、画面のタップ (指で軽く叩く) などのジェスチャを認識し、それに対応した動作をするようにアプリを拡張します。
ジェスチャの種類としてはタップ、ダブルタップ (2度叩く)、ホールド (長押しする)、フリック (サッとこする) などがありますが、呼び方はさまざまなので注意してください。なお、Android OS 2.1 以降ではマルチタッチ (2本以上の指で同時に画面に触れる操作) が可能になっていますが、本講座では広い範囲のモデルで動作するように、マルチタッチは扱っていません。ただ、本講座でのジェスチャの扱いを習得すれば、それを拡張することは容易なので、興味がある方はぜひ挑戦してみてください。
メインのアクティビティは、ジェスチャの認識部を含んだ結果、次のように変わります。
AimnShoot.java
package com.artiscc.aimnshoot;
import android.app.Activity;
import android.graphics.Point;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.Window;
import android.view.WindowManager;
import android.view.ViewGroup.LayoutParams;
public class AimnShoot extends Activity
{
private static final String TAG = "AimnShootMain";
private CamView mCamView = null;
private ARView mARView = null;
private GestureDetector mGDetector = null;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
mCamView = new CamView(this);
setContentView(mCamView);
mARView = new ARView(this);
addContentView(mARView, new LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.FILL_PARENT));
mGDetector = new GestureDetector(this, new OnGestureListener());
}
@Override
public boolean onTouchEvent(MotionEvent ev)
{
if(mGDetector != null){
mGDetector.onTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
class OnGestureListener extends GestureDetector.SimpleOnGestureListener
{
@Override
public boolean onDown(MotionEvent ev)
{
Log.e(TAG, "onDown: " + ev.toString());
return super.onDown(ev);
}
@Override
public boolean onSingleTapUp(MotionEvent ev)
{
Log.e(TAG, "onSingleTapUp: " + ev.toString());
mARView.setCoordBomb(new Point((int)ev.getX(), (int)ev.getY()));
return super.onSingleTapUp(ev);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent ev)
{
Log.e(TAG, "onSingleTapConfirmed: " + ev.toString());
return super.onSingleTapConfirmed(ev);
}
@Override
public boolean onDoubleTap(MotionEvent ev)
{
Log.e(TAG, "onDoubleTap: " + ev.toString());
return super.onDoubleTap(ev);
}
@Override
public boolean onDoubleTapEvent(MotionEvent ev)
{
Log.e(TAG, "onDoubleTapEvent: " + ev.toString());
return super.onDoubleTapEvent(ev);
}
@Override
public void onShowPress(MotionEvent ev)
{
Log.e(TAG, "onShowPress: " + ev.toString());
super.onShowPress(ev);
}
@Override
public void onLongPress(MotionEvent ev)
{
Log.e(TAG, "onLongPress: " + ev.toString());
mARView.setCoordReticle(new Point((int)ev.getX(), (int)ev.getY()));
mARView.setReticleLocked(true);
super.onLongPress(ev);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distX, float distY)
{
Log.e(TAG, "onScroll: " + e1.toString() + " : " + e2.toString());
return super.onScroll(e1, e2, distX, distY);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
{
Log.e(TAG, "onFling: " + e1.toString() + " : " + e2.toString());
mARView.toggleReticleLocked();
return super.onFling(e1, e2, velocityX, velocityY);
}
}
}
まず、onCreate() はジェスチャ認識用に1行追加になりました。さらに、onTouchEvent() をオーバーライドすることで、タッチイベントをジェスチャ認識クラスに送るようにします。その結果、ジェスチャは OnGestureListener クラスの各リスナー関数で受け取れるようになります。
各リスナー関数の詳細は省略しますが、上のコードにはそれぞれログを入れてあるので、実際に動かしてみると、どのジェスチャをどのように拾っているかが容易にわかると思います。サンプルコードは長くなってしまいましたが、このリスナークラスで受け取れるすべてのイベントが網羅してあるので、後々役立ててください。
ここでは、ジェスチャ認識に伴う動作の例として、下記のような動作をするように記述しました。それぞれは、ARView クラスの関数を呼び出すことにより実行されます。
- ・シングルタップ
- タップした位置に爆弾を表示する
- ・ホールド
- ホールドした位置に照準マークを表示する
- ・フリック
- 照準マークのロックをトグルする (意味は最後に説明します)
以上の動作を実現するために、ARView クラスは次のように変わります。
ARView.java
package com.artiscc.aimnshoot;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Point;
import android.view.View;
public class ARView extends View
{
private static final String TAG = "ARView";
public ARView(Context context)
{
super(context);
prepareImages();
}
@Override
public void onDraw(Canvas canvas)
{
Bitmap bmpReticle = isReticleLocked ? bmpReticleOn : bmpReticleOff;
if((bmpReticle != null) && (coordReticle != null)){
canvas.drawBitmap(bmpReticle, coordReticle.x - bmpReticle.getWidth()/2, coordReticle.y - bmpReticle.getHeight()/2, null);
}
if((bmpBomb != null) && (coordBomb != null)){
canvas.drawBitmap(bmpBomb, coordBomb.x - bmpBomb.getWidth()/2, coordBomb.y - bmpBomb.getHeight()/2, null);
}
}
private Bitmap bmpBomb = null;
private Bitmap bmpReticleOn = null;
private Bitmap bmpReticleOff = null;
private void prepareImages()
{
Resources r = getResources();
bmpBomb = BitmapFactory.decodeResource(r, R.drawable.bomb);
bmpReticleOn = BitmapFactory.decodeResource(r, R.drawable.reticleon);
bmpReticleOff = BitmapFactory.decodeResource(r, R.drawable.reticleoff);
}
private boolean isReticleLocked = true;
public void setReticleLocked(boolean val)
{
isReticleLocked = val;
invalidate();
}
public void toggleReticleLocked()
{
isReticleLocked = ! isReticleLocked;
invalidate();
}
private Point coordReticle = null;
public void setCoordReticle(Point p)
{
coordReticle = p;
invalidate();
}
private Point coordBomb = null;
public void setCoordBomb(Point p)
{
coordBomb = p;
invalidate();
}
}
ここで、新たに bmpReticleOff という画像が登場しましたが、これは目標の動きに追跡システムが対応できず、ロックオン状態が外れた場合に用いる照準マークです。もちろん、まだ目標の追跡系には手をつけていないので本来は不必要なのですが、後々いずれにしても使うものですから、今のうちに入れておいて動作例として利用することにします。画像は下のようなもので、前回の画像と同様にあらかじめリソースに入れておきます。
reticleoff.png |
ARViewクラスは、先に下の方から見ていきましょう。まず、prepareImages() では上の画像を含む 3 つの画像をロードしてビットマップに変換しています。次に、目標のロックオン状態を保持する isReticleLocked という変数を導入し、その状態を設定する setReticleLocked() と、その状態を反転させる toggleReticleLocked() とを定義します。なお、ロックが外れても、照準マークの座標は変わりませんから、その下の座標設定関数は前回のままとなっています。
onDraw() に関しては、bmpReticle というポインタを用意し、目標のロックオン状態に従って、オンの画像、オフの画像をそれぞれ指すように変更しました。その後は第2回とほぼ同様です。
第1部 ARアプリ作成の基本
第1回: カメラ画面の表示画面上にカメラのプレビュー画像を表示し続けるだけのアプリで、ARの基本中の基本です。
第2回: AR画面の表示
カメラ画面の上にAR画面を載せ、カメラ画面上にいろいろな画像を重ね合わせます。
第3回: ジェスチャの認識
タップ、ダブルタップ、長押し、フリックなどの画面操作を認識し、対応する処理を行います。
第4回: アニメーションの表示
タイマーを用いてミサイルが目標に向かって飛んでいく単純なアニメーションを表示します。
第5回: 複雑なアニメーションの表示
ミサイルが当たった後の爆発や残像の表示など、複雑なアニメーションを表示します。
第6回: 複数の目標の扱い
同時に複数個の目標を設定し、それぞれをミサイルで撃破する枠組みを導入します。
第7回: ズーム機能の導入
ズームボタンを設置し、カメラ画面をズームして表示する機構を手作りで実装します。
第8回: ネイティブコードの利用
ズーム機構を高速化するため、C、C++ を使ったネイティブコードを導入します。