Google MapでZoom event/Scroll event取得 - MapViewの拡張
再度、前回に続いてGoogle Maps APIを使用した実装に関するPostになります。Google Maps APIを使用していて、Zoom level変更event、或はMapのCenter position変更event(Scroll event)を取得したいと思い、調べてみたところ残念ながらAPIは提供されていない模様。ということで更に調べているとStackoverflowでこういうPostを見かけました。このPostを参考にUser interaction以外でもMapView#animateTo()といったCenter position変更eventを取得出来るように改良してみました(このPostのままだと自分がやりたい事をやった際に上手くいきませんでした)
今回やってみたいこと
- MapViewを拡張しZoom level変更event、Center position変更eventを取得(Listenerに通知も)
ちなみに前回やったこと
- ItemizedOverlayを使用しtouchされたmapの場所にOverlayItemを表示
- OverlayItemがtapされた場合は、それが保持するLocation情報を表示
こんな感じでZoom levelが変更されるとToastを表示
実装の要点は下記です(前回実装したことに関しては省略)
1. MapViewを拡張したclassを作成
public class MyMapView extends MapView { @Override public boolean onTouchEvent(MotionEvent ev) { return super.onTouchEvent(ev); } @Override public void computeScroll() { super.computeScroll(); } }
2. MapViewを拡張したclassをlayout xmlに定義
<com.android.practice.map.MyMapView android:id="@+id/map_view" android:layout_width="fill_parent" android:layout_height="fill_parent" android:clickable="true" android:apiKey="自分のGoogle Maps API key" />
3. Zoom level変更/Center position変更event listenerを作成
public interface ZoomListener{ public void onZoomChange(MapView mapView, int newZoom, int oldZoom); } public interface PanListener{ public void onCenterChange(MapView mapView, GeoPoint newGeoPoint, GeoPoint oldGeoPoint); } 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(); } }
4. 3で作成したMapView event listener classをlistenerとしてMapView拡張classに登録
mMyMapViewListener = new MyMapViewListener(); if(mMapView != null){ mMapView.setZoomListener(mMyMapViewListener); mMapView.setPanListener(mMyMapViewListener); }
5. Zoom level変更用、Center position変更用の通知Runnableをそれぞれ作成
mZoomChangeNotifier = new Runnable(){ @Override public void run() { mHandler.removeCallbacks(this); notifyZoomChange(); } }; mCenterChangeNotifier = new Runnable(){ @Override public void run() { mHandler.removeCallbacks(this); notifyCenterChange(); } };
6. 移動Animation終了check用のRunnableを作成(User interaction以外でもZoom level/Center positionの変更を確認するため)
mAnimationFinishChecker = new Runnable(){ @Override public void run() { mHandler.removeCallbacks(this); if(isAnimationFinished()){ isDuringAnimation = false; mHandler.post(new Runnable(){ @Override public void run() { mHandler.removeCallbacks(mZoomChangeNotifier); mHandler.removeCallbacks(mCenterChangeNotifier); if(mLastZoomLevel != getZoomLevel()){ notifyZoomChange(); } if(isCenterChanged()){ notifyCenterChange(); } } }); }else{ mHandler.postDelayed(this, EVENT_DELAY); } } };
7. MapView#onTouchEvent()をoverrideしtouch中か移動Animation中かのflagを設定
@Override public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch(action & MotionEvent.ACTION_MASK){ case MotionEvent.ACTION_DOWN: isTouched = true; isDuringAnimation = false; break; case MotionEvent.ACTION_UP: isTouched = false; break; } return super.onTouchEvent(ev); }
8. MapView#computeScroll()をoverrideしZoom level変更/Center position変更の確認。確認には6で作成したRunnableをHandlerでpost
@Override public void computeScroll() { super.computeScroll(); if(!isDuringAnimation){ checkAndNotifyChanges(EVENT_DELAY); } } private void checkAndNotifyChanges(long delay){ if(getZoomLevel() != mLastZoomLevel){ mHandler.postDelayed(mZoomChangeNotifier, delay); } if(isCenterChanged() && !isTouched){ mHandler.postDelayed(mCenterChangeNotifier, delay); } }
長々となりましたが、こんな感じです
気付いた点をまとめます
- UI上移動Animationは終了しているが、移動距離が長い場合MapVeiw#computeScroll()が実行されている場合がある(実装を気をつけないとcomputeScroll()でevent通知を行っている為、変なtimingでevent通知がされる)
それでは実装です
MapActivityなど(MapViewのevent listener class追加、MapController関係code削除以外前回と同じ)
package com.android.practice.map; 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 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の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(); } 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(); } } }
MapView拡張class
package com.android.practice.map; import java.util.ArrayList; import android.content.Context; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import com.google.android.maps.GeoPoint; import com.google.android.maps.MapView; //MapViewを拡張したclassを作成 public class MyMapView extends MapView { private static final String TAG = "MyMapView"; private static final boolean DEBUG = true; private long EVENT_DELAY = 500L; private final Handler mHandler; private final Runnable mZoomChangeNotifier; private final Runnable mCenterChangeNotifier; private final Runnable mAnimationFinishChecker; private ArrayList<ZoomListener> mZoomListeners; private int mLastZoomLevel; private ArrayList<PanListener> mPanListeners; private int mLastCenterLati; private int mLastCenterLongi; private int mAnimationToLati; private int mAnimationToLongi; private boolean isTouched; private boolean isDuringAnimation; public MyMapView(Context arg0, AttributeSet arg1) { super(arg0, arg1); //Handler objectを取得 mHandler = new Handler(); //Zoom level変更用、Center position変更用の通知Runnableをそれぞれ作成 //Zoom level変更を通知するRunnableを作成 mZoomChangeNotifier = new Runnable(){ @Override public void run() { //前回postしたZoom level変更を通知するRunnableが残っていればcancelする mHandler.removeCallbacks(this); notifyZoomChange(); } }; //ZoomListenerを保持するArrayListを作成 mZoomListeners = new ArrayList<ZoomListener>(); mLastZoomLevel = getZoomLevel(); //Center position変更を通知するRunnableを作成 mCenterChangeNotifier = new Runnable(){ @Override public void run() { //前回postしたcenter position変更を通知するRunnableが残っていればcancelする mHandler.removeCallbacks(this); notifyCenterChange(); } }; //PanListenerを保持するArrayListを作成 mPanListeners = new ArrayList<PanListener>(); GeoPoint center = getMapCenter(); mLastCenterLati = center.getLatitudeE6(); mLastCenterLongi = center.getLongitudeE6(); //移動Animation終了check用のRunnableを作成 mAnimationFinishChecker = new Runnable(){ @Override public void run() { mHandler.removeCallbacks(this); //移動Animationが終了していればZoom level変更、Center position変更確認後、listenerに通知 if(isAnimationFinished()){ isDuringAnimation = false; mHandler.post(new Runnable(){ @Override public void run() { mHandler.removeCallbacks(mZoomChangeNotifier); mHandler.removeCallbacks(mCenterChangeNotifier); //UI上移動Animationは終了しているが、移動距離が長い場合MapVeiw#computeScroll()が実行されている場合があるので //computeScroll() -> checkAndNotifyChanges()による通知cancelを避けるために //mZoomChangeNotifier, mCetnerChangeNotifierをpostせずにListenerに通知 if(mLastZoomLevel != getZoomLevel()){ notifyZoomChange(); } if(isCenterChanged()){ notifyCenterChange(); } } }); //移動Animationが終了していなければ再度、自分をpostする }else{ mHandler.postDelayed(this, EVENT_DELAY); } } }; isTouched = false; isDuringAnimation = false; } //MapView#onTouchEvent()をoverrideしtouch中か移動Animation中かのflagを設定 @Override public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch(action & MotionEvent.ACTION_MASK){ case MotionEvent.ACTION_DOWN: if(DEBUG)Log.d(TAG, "ACTION_DOWN"); isTouched = true; isDuringAnimation = false; break; case MotionEvent.ACTION_UP: if(DEBUG)Log.d(TAG, "ACTION_UP"); isTouched = false; break; } return super.onTouchEvent(ev); } //MapView#computeScroll()をoverrideしZoom level変更/Center position変更の確認 @Override public void computeScroll() { super.computeScroll(); if(DEBUG)Log.d(TAG, "computeScroll"); //移動Animation中のZoom level変更, Center position変更は通知しない if(!isDuringAnimation){ checkAndNotifyChanges(EVENT_DELAY); } } private void checkAndNotifyChanges(long delay){ //直近に取得したZoom levelと異なるZoom levelか確認 if(getZoomLevel() != mLastZoomLevel){ if(DEBUG)Log.d(TAG, "Zoom level changed"); //Zoom level変更を通知するRunnableをpost mHandler.postDelayed(mZoomChangeNotifier, delay); }else{ if(DEBUG)Log.d(TAG, "Zoom level NOT changed"); } //直近に取得したcenter positionと現在のcenter positionが異なるか確認 //touch中か確認 if(isCenterChanged() && !isTouched){ //Center position変更を通知するRunnableをpost mHandler.postDelayed(mCenterChangeNotifier, delay); } } private boolean isCenterChanged(){ GeoPoint gp = getMapCenter(); if(mLastCenterLati == gp.getLatitudeE6() && mLastCenterLongi == gp.getLongitudeE6()){ if(DEBUG)Log.d(TAG, "Center NOT changed"); return false; } if(DEBUG)Log.d(TAG, "Center changed"); return true; } public void animateTo(GeoPoint gp){ isDuringAnimation = true; //どこが移動Animation終了後のCenter positionになるべきか保持しておく mAnimationToLati = gp.getLatitudeE6(); mAnimationToLongi = gp.getLongitudeE6(); getController().animateTo(gp); //移動Animationが終了したかどうかの確認の為のRunnableをpostする mHandler.postDelayed(mAnimationFinishChecker, EVENT_DELAY); } private boolean isAnimationFinished(){ GeoPoint center = getMapCenter(); int centerLati = center.getLatitudeE6(); int centerLongi = center.getLongitudeE6(); if(DEBUG){ Log.d(TAG, "mAnimationToLati:" + mAnimationToLati); Log.d(TAG, "mAnimationToLongi:" + mAnimationToLongi); Log.d(TAG, "centerLati:" + centerLati); Log.d(TAG, "centerLongi:" + centerLongi); } if(centerLati == mAnimationToLati && centerLongi == mAnimationToLongi){ return true; } return false; } public void setZoom(int zoomLevel){ getController().setZoom(zoomLevel); //移動Animationが終了したかどうかの確認の為のRunnableをpostする mHandler.postDelayed(mAnimationFinishChecker, EVENT_DELAY); } public void setZoomListener(ZoomListener listener){ mZoomListeners.add(listener); } public void notifyZoomChange(){ if(DEBUG)Log.d(TAG, "New zoom level: " + getZoomLevel()); int newZoomLevel = getZoomLevel(); for(ZoomListener listener: mZoomListeners){ //Zoom level変更をlistenerに通知 listener.onZoomChange(MyMapView.this, newZoomLevel, mLastZoomLevel); } //現Zoom levelを直近のZoom levelとして保持 mLastZoomLevel = newZoomLevel; } public void setPanListener(PanListener listener){ mPanListeners.add(listener); } public void notifyCenterChange(){ if(DEBUG)Log.d(TAG, "notify center change"); for(PanListener listener: mPanListeners){ //Center position変更をlistenerに通知 listener.onCenterChange(MyMapView.this, getMapCenter(), new GeoPoint(mLastCenterLati, mLastCenterLongi)); } //現Center positionを直近のCenter positionとして保持 mLastCenterLati = getMapCenter().getLatitudeE6(); mLastCenterLongi = getMapCenter().getLongitudeE6(); } public interface ZoomListener{ public void onZoomChange(MapView mapView, int newZoom, int oldZoom); } public interface PanListener{ public void onCenterChange(MapView mapView, GeoPoint newGeoPoint, GeoPoint oldGeoPoint); } }
という感じです。今回はTimerをHandlerに変更したくらいでチャチャっと終わるかなと思ってたんですが、MapView#animateTo()を考慮した処理に苦労しました。まだ不具合あるかも...