一応、最後にしたいGoogle Map - Overlayの拡張

今回のPostで今までやってきたGoogle Maps APIを使用した実装の最後としたいと思います。ItemizedOverlayを実装した回のあとがきで少しふれましたが、ItemizedOverlayを使用した場合、Multi ThreadでReal timeにOverlayItemの数などを変更するとAPIの内部でcrashしてしまう場合がありAPIのuserがどうにも対処出来ない(?)問題があります。ということで今回はもう少しcustomizeが可能なOverlayを拡張してみたいと思います

今回やりたいこと

  1. Overlay拡張classを使用しmap上にiconを表示
  2. iconをtapするとiconに紐づけられたstringを表示

ちなみに前回やったこと

  1. MapViewを拡張しZoom level変更event、Center position変更eventを取得(Listenerに通知も)


こんな感じでiconを表示し、iconのtapでtoastの表示

実装の要点は下記です(前回実装したことに関しては省略)

1. Overlayを拡張したclassを作成

public class MyOverlay extends Overlay {
	
        public synchronized void setItems(ArrayList<MyOverlayItem> items){
		mItems = items;
	}

	@Override
	public synchronized void draw(Canvas canvas, MapView mapView, boolean shadow) {

	}
	
	@Override
	public synchronized boolean onTap(GeoPoint p, MapView mapView) {
		return super.onTap(p, mapView);
	}
}

2. Overlay#draw()をoverrideし保持しているMyOverlayItemのiconを描画する処理を記載

@Override
public synchronized void draw(Canvas canvas, MapView mapView, boolean shadow) {

	//shadowがtrueの場合、Iconの影を描くためのdrawなのでIconのみを描く場合は無視
	if(!shadow){
		Projection pj = mapView.getProjection();
		Point point = new Point();
		for(MyOverlayItem item: mItems){
			pj.toPixels(item.getGeoPoint(), point);
			Drawable icon = item.getIcon();
			
			Rect bounds = new Rect();
			int halfWidth = icon.getIntrinsicWidth() / 2;
				
			bounds.left = point.x - halfWidth;
			bounds.right = point.x + halfWidth;
			bounds.top = point.y - icon.getIntrinsicHeight();
			bounds.bottom = point.y;
				
			icon.setBounds(bounds);
			icon.draw(canvas);
		}
	}
}

3. Overlay#onTap()をoverrideしMyOverlayItemをtapした場合はToastを表示する処理を記載

@Override
public synchronized boolean onTap(GeoPoint p, MapView mapView) {
	Projection pj = mapView.getProjection();
	int hitIndex = hitTest(pj, p);
		
	if(hitIndex != -1){
		MyOverlayItem item = mItems.get(hitIndex);
		Toast.makeText(mContext, item.getString(), Toast.LENGTH_SHORT).show();
	}
		
	return super.onTap(p, mapView);
}

4. Overlay拡張classで表示する(要素を保持する)classを作成

public class MyOverlayItem {
	
}

5. 独自のOverlayを表示するためOverlayを拡張したclassのobjectを取得

mMyOverlay = new MyOverlay(this);


6. MyOverlayItem(表示要素を保持するclass)のobject listを取得しOverlay拡張classにset

ArrayList<MyOverlayItem> items = getMyOverlayItems();
mMyOverlay.setItems(items);

7. overlayのlistにOverlayを拡張したclassを登録

List<Overlay> overlays = mMapView.getOverlays();
overlays.add(mMyOverlay);

気付いた点

  • Overlay#onTap()はOverlay#onTouch()が呼ばれた後に呼ばれる

んで実装です

MapActivityなど(Overlay拡張classに関係するcode以外前回と同じ)

package com.android.practice.map;

import java.util.ArrayList;
import java.util.List;

import com.android.practice.map.MyMapView.PanListener;
import com.android.practice.map.MyMapView.ZoomListener;
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 MyMapView mMapView;
	private MyLocationOverlay mMyLocationOverlay;
	private MyItemizedOverlay mItemizedOverlay;
	private MyOverlay mMyOverlay;
	
	private MyMapViewListener mMyMapViewListener; 
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        initMapSet();
        setMapEventListeners();
    }
    
	@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(){
		//MyMapView objectの取得
        mMapView = (MyMapView)findViewById(R.id.map_view);
        //MapView#setBuiltInZoomControl()でZoom controlをbuilt-in moduleに任せる
        mMapView.setBuiltInZoomControls(true);
        //MapController objectを取得
        //mMapController = mMapView.getController();
	}
	
	//作成したMapView event listener classをlistenerとしてMapView拡張classに登録
	private void setMapEventListeners(){
		mMyMapViewListener = new MyMapViewListener();
		if(mMapView != null){
			mMapView.setZoomListener(mMyMapViewListener);
			mMapView.setPanListener(mMyMapViewListener);
		}
	}
	
	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();
				mMapView.animateTo(gp);
				mMapView.setZoom(19);
        	}
        });
        //LocationManagerからのLocation update取得
		mMyLocationOverlay.enableMyLocation();
		
		//OverlayItemを表示するためのMyItemizedOverlayを拡張したclassのobjectを取得
		mItemizedOverlay = new MyItemizedOverlay(getResources().getDrawable(R.drawable.icon), this);
		
		//独自のOverlayを表示するためOverlayを拡張したclassのobjectを取得
		setMyOverlay();
		
		//overlayのlistにMyLocationOverlayを登録
        List<Overlay> overlays = mMapView.getOverlays();
        //overlayのlistにOverlayを拡張したclassを登録
        overlays.add(mMyOverlay);
        overlays.add(mMyLocationOverlay);
        //overlayのlistに拡張したItemizedOverlayを拡張したclassを登録
        overlays.add(mItemizedOverlay);
	}
	
	private void setMyOverlay(){
		mMyOverlay = new MyOverlay(this);
		//Overlay拡張classで表示するMyOverlayItemを取得
		ArrayList<MyOverlayItem> items = getMyOverlayItems();
		//MyOverlayItemをOverlay拡張classにset
		mMyOverlay.setItems(items);
	}
	
	//今回は適当に取得
	private ArrayList<MyOverlayItem> getMyOverlayItems() {
		ArrayList<MyOverlayItem> items = new ArrayList<MyOverlayItem>();
		for(int i = 0; i < 3; i++){
			MyOverlayItem item = new MyOverlayItem();
			item.setIcon(getResources().getDrawable(R.drawable.pin_map_1 + i));
			item.setGeoPoint(new GeoPoint(38994104 + (i * 100), 141317551 + (i * 1000)));
			item.setString("Pin " + i);
			items.add(item);
		}
		return items;
	}

	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();    	
    }
	
	private 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で現在値に移動する
					mMapView.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();
				}
			}
		}
	}
	
	//Zoom level変更/Center position変更event listenerを作成
	private class MyMapViewListener implements ZoomListener, PanListener{
		
		@Override
		public void onZoomChange(MapView mapView, int newZoom, int oldZoom) {
			Toast.makeText(MyMapActivity.this, "New zoom lv:" + newZoom + "\nOld zoom lv:" + oldZoom, Toast.LENGTH_SHORT).show();
		}

		@Override
		public void onCenterChange(MapView mapView, GeoPoint newGeoPoint,
				GeoPoint oldGeoPoint) {
			Toast.makeText(MyMapActivity.this, 
					"New center latitude:" + newGeoPoint.getLatitudeE6() + "\nNew center longitude:" + newGeoPoint.getLongitudeE6(), 
					Toast.LENGTH_SHORT).show();
		}
	}
}

Overlay拡張class

package com.android.practice.map;

import java.util.ArrayList;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
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.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;

//Overlayを拡張したclassを作成
public class MyOverlay extends Overlay {
	
	private static final String TAG = "MyOverlay";
	private static final boolean DEBUG = true;
	
	private static final int NO_HIT = -1;
	
	private final Context mContext;
	private ArrayList<MyOverlayItem> mItems;
	
	public MyOverlay(Context context){
		mContext = context;
		mItems = new ArrayList<MyOverlayItem>();
	}
	
	public synchronized void setItems(ArrayList<MyOverlayItem> items){
		mItems = items;
	}
	
	@Override
	public boolean draw(Canvas canvas, MapView mapView, boolean shadow,
			long when) {
		if(DEBUG){
			Log.d(TAG, "draw(Canvas canvas, MapView mapView, boolean shadow, long when)");
			Log.d(TAG, "shadow: " + shadow);
			Log.d(TAG, "when: " + when);
		}
		
		return super.draw(canvas, mapView, shadow, when);
	}
	
	//Overlay#draw()をoverrideし保持しているMyOverlayItemのiconを描画する処理を記載
	@Override
	public synchronized void draw(Canvas canvas, MapView mapView, boolean shadow) {
		if(DEBUG){
			Log.d(TAG, "draw(Canvas canvas, MapView mapView, boolean shadow)");
			Log.d(TAG, "shadow: " + shadow);
		}
		
		//shadowがtrueの場合、Iconの影を描くためのdrawなのでIconのみを描く場合は無視
		if(!shadow){
			//Projection objectを取得
			Projection pj = mapView.getProjection();
			Point point = new Point();
			for(MyOverlayItem item: mItems){
				pj.toPixels(item.getGeoPoint(), point);
				Drawable icon = item.getIcon();
				
				Rect bounds = new Rect();
				int halfWidth = icon.getIntrinsicWidth() / 2;
				
				bounds.left = point.x - halfWidth;
				bounds.right = point.x + halfWidth;
				bounds.top = point.y - icon.getIntrinsicHeight();
				bounds.bottom = point.y;
				
				icon.setBounds(bounds);
				icon.draw(canvas);
			}
		}
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent e, MapView mapView) {
		Log.d(TAG, "onTouchEvent");
		return super.onTouchEvent(e, mapView);
	}
	
	//Overlay#onTap()をoverrideしMyOverlayItemをtapした場合はToastを表示する処理を記載
	@Override
	public synchronized boolean onTap(GeoPoint p, MapView mapView) {
		if(DEBUG)Log.d(TAG, "onTap");
		Projection pj = mapView.getProjection();
		int hitIndex = hitTest(pj, p);
		
		if(hitIndex != -1){
			MyOverlayItem item = mItems.get(hitIndex);
			Toast.makeText(mContext, item.getString(), Toast.LENGTH_SHORT).show();
		}
		
		return super.onTap(p, mapView);
	}
	
	private int hitTest(Projection pj, GeoPoint gp){
		int hitIndex = NO_HIT;
		Point hit = new Point();
		pj.toPixels(gp, hit);

		for(int i = 0; i < mItems.size(); i++){
			MyOverlayItem item = mItems.get(i);
			Point point = new Point();
			pj.toPixels(item.getGeoPoint(), point);
			
			int halfWidth = item.getIcon().getIntrinsicWidth() / 2;
			
			int left = point.x - halfWidth;
			int right = point.x + halfWidth;
			int top = point.y - item.getIcon().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, "hit.x: " + hit.x);
				Log.d(TAG, "hit.y: " + hit.y);
			}
			
			if(left <= hit.x && hit.x <= right){
				if(top <= hit.y && hit.y <= bottom){
					hitIndex = i;
					return i;
				}
			}
		}	
		
		return hitIndex;
	}
}

Overlay拡張classで表示する(要素を保持する)class

package com.android.practice.map;

import com.google.android.maps.GeoPoint;

import android.graphics.drawable.Drawable;

//Overlay拡張classで表示する要素を保持するclassを作成
public class MyOverlayItem {
	
	private Drawable mIcon;
	private GeoPoint mGeoPoint;
	private String mString;
	
	public void setIcon(Drawable icon){
		mIcon = icon;
	}
	
	public Drawable getIcon(){
		return mIcon;
	}
	
	public void setGeoPoint(GeoPoint geoPoint){
		mGeoPoint = geoPoint;
	}
	
	public GeoPoint getGeoPoint(){
		return mGeoPoint;
	}
	
	public void setString(String string){
		mString = string;
	}
	
	public String getString(){
		return mString;
	}
}
  • MapView拡張classとlayout xml前回と一緒です
  • ItemizedOverlay拡張classは前々回と一緒です

と、こんな感じです

HashTag #Java, #Android, #MapActivity, #MapView, #Overlay