Google Mapでicon表示、User interaction - ItemizedOverlay、OverlayItemを使用

今回も前回のPostに続いてGoogle Maps APIを使用した実装をしてみたいと思います

前回やったこと

  1. MapActivity、MapViewを使用しmapを表示/ちょっとした操作
  2. LocationManagerを使用しUser location情報を取得
  3. MyLocationOverlayを使用しUser location(現在地)をmap上に表示

ということで今回は

やりたいこと

  1. ItemizedOverlayを使用しtouchされたmapの場所にOverlayItemを表示
  2. 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