2010年10月26日火曜日

タッチイベントはMapViewとItemizedOverlayのどちらにまず伝わるのか

Android+Google MAP APIでタッチイベントを処理する時のイベントの流れを確認する。
使ったのは以下のクラス。

・MapDemoActivityクラス
MapActivityを継承したクラス。実行クラス。

・CusomizedMapViewクラス
MapViewを継承したクラス。MapDemoActivityのContentViewとして設定。

・GeoItemizedOverlayクラス
ItemizedOverlayを継承したクラス。


これらのクラスは、下から積み上げると以下のような階層関係にある。

GeoItemizedOverlay
|
CusomizedMapView
|
MapDemoActivity


・検証

ディバイスの画面にタッチしたときには、onTouchEvent(MotionEvent event)が呼び出される。

MapViewを使ったときは、Activityに記述したonTouchEventではなくMapView側のonTouchEventが呼び出されることは、前の記事に書いた。

では、MapViewにItemizedOverlayが追加されているときは、タッチイベントはMapViewとItemizedOverlayのどちらにまず伝わるのか。

public class MapDemoActivity extends MapActivity {
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  /** マップビュー作成 */
  CustomizedMapView mapView = (CustomizedMapView) findViewById(R.id.mapview);

  Drawable drawable = this.getResources().getDrawable(R.drawable.star);
  GeoItemizedOverlay geoOverlay = new GeoItemizedOverlay(drawable, this);

  /** オーバーレイをマップに追加 */
  List<Overlay> mapOverlays = mapView.getOverlays();
  mapOverlays.add(geoOverlay);
 }
}
public class CustomizedMapView extends MapView {
 @Override
 public boolean onTouchEvent(MotionEvent event) {
  Log.i("onTouchEvent", "CustomizedMapView.onTouchEvent");
  return super.onTouchEvent(event);
 }
}
public class GeoItemizedOverlay extends ItemizedOverlay<OverlayItem> {
 @Override
 public boolean onTouchEvent(MotionEvent event, MapView mapView) {
  Log.i("onTouchEvent", "GeoItemizedOverlay.onTouchEvent");
  return super.onTouchEvent(event, mapView);
 }
}

試してところ、ItemizedOverlayよりも先にMapViewへタッチイベントが伝わることが分かった。
10-26 13:29:55.577: INFO/MapDemo(4253): CustomizedMapView.onTouchEvent
10-26 13:29:55.577: INFO/MapDemo(4253): GeoItemizedOverlay.onTouchEvent

上記のクラスで言うと、

CustomizedMapView→GeoItemizedOverlay

の順番だ。


・結論

MapView側に記述してあるonTouchEventがまず呼ばれる。

そして、MapViewのonTouchEventの最後に実行する、super.onTouchEvent(event)により、MapViewに追加されている各Overlayへタッチイベントが伝えられていく、という流れのようだ。

ItemizedOverlayをMapViewに追加しているときにアイテムが無いとNullPointerExceptionが発生するバグへの対応

ItemizedOverlayがMapViewに追加されているときは、MapViewクラスのonTouchEvent(MotionEvent event)が実行される際に、ItemizedOverlayのonTouchEvent(MotionEvent event)にもイベントが投げられる。

問題は、それがアイテム数が0の時にも起こるので、アイテム数が0の時に画面にタッチするとNullPointerExceptionが発生すること。

10-26 12:00:24.607: ERROR/AndroidRuntime(2753): java.lang.NullPointerException
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.ItemizedOverlay.getItemsAtLocation(ItemizedOverlay.java:617)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.ItemizedOverlay.getItemAtLocation(ItemizedOverlay.java:586)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.ItemizedOverlay.handleMotionEvent(ItemizedOverlay.java:498)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.ItemizedOverlay.onTouchEvent(ItemizedOverlay.java:572)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at hoge.MapDemo.GeoItemizedOverlay.onTouchEvent(GeoItemizedOverlay.java:54)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.OverlayBundle.onTouchEvent(OverlayBundle.java:63)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.MapView.onTouchEvent(MapView.java:625)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at hoge.MapDemo.CustomizedMapView.onTouchEvent(CustomizedMapView.java:52)

今の所この現象を避けるには、ItemizedOverlayを継承したクラスのコンストラクタで、super(boundCenterBottom(defaultMarker));の後で、populate()を呼ぶこと。


・ItemizedOverlayを使うときは、必ずコンストラクタ内でpopulate()を呼ぶ

populate()はアイテムを追加した後に必ず呼ぶメソッドだが、アイテムがあろうがなかろうが、最初からコールしておくということ。これでNullPointerExceptionは発生しなくなる。

public class GeoItemizedOverlay extends ItemizedOverlay<OverlayItem> {

 private ArrayList<OverlayItem> mOverlays = new ArrayList<OverlayItem>();
 private Context mContext;

 public GeoItemizedOverlay(Drawable defaultMarker, Context context) {
  super(boundCenterBottom(defaultMarker));
  mContext = context;
  populate();//これが大事!
 }
}

このバグはIssueとして管理されているので、早めに解決して欲しい方はvoteしてください。
Issue 2035 - android - NullPointerException when scrolling through a MapView with an ItemizedOverlay with no OverlayItems - Project Hosting on Google Code

MapView継承クラスでonTouchEvent()を処理するときに気をつけること

MapViewを利用すると、MapActivityを継承したクラス内にonTouchEvent(MotionEvent event)を書いてもイベントをキャッチできない。

以下のようなコードを書いても、MapViewの方にイベントが取られて、ログが出力されない。

public class MapDemo extends MapActivity {
 @Override
 public boolean onTouchEvent(MotionEvent event) {

  String action = "";
  switch (event.getAction()) {
  case MotionEvent.ACTION_DOWN:
   action = "ACTION_DOWN";
   break;
  case MotionEvent.ACTION_UP:
   action = "ACTION_UP";
   break;
  case MotionEvent.ACTION_MOVE:
   action = "ACTION_MOVE";
   break;
  case MotionEvent.ACTION_CANCEL:
   action = "ACTION_CANCEL";
   break;
  }

  Log.i("onTouchEvent", "action = " + action + ", " + "x = "
    + String.valueOf(event.getX()) + ", " + "y = "
    + String.valueOf(event.getY()));

  return super.onTouchEvent(event);
 }
}

そのため、MapViewクラスを継承したオリジナルのMapViewクラスを作成し、そのクラス内にonTouchEvent(MotionEvent event)を記述した。その時に気をつけること。


・MapViewクラスを継承する場合は、MapViewクラスのコンストラクタMapView(Context context, AttributeSet attrs)を必ずオーバーライドする

そうしないと、デフォルトのズームコントローラーを使ったり、フリップしてマップを動かすことができない。
静止画像のようにマップが表示されるだけの画面になってしまう。

コードは以下。

public class CustomizedMapView extends MapView {
 public CustomizedMapView(Context context, AttributeSet attrs) {
  super(context, attrs);
 }
 @Override
 public boolean onTouchEvent(MotionEvent event) {

  String action = "";
  switch (event.getAction()) {
  case MotionEvent.ACTION_DOWN:
   action = "ACTION_DOWN";
   break;
  case MotionEvent.ACTION_UP:
   action = "ACTION_UP";
   break;
  case MotionEvent.ACTION_MOVE:
   action = "ACTION_MOVE";
   break;
  case MotionEvent.ACTION_CANCEL:
   action = "ACTION_CANCEL";
   break;
  }

  Log.i("onTouchEvent", "action = " + action + ", " + "x = "
    + String.valueOf(event.getX()) + ", " + "y = "
    + String.valueOf(event.getY()));

  return super.onTouchEvent(event);
 }
}
この時のレイアウトは以下。
<?xml version="1.0" encoding="utf-8"?>
<hoge.MapDemo.CustomizedMapView
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/mapview"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:clickable="true"
 android:apiKey="apiKey" />
MapView呼び出しはこんな感じ。
public class MapDemoActivity extends MapActivity {
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  /** マップビュー作成 */
  CustomizedMapView mapView = (CustomizedMapView) findViewById(R.id.mapview);
 }
}