Google Mapでicon表示、User interaction - ItemizedOverlay、OverlayItemを使用
今回も前回のPostに続いてGoogle Maps APIを使用した実装をしてみたいと思います
前回やったこと
- MapActivity、MapViewを使用しmapを表示/ちょっとした操作
- LocationManagerを使用しUser location情報を取得
- MyLocationOverlayを使用しUser location(現在地)をmap上に表示
ということで今回は
やりたいこと
- ItemizedOverlayを使用しtouchされたmapの場所にOverlayItemを表示
- OverlayItemがtapされた場合は、それが保持するLocation情報を表示
こんな感じの表示(droidくんiconがOverlayItem。ToastでtapされたOverlayItemのLocationを表示)
実装の要点は下記です(前回実装したことに関しては省略)
1. ItemizedOverlayを拡張したclassを作成
public class MyItemizedOverlay extends ItemizedOverlay<OverlayItem> { //引数に渡されるindexのitemを返す @Override protected OverlayItem createItem(int i) { return mItems.get(i); } //itemの数を返す @Override public int size() { return mItems.size(); } }
2. overlayのlistにItemizedOverlayを登録
mItemizedOverlay = new MyItemizedOverlay(getResources().getDrawable(R.drawable.icon), this); List<Overlay> overlays = mMapView.getOverlays(); overlays.add(mItemizedOverlay);
3. Projection objectを取得する(OverlayItem表示/tap判定の為)
Projection pj = mapView.getProjection();
4. OverlayItem objectを作成
OverlayItem item = new OverlayItem(touchLocation, "test", "test"); Drawable drawable = mContext.getResources().getDrawable(R.drawable.icon_2); item.setMarker(drawable);
5. OverlayItemのicon表示の基準点をどこにするか設定
boundCenterBottom(item.getMarker(0));
6. ItemizedOverlayのOverlayItemに変更がある度にItemizedOverlay#populate()を呼ぶ
populate();
気付いた点を記載します
- MotionEventは使い回されているよ。変数に代入、保存し、次回MotionEventを受け取った際に、保存していたものを参照するとMotionEventの中身が今回のものに変わっている
- OverlayItem#setMarker()するDrawableはDrawable#setBounds()はしなくてもいい
- OverlayItem#setMarker()しないとtouchの際にcrashする
- ItemizedOverlay#boundCenter()もしくはItemizedOverlay#boundCenterBottom()を呼ばないと生成されたOverlayItemが表示されない
- onTap()はUser interactionを契機にOverlayItemを追加していくといった操作をした場合、OverlayItemを表示時も呼ばれてしまうので注意が必要
- ItemizedOverlay#onTap(GeoPoint p, MapView mapView)でtrueを返すとItemizedOverlay#onTap(int index)が呼ばれない
それでは実装です
MapActivityなど(OverlayにItemizedOverlay拡張classを登録するcode以外前回と同じ)
package com.android.practice.map; import java.util.List; import com.google.android.maps.GeoPoint; import com.google.android.maps.MapActivity; import com.google.android.maps.MapController; import com.google.android.maps.MapView; import com.google.android.maps.MyLocationOverlay; import com.google.android.maps.Overlay; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.util.Log; import android.widget.Toast; //MapActivityを拡張したclassを作成 public class MyMapActivity extends MapActivity{ private static final String TAG = "MyMapActivity"; private static final boolean DEBUG = true; private static final String ACTION_LOCATION_UPDATE = "com.android.practice.map.ACTION_LOCATION_UPDATE"; private MapController mMapController; private MapView mMapView; private MyLocationOverlay mMyLocationOverlay; private MyItemizedOverlay mItemizedOverlay; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); initMapSet(); } @Override protected void onResume() { super.onResume(); setOverlays(); setIntentFilterToReceiver(); requestLocationUpdates(); } @Override protected void onPause() { super.onPause(); resetOverlays(); removeUpdates(); } @Override protected void onDestroy() { super.onDestroy(); } @Override protected boolean isRouteDisplayed() { return false; } private void initMapSet(){ //MapView objectの取得 mMapView = (MapView)findViewById(R.id.map_view); //MapView#setBuiltInZoomControl()でZoom controlをbuilt-in moduleに任せる mMapView.setBuiltInZoomControls(true); //MapController objectを取得 mMapController = mMapView.getController(); } private void setOverlays(){ //User location表示用のMyLocationOverlay objectを取得 mMyLocationOverlay = new MyLocationOverlay(this, mMapView); //初めてLocation情報を受け取った時の処理を記載 //試しにそのLocationにanimationで移動し、zoom levelを19に変更 mMyLocationOverlay.runOnFirstFix(new Runnable(){ public void run(){ GeoPoint gp = mMyLocationOverlay.getMyLocation(); mMapController.animateTo(gp); mMapController.setZoom(19); } }); //LocationManagerからのLocation update取得 mMyLocationOverlay.enableMyLocation(); //OverlayItemを表示するためのMyItemizedOverlayを拡張したclassのobjectを取得 mItemizedOverlay = new MyItemizedOverlay(getResources().getDrawable(R.drawable.icon), this); //overlayのlistにMyLocationOverlayを登録 List<Overlay> overlays = mMapView.getOverlays(); overlays.add(mMyLocationOverlay); //overlayのlistに拡張したItemizedOverlayを拡張したclassを登録 overlays.add(mItemizedOverlay); } private void resetOverlays(){ //LocationManagerからのLocation update情報を取得をcancel mMyLocationOverlay.disableMyLocation(); //overlayのlistからMyLocationOverlayを削除 List<Overlay> overlays = mMapView.getOverlays(); overlays.remove(mMyLocationOverlay); //overlayのlistからItemizedOverlayを拡張したclassを削除 overlays.remove(mItemizedOverlay); } private void setIntentFilterToReceiver(){ final IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_LOCATION_UPDATE); registerReceiver(new LocationUpdateReceiver(), filter); } private void requestLocationUpdates(){ final PendingIntent requestLocation = getRequestLocationIntent(this); LocationManager lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE); for(String providerName: lm.getAllProviders()){ if(lm.isProviderEnabled(providerName)){ lm.requestLocationUpdates(providerName, 0, 0, requestLocation); if(DEBUG){ Toast.makeText(this, "Request Location Update", Toast.LENGTH_SHORT).show(); if(DEBUG)Log.d(TAG, "Provider: " + providerName); } } } } private PendingIntent getRequestLocationIntent(Context context){ return PendingIntent.getBroadcast(context, 0, new Intent(ACTION_LOCATION_UPDATE), PendingIntent.FLAG_UPDATE_CURRENT); } private void removeUpdates(){ final PendingIntent requestLocation = getRequestLocationIntent(this); LocationManager lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE); lm.removeUpdates(requestLocation); if(DEBUG)Toast.makeText(this, "Remove update", Toast.LENGTH_SHORT).show(); } public class LocationUpdateReceiver extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if(action == null){ return; } if(action.equals(ACTION_LOCATION_UPDATE)){ //Location情報を取得 Location loc = (Location)intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED); if(loc != null){ //試しにMapControllerで現在値に移動する mMapController.animateTo(new GeoPoint((int)(loc.getLatitude() * 1E6), (int)(loc.getLongitude() * 1E6))); if(DEBUG)Toast.makeText(context, "latitude:" + loc.getLatitude() + "\nlongitude:" + loc.getLongitude(), Toast.LENGTH_SHORT).show(); } } } } }
ItemizedOverlay拡張class
package com.android.practice.map; import java.util.ArrayList; import java.util.List; import android.content.Context; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.util.Log; import android.view.MotionEvent; import android.widget.Toast; import com.google.android.maps.GeoPoint; import com.google.android.maps.ItemizedOverlay; import com.google.android.maps.MapView; import com.google.android.maps.Overlay; import com.google.android.maps.OverlayItem; import com.google.android.maps.Projection; //ItemizedOverlayを拡張したclassを作成 public class MyItemizedOverlay extends ItemizedOverlay<OverlayItem> { private static final String TAG = "MyItemizedOverlay"; private static final boolean DEBUG = true; private static final int ACTION_MOVE_THRESHOLD = 20; private static final int NO_HIT = -1; private static enum TOUCH_STATE{ NONE, TAP, MOVE } private Context mContext; private TOUCH_STATE mTouchState; private Point mTouchStartPoint; private ArrayList<OverlayItem> mItems; public MyItemizedOverlay(Drawable defaultMarker, Context context){ super(defaultMarker); mContext = context; mItems = new ArrayList<OverlayItem>(); //populateをしないとcrashする populate(); } public void addItem(OverlayItem item){ mItems.add(item); if(DEBUG)Log.d(TAG, "addItem"); //表示対象のitem dataを更新したのでItemizedOverlay#populate()を呼んで再処理 populate(); } //引数に渡されるindexのitemを返す @Override protected OverlayItem createItem(int i) { if(DEBUG)Log.d(TAG, "createItem i: " + i); return mItems.get(i); } //itemの数を返す @Override public int size() { if(DEBUG)Log.d(TAG, "size: " + mItems.size()); return mItems.size(); } @Override public void draw(Canvas canvas, MapView mapView, boolean shadow) { super.draw(canvas, mapView, shadow); if(DEBUG)Log.d(TAG, "draw"); } @Override public boolean onTouchEvent(MotionEvent event, MapView mapView) { int act = event.getAction(); switch(act & MotionEvent.ACTION_MASK){ case MotionEvent.ACTION_DOWN: if(DEBUG)Log.d(TAG, "ACTION_DOWN"); mTouchState = TOUCH_STATE.TAP; if(mTouchStartPoint == null){ mTouchStartPoint = new Point(); mTouchStartPoint.x = (int)event.getX(); mTouchStartPoint.y = (int)event.getY(); } break; case MotionEvent.ACTION_MOVE: if(DEBUG)Log.d(TAG, "ACTION_MOVE"); if(mTouchState == TOUCH_STATE.TAP){ float curX = event.getX(); float curY = event.getY(); double dist = Math.sqrt(Math.pow((curX - mTouchStartPoint.x), 2) + Math.pow((curY - mTouchStartPoint.y), 2)); if(dist > ACTION_MOVE_THRESHOLD){ mTouchState = TOUCH_STATE.MOVE; } } break; case MotionEvent.ACTION_UP: if(DEBUG)Log.d(TAG, "ACTION_UP"); if(mTouchState == TOUCH_STATE.TAP){ //Projection objectを取得 Projection pj = mapView.getProjection(); //ItemizedOverlay#hitTest()というmethodがあったが上手くいかなかったので自分で実装 int hitIndex = hitTest(pj, (int)event.getX(), (int)event.getY()); if(hitIndex != -1){ OverlayItem item = mItems.get(hitIndex); Toast.makeText(mContext, "Latitude1E6:" + item.getPoint().getLatitudeE6() + "\nLongitude1E6:" + item.getPoint().getLongitudeE6(), Toast.LENGTH_SHORT).show(); }else{ //Projection#fromPixels()でtouchした位置のGeoPointを取得する GeoPoint touchLocation = pj.fromPixels((int)event.getX(), (int)event.getY()); //OverlayItem objectを作成 OverlayItem item = new OverlayItem(touchLocation, "test", "test"); Drawable drawable = mContext.getResources().getDrawable(R.drawable.icon_2); //OverlayItem#setMarker()するDrawableはDrawable#setBounds()はしなくてもいい //OverlayItem#setMarker()しないとtouchの際にcrashする item.setMarker(drawable); //OverlayItemのicon表示の基準点をどこにするか設定 //ItemizedOverlay#boundCenter()もしくはItemizedOverlay#boundCenterBottom()を呼ばないと生成されたOverlayItemが表示されない boundCenterBottom(item.getMarker(0)); addItem(item); } } mTouchState = TOUCH_STATE.NONE; mTouchStartPoint = null; break; default: return super.onTouchEvent(event, mapView); } return super.onTouchEvent(event, mapView); } private int hitTest(Projection pj, int hitX, int hitY){ int hitIndex = NO_HIT; for(int i = 0; i < mItems.size(); i++){ OverlayItem item = mItems.get(i); Point point = new Point(); pj.toPixels(item.getPoint(), point); int halfWidth = item.getMarker(0).getIntrinsicWidth() / 2; int left = point.x - halfWidth; int right = point.x + halfWidth; int top = point.y - item.getMarker(0).getIntrinsicHeight(); int bottom = point.y; if(DEBUG){ Log.d(TAG, "left: " + left); Log.d(TAG, "rihgt: " + right); Log.d(TAG, "top: " + top); Log.d(TAG, "bottom: " + bottom); Log.d(TAG, "hitX: " + hitX); Log.d(TAG, "hitY: " + hitY); } if(left <= hitX && hitX <= right){ if(top <= hitY && hitY <= bottom){ hitIndex = i; return i; } } } return hitIndex; } //onTap()はUser interactionを契機にOverlayItemを追加していくといった操作をした場合、OverlayItemを表示時も呼ばれてしまうので注意が必要 @Override public boolean onTap(GeoPoint p, MapView mapView) { if(DEBUG){ Log.d(TAG, "onTap - Latitude1E6:" + p.getLatitudeE6()); Log.d(TAG, "onTap - Longitude1E6:" + p.getLongitudeE6()); } //ItemizedOverlay#onTap(GeoPoint p, MapView mapView)でtrueを返すとItemizedOverlay#onTap(int index)が呼ばれない return super.onTap(p, mapView); } @Override protected boolean onTap(int index) { if(DEBUG){ Log.d(TAG, "onTap - index:" + index); } return super.onTap(index); }
と、こんな感じです。少ないcodeで表示したいものが簡単にmap上に表示されます。影も勝手についてくれます。ただ、draw時にreal timeでOverlayItemの数が変わってきたりするとArrayIndexOfBoundaryExceptionでcrashしたりとAPI userがどうしよう(?)も対処出来ないものがあるので使う際は気をつけたいと思います
HashTag #Java, #Android, #MapActivity, #ItemizedOverlay, #OverlayItem, #Projection