Google Maps Android API v2 - detect touch on map - android
I can't find an example on how to intercept the map touch on the new Google Maps API v2.
I need to know when the user touches the map in order to stop a thread (the centering of the map around my current location).
#ape wrote an answer here on how to intercept the map clicks, but I need to intercept the touches, and then he suggested the following link in a comment of its answer, How to handle onTouch event for map in Google Map API v2?.
That solution seems to be a possible workaround, but the suggested code was incomplete. For this reason I rewrote and tested it, and now it works.
Here it is the working code:
I created the class MySupportMapFragment.java
import com.google.android.gms.maps.SupportMapFragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class MySupportMapFragment extends SupportMapFragment {
public View mOriginalContentView;
public TouchableWrapper mTouchView;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
mOriginalContentView = super.onCreateView(inflater, parent, savedInstanceState);
mTouchView = new TouchableWrapper(getActivity());
mTouchView.addView(mOriginalContentView);
return mTouchView;
}
#Override
public View getView() {
return mOriginalContentView;
}
}
I even created the class TouchableWrapper.java:
import android.content.Context;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class TouchableWrapper extends FrameLayout {
public TouchableWrapper(Context context) {
super(context);
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
MainActivity.mMapIsTouched = true;
break;
case MotionEvent.ACTION_UP:
MainActivity.mMapIsTouched = false;
break;
}
return super.dispatchTouchEvent(event);
}
}
In the layout I declare it this way:
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mapFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="#+id/buttonBar"
class="com.myFactory.myApp.MySupportMapFragment"
/>
Just for test in the main Activity I wrote only the following:
public class MainActivity extends FragmentActivity {
public static boolean mMapIsTouched = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Here is a simple solution to get the location based on user selection (click option on map):
googleMap.setOnMapClickListener(new OnMapClickListener() {
#Override
public void onMapClick(LatLng arg0) {
// TODO Auto-generated method stub
Log.d("arg0", arg0.latitude + "-" + arg0.longitude);
}
});
This feature and many more are now supported :)
this is the developer note(Issue 4636) :
The August 2016 release introduces a set of new camera change listeners for camera motion start, ongoing, and end events. You can also see why the camera is moving, whether it's caused by user gestures, built-in API animations or developer-controlled movements. For details, see the guide to camera change events:
https://developers.google.com/maps/documentation/android-api/events#camera-change-events
Also, see the release notes:
https://developers.google.com/maps/documentation/android-api/releases#august_1_2016
here is a code snippet from the documentation page
public class MyCameraActivity extends FragmentActivity implements
OnCameraMoveStartedListener,
OnCameraMoveListener,
OnCameraMoveCanceledListener,
OnCameraIdleListener,
OnMapReadyCallback {
private GoogleMap mMap;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_camera);
SupportMapFragment mapFragment =
(SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
}
#Override
public void onMapReady(GoogleMap map) {
mMap = map;
mMap.setOnCameraIdleListener(this);
mMap.setOnCameraMoveStartedListener(this);
mMap.setOnCameraMoveListener(this);
mMap.setOnCameraMoveCanceledListener(this);
// Show Sydney on the map.
mMap.moveCamera(CameraUpdateFactory
.newLatLngZoom(new LatLng(-33.87365, 151.20689), 10));
}
#Override
public void onCameraMoveStarted(int reason) {
if (reason == OnCameraMoveStartedListener.REASON_GESTURE) {
Toast.makeText(this, "The user gestured on the map.",
Toast.LENGTH_SHORT).show();
} else if (reason == OnCameraMoveStartedListener
.REASON_API_ANIMATION) {
Toast.makeText(this, "The user tapped something on the map.",
Toast.LENGTH_SHORT).show();
} else if (reason == OnCameraMoveStartedListener
.REASON_DEVELOPER_ANIMATION) {
Toast.makeText(this, "The app moved the camera.",
Toast.LENGTH_SHORT).show();
}
}
#Override
public void onCameraMove() {
Toast.makeText(this, "The camera is moving.",
Toast.LENGTH_SHORT).show();
}
#Override
public void onCameraMoveCanceled() {
Toast.makeText(this, "Camera movement canceled.",
Toast.LENGTH_SHORT).show();
}
#Override
public void onCameraIdle() {
Toast.makeText(this, "The camera has stopped moving.",
Toast.LENGTH_SHORT).show();
}
}
I created an empty FrameLayout layered over top of the MapFragment in the layout. I then set an onTouchListener on this view so I know when the map has been touched but return false so that the touch gets passed on to the map.
<FrameLayout
android:id="#+id/map_touch_layer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
mapTouchLayer.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
Utils.logDebug(TAG, "Map touched!");
timeLastTouched = System.currentTimeMillis();
return false; // Pass on the touch to the map or shadow layer.
}
});
Gaucho has a great answer, and seeing the many upvotes I figured there might be some need for another implementation:
I needed it to use a listener so I can react on the touch and do not have to check it constantly.
I put all in one class that can be used like this:
mapFragment.setNonConsumingTouchListener(new TouchSupportMapFragment.NonConsumingTouchListener() {
#Override
public void onTouch(MotionEvent motionEvent) {
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// map is touched
break;
case MotionEvent.ACTION_UP:
// map touch ended
break;
default:
break;
// use more cases if needed, for example MotionEvent.ACTION_MOVE
}
}
});
where the mapfragment needs to be of type TouchSupportMapFragment and in the layout xml this line is needed:
<fragment class="de.bjornson.maps.TouchSupportMapFragment"
...
Here is the class:
package de.bjornson.maps;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.google.android.gms.maps.SupportMapFragment;
public class TouchSupportMapFragment extends SupportMapFragment {
public View mOriginalContentView;
public TouchableWrapper mTouchView;
private NonConsumingTouchListener mListener;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
mOriginalContentView = super.onCreateView(inflater, parent, savedInstanceState);
mTouchView = new TouchableWrapper(getActivity());
mTouchView.addView(mOriginalContentView);
return mTouchView;
}
#Override
public View getView() {
return mOriginalContentView;
}
public void setNonConsumingTouchListener(NonConsumingTouchListener listener) {
mListener = listener;
}
public interface NonConsumingTouchListener {
boolean onTouch(MotionEvent motionEvent);
}
public class TouchableWrapper extends FrameLayout {
public TouchableWrapper(Context context) {
super(context);
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (mListener != null) {
mListener.onTouch(event);
}
return super.dispatchTouchEvent(event);
}
}
}
https://developers.google.com/maps/documentation/android/reference/com/google/android/gms/maps/GoogleMap.OnMapClickListener
See this link. Implement the interface and fill in the onMapClick() method or whichever you need and set the onMapClickListener to the right implementation.
public class YourActivity extends Activity implements OnMapClickListener {
#Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
...
my_map.setOnMapClickListener(this)
...
}
public void onMapClick (LatLng point) {
// Do Something
}
}
I took the idea from the accepted answer and improved it by converting to Kotlin and adding constructors that allow the touchable wrapper to be declared in the markup, and using a settable callback property for the touch detection to remove the coupling directly to the activity which allows it to be reused more easily:
class TouchableWrapper : FrameLayout {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
var onTouch: ((event :MotionEvent) -> Unit)? = null
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
onTouch?.invoke(event)
return super.dispatchTouchEvent(event)
}
}
Then in your layout:
<com.yourpackage.views.TouchableWrapper
android:id="#+id/viewMapWrapper"
android:layout_height="match_parent"
android:layout_width="match_parent">
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/map"
tools:context=".MapsActivity"
android:name="com.google.android.gms.maps.SupportMapFragment"/>
</com.yourpackage.views.TouchableWrapper>
Then setup your callback like this:
findViewById<TouchableWrapper>(R.id.viewMapWrapper)
.onTouch = {
if (MotionEvent.ACTION_DOWN == it.action) {
//Handle touch down on the map
}
}
// Initializing
markerPoints = new ArrayList<LatLng>();
// Getting reference to SupportMapFragment of the activity_main
SupportMapFragment sfm = (SupportMapFragment)getSupportFragmentManager().findFragmentById(R.id.map);
// Getting Map for the SupportMapFragment
map = sfm.getMap();
// Enable MyLocation Button in the Map
map.setMyLocationEnabled(true);
// Setting onclick event listener for the map
map.setOnMapClickListener(new OnMapClickListener() {
#Override
public void onMapClick(LatLng point) {
// Already two locations
if(markerPoints.size()>1){
markerPoints.clear();
map.clear();
}
// Adding new item to the ArrayList
markerPoints.add(point);
// Creating MarkerOptions
MarkerOptions options = new MarkerOptions();
// Setting the position of the marker
options.position(point);
if(markerPoints.size()==1){
options.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN));
}else if(markerPoints.size()==2){
options.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED));
}
// Add new marker to the Google Map Android API V2
map.addMarker(options);
// Checks, whether start and end locations are captured
if(markerPoints.size() >= 2){
LatLng origin = markerPoints.get(0);
LatLng dest = markerPoints.get(1);
//Do what ever you want with origin and dest
}
}
});
For Mono lovers:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Android.Widget;
using Android.Gms.Maps;
namespace apcurium.MK.Booking.Mobile.Client.Controls
{
public class TouchableMap : SupportMapFragment
{
public View mOriginalContentView;
public TouchableWrapper Surface;
public override View OnCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)
{
mOriginalContentView = base.OnCreateView(inflater, parent, savedInstanceState);
Surface = new TouchableWrapper(Activity);
Surface.AddView(mOriginalContentView);
return Surface;
}
public override View View
{
get
{
return mOriginalContentView;
}
}
}
public class TouchableWrapper: FrameLayout {
public event EventHandler<MotionEvent> Touched;
public TouchableWrapper(Context context) :
base(context)
{
}
public TouchableWrapper(Context context, IAttributeSet attrs) :
base(context, attrs)
{
}
public TouchableWrapper(Context context, IAttributeSet attrs, int defStyle) :
base(context, attrs, defStyle)
{
}
public override bool DispatchTouchEvent(MotionEvent e)
{
if (this.Touched != null)
{
this.Touched(this, e);
}
return base.DispatchTouchEvent(e);
}
}
}
I have a more simple solution diferent to the TouchableWrapper and this works with the last version of play-services-maps:10.0.1. This solution only uses the maps events and does not use custom views. Does not uses deprecated functions and will likely have support for several versions.
First you need a flag variable that stores if the map is being moved by an animation or by user input (this codes asumes that every camera move that is not triggered by an animation is triggered by the user)
GoogleMap googleMap;
boolean movedByApi = false;
Your fragament or activity must implement GoogleMap.OnMapReadyCallback, GoogleMap.CancelableCallback
public class ActivityMap extends Activity implements OnMapReadyCallback, GoogleMap.CancelableCallback{
...
}
and this forces you to implement the methods onMapReady, onFinish, onCancel. And the googleMap object in onMapReady must set an eventlistener for camera move
#Override
public void onMapReady(GoogleMap mMap) {
//instantiate the map
googleMap = mMap;
[...] // <- set up your map
googleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
#Override
public void onCameraMove() {
if (movedByApi) {
Toast.makeText(ActivityMap.this, "Moved by animation", Toast.LENGTH_SHORT).show();
[...] // <-- do something whe you want to handle api camera movement
} else {
Toast.makeText(ActivityMap.this, "Moved by user", Toast.LENGTH_SHORT).show();
[...] // <-- do something whe you want to handle user camera movement
}
}
});
}
#Override
public void onFinish() {
//is called when the animation is finished
movedByApi = false;
}
#Override
public void onCancel() {
//is called when the animation is canceled (the user drags the map or the api changes to a ne position)
movedByApi = false;
}
And finally its beter if you create a generic function for moving the map
public void moveMapPosition(CameraUpdate cu, boolean animated){
//activate the flag notifying that the map is being moved by the api
movedByApi = true;
//if its not animated, just do instant move
if (!animated) {
googleMap.moveCamera(cu);
//after the instant move, clear the flag
movedByApi = false;
}
else
//if its animated, animate the camera
googleMap.animateCamera(cu, this);
}
or just every time you move the map, activate the flag before the animation
movedByApi = true;
googleMap.animateCamera(cu, this);
I hope this helps!
#Gaucho MySupportMapFragment will obviously be used by some other fargment or activity(where there might be more view elements than the map fragment). So how can one dispatch this event to the next fragment where it is to be used. Do we need to write an interface again to do that?
Related
Recreate method to refresh activity shows a white screen in google map
I used a recreate method of activity lifecycle in my onActivityResult so that the activity refresh everything works fine in my activity but the google map shows a white screen !!! and when i move to another activity and come back to this activity the map is load and its works fine !! Thanks for the help in advance recreate(); And i have the map fragment in the layout,I used workaroundMapFragment to customize map to have a scroll on map <fragment android:id="#+id/view_receipt_map" android:name="com.receiptmatch.android.utils.WorkaroundMapFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> public class WorkaroundMapFragment extends SupportMapFragment { private OnTouchListener mListener; View layout; #Override public View onCreateView(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle savedInstance) { layout = super.onCreateView(layoutInflater, viewGroup, savedInstance); TouchableWrapper frameLayout = new TouchableWrapper(getActivity()); frameLayout.setBackgroundColor(getResources().getColor(android.R.color.transparent)); ((ViewGroup) layout).addView(frameLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return layout; } public WorkaroundMapFragment setListener(OnTouchListener listener) { mListener = listener; return this; } public interface OnTouchListener { void onTouch(); } public class TouchableWrapper extends FrameLayout { public TouchableWrapper(Context context) { super(context); } #Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mListener.onTouch(); break; case MotionEvent.ACTION_UP: mListener.onTouch(); break; } return super.dispatchTouchEvent(event); } } } In my Activity private WorkaroundMapFragment supportMapFragment; private GoogleMap mMap; mMap = ((WorkaroundMapFragment) getSupportFragmentManager().findFragmentById(R.id.view_receipt_map)).getMap(); supportMapFragment = ((WorkaroundMapFragment) getSupportFragmentManager().findFragmentById(R.id.view_receipt_map)) .setListener(new WorkaroundMapFragment.OnTouchListener() { #Override public void onTouch() { scrollViewRootLayout.requestDisallowInterceptTouchEvent(true); } }); onMapReady(mMap); #Override public void onMapReady(GoogleMap googleMap) { googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); LatLng latLng = new LatLng(Double.valueOf(viewReceiptParser.receipt.Latitude), Double.valueOf(viewReceiptParser.receipt.Longitude)); CameraPosition cameraPosition = new CameraPosition.Builder() .target(latLng) .zoom(14) .build(); googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); googleMap.addMarker(new MarkerOptions().position(latLng)/*.title(viewReceiptParser.receipt.VendorName)*/); googleMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() { #Override public void onMapClick(LatLng latLng) { launchGoogleMap(); } }); googleMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() { #Override public boolean onMarkerClick(Marker marker) { launchGoogleMap(); return false; } }); }
Android & Google Maps - close info window with back button
I have an activity that holds a fragment with Google Map view in it. App adds several dozens of markers to the MapView, using MarkerManager and ClusterRenderer to form clusters. The problem is that when I have marker's InfoWindow opened and I press hardware Back button, it closes the app. Instead of that, I would like to have the InfoWindow closed. Is there any straightforward way to achieve this?
I managed to solve the problem. I modified MarkerManager to send notification via EventBus when InfoWindow is about to be opened: #Override public View getInfoContents(Marker marker) { View content = fillContent(); EventBus.getDefault().post(new MapInfoWindowShownEvent(marker)); return content; } and I added event handling in the activity: private Marker mLastShownInfoWindowMarker = null; #Override public void onBackPressed() { if(mLastShownInfoWindowMarker != null && mLastShownInfoWindowMarker.isInfoWindowShown()) { mLastShownInfoWindowMarker.hideInfoWindow(); } else { super.onBackPressed(); } } public void onEvent(MapInfoWindowShownEvent event) { mLastShownInfoWindowMarker = event.getMarker(); }
Using this information I decided to make it a bit simpler for myself: private Marker mLastShownInfoWindowMarker = null; public void setMLastShownInfoWindowMarker(Marker marker) {this.mActiveMapMarker=marker;} #Override public void onBackPressed() { if(mLastShownInfoWindowMarker != null && mLastShownInfoWindowMarker.isInfoWindowShown()) { mLastShownInfoWindowMarker.hideInfoWindow(); } else { super.onBackPressed(); } } Then the following where you have your mapfragment: private MainActivity activity; // swap this to your activity public MainActivityMapController(MainActivity activity) { this.activity = activity; } // override markerclicklistener to store lastShownInfoWindowMarker in // the activity where back button will be used map.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() { #Override public boolean onMarkerClick(Marker marker) { activity.setMLastShownInfoWindowMarker(marker); return false; // false keeps the standard behavior } });
Showing custom InfoWindow for Android Maps Utility Library for Android
I'm using the library Google Maps Utility for Android which allows to create clustering int he maps and I need to show a custom InfoWindow but I can't find any method to do this. In order to show the info window, I have the following class, and in the method onClusterItemRendered is where I have access to the info of the marker: class MyClusterRenderer extends DefaultClusterRenderer<MarkerItem> { public MyClusterRenderer(Context context, GoogleMap map, ClusterManager<MarkerItem> clusterManager) { super(context, map, clusterManager); } #Override protected void onBeforeClusterItemRendered(MarkerItem item, MarkerOptions markerOptions) { super.onBeforeClusterItemRendered(item, markerOptions); markerOptions.title(String.valueOf(item.getMarkerId())); } #Override protected void onClusterItemRendered(MarkerItem clusterItem, Marker marker) { super.onClusterItemRendered(clusterItem, marker); } } Is there anybody who has used the library and knows how to show a custom InfoWindow such as the way it was used in the Google Maps? Like: getMap().setInfoWindowAdapter(new InfoWindowAdapter() { #Override public View getInfoWindow(Marker arg0) { return null; } #Override public View getInfoContents(Marker arg0) { return null; } });
Yes, this can be done. ClusterManager maintains two MarkerManager.Collections: one for cluster markers, and one for individual item markers You can set a custom InfoWindowAdapter for each of these kinds of markers independently. Implementation First, install your ClusterManager's MarkerManager as the map's InfoWindowAdapter: ClusterManager<MarkerItem> clusterMgr = new ClusterManager<MarkerItem>(context, map); map.setInfoWindowAdapter(clusterMgr.getMarkerManager()); Next, install your custom InfoWindowAdapter as the adapter for one or both of the marker collections: clusterMgr.getClusterMarkerCollection().setOnInfoWindowAdapter(new MyCustomAdapterForClusters()); clusterMgr.getMarkerCollection().setOnInfoWindowAdapter(new MyCustomAdapterForItems()); The final piece is mapping the raw Marker object that you'll receive in your custom InfoWindowAdapter's callback to the ClusterItem object(s) that you added to the map in the first place. This can be achieved using the onClusterClick and onClusterItemClick listeners, as follows: map.setOnMarkerClickListener(clusterMgr); clusterMgr.setOnClusterClickListener(new OnClusterClickListener<MarkerItem>() { #Override public boolean onClusterClick(Cluster<MarkerItem> cluster) { clickedCluster = cluster; // remember for use later in the Adapter return false; } }); clusterMgr.setOnClusterItemClickListener(new OnClusterItemClickListener<MarkerItem>() { #Override public boolean onClusterItemClick(MarkerItem item) { clickedClusterItem = item; return false; } }); Now you have everything you need to assemble your custom InfoWindow content in your respective Adapters! For example: class MyCustomAdapterForClusters implements InfoWindowAdapter { #Override public View getInfoContents(Marker marker) { if (clickedCluster != null) { for (MarkerItem item : clickedCluster.getItems()) { // Extract data from each item in the cluster as needed } } // build your custom view // ... return view; } }
So basically you have your class starting like this: private class MarkerItemClusterRenderer extends DefaultClusterRenderer<MarkerItem> { public MarkerItemClusterRenderer(Context context, GoogleMap map, ClusterManager<MarkerItem> clusterManager) { ... So in that parameter map of the type GoogleMap is where you have to specify your setInfoWindowAdapter.
Android Google Maps API V2 - Animation after Map start
I would like to add an animation after the map is loaded. My Problem is, i dont know when the map is ready. If i start the animation after my "setUpMapIfNeeded"-function , i see the animation, but whitout map content. Is there any function which tells me when the map content is loaded ?
I had a similar problem and ended with subclassing MapFragment and calling a Callback interface at onViewCreated: public class TLMapFragment extends SupportMapFragment { private TLMapCallBack callback; public interface TLMapCallBack{ public void onMapCreated(); } public static TLMapFragment newInstance(TLMapCallBack callback){ TLMapFragment fragment = new TLMapFragment(); fragment.callback = callback; return fragment; } public TLMapFragment(){ callback = null; } #Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.v(TAG, "onViewCreated"); initMap(); if(callback != null) callback.onMapCreated(); } private void initMap(){ //init map here } }
Yes, here is a sample onCreateview for a Map Fragment #Override public View onCreateView(LayoutInflater arg0, ViewGroup arg1, Bundle arg2) { View v = super.onCreateView(arg0, arg1, arg2); // init map mGoogleMap = getMap(); // map loaded mGoogleMap.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() { #Override public void onMapLoaded() { Log.d(LOG_CATEGORY, "map loaded"); // do other stuff } }); return v; }
How can I handle map move end using Google Maps for Android V2?
I want to geocode address as soon as map center has been changed. How can I handle map moveend with new Google Maps for Android V2? (I'm talking about the case then user drags map by finger)
Check out new maps api. #Override public void onMapReady(GoogleMap map) { mMap = map; mMap.setOnCameraIdleListener(this); mMap.setOnCameraMoveStartedListener(this); mMap.setOnCameraMoveListener(this); mMap.setOnCameraMoveCanceledListener(this); // Show Sydney on the map. mMap.moveCamera(CameraUpdateFactory .newLatLngZoom(new LatLng(-33.87365, 151.20689), 10)); } #Override public void onCameraMoveStarted(int reason) { if (reason == OnCameraMoveStartedListener.REASON_GESTURE) { Toast.makeText(this, "The user gestured on the map.", Toast.LENGTH_SHORT).show(); } else if (reason == OnCameraMoveStartedListener .REASON_API_ANIMATION) { Toast.makeText(this, "The user tapped something on the map.", Toast.LENGTH_SHORT).show(); } else if (reason == OnCameraMoveStartedListener .REASON_DEVELOPER_ANIMATION) { Toast.makeText(this, "The app moved the camera.", Toast.LENGTH_SHORT).show(); } } #Override public void onCameraMove() { Toast.makeText(this, "The camera is moving.", Toast.LENGTH_SHORT).show(); } #Override public void onCameraMoveCanceled() { Toast.makeText(this, "Camera movement canceled.", Toast.LENGTH_SHORT).show(); } #Override public void onCameraIdle() { Toast.makeText(this, "The camera has stopped moving.", Toast.LENGTH_SHORT).show(); } developers.google.com sample
Here is a possible workaround for determining drag start and drag end events: You have to extend SupportMapFragment or MapFragment. In onCreateView you have to wrap your MapView in a customized FrameLayout (in example below it is the class "TouchableWrapper"), in which you intercepts touch events and recognizes whether the map is tapped or not. If your "onCameraChange" gets called, just check whether the map view is pressed or not (in example below this is the variable "mMapIsTouched"). Example code: UPDATE 1: return original created view in getView() use dispatchTouchEvent() instead of onInterceptTouchEvent() Customized FrameLayout: private class TouchableWrapper extends FrameLayout { #Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mMapIsTouched = true; break; case MotionEvent.ACTION_UP: mMapIsTouched = false; break; } return super.dispatchTouchEvent(ev); } } In your customized MapFragment: #Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { mOriginalContentView = super.onCreateView(inflater, parent, savedInstanceState); mTouchView = new TouchableWrapper(getActivity()); mTouchView.addView(mOriginalContentView); return mTouchView; } #Override public View getView() { return mOriginalContentView; } In your camera change callback method: private final OnCameraChangeListener mOnCameraChangeListener = new OnCameraChangeListener() { #Override public void onCameraChange(CameraPosition cameraPosition) { if (!mMapIsTouched) { refreshClustering(false); } } };
OUTDATED Use the new maps API instead. See answer from punksta. After using AZ13's solution above, and running into the problem mentioned in the comments, I created the following solution, that solves the issue more reliably. However, since I am using a timer after the onRelease event to determine whether the map is still animating, there is a slight delay in this solution. The code can be found on Github via this link: https://github.com/MadsFrandsen/MapStateListener My solution can be used from an activity in the following way: new MapStateListener(mMap, mMapFragment, this) { #Override public void onMapTouched() { // Map touched } #Override public void onMapReleased() { // Map released } #Override public void onMapUnsettled() { // Map unsettled } #Override public void onMapSettled() { // Map settled } };
I would try a onCameraChangeListener. The listener is called every time a movement of the camera is finished. The listener will also give you the new location. In my tests the listener was called pretty often during dragging maybe there is a better solution.
The simplest way is to use setOnCameraIdleListener method to handle your move end state of touch listener on your map fragment. see the example below: mMap.setOnCameraMoveStartedListener(new GoogleMap.OnCameraMoveStartedListener() { #Override public void onCameraMoveStarted(int i) { mapPin.startAnimation(animZoomOut); } }); mMap.setOnCameraIdleListener(new GoogleMap.OnCameraIdleListener() { #Override public void onCameraIdle() { mapPin.startAnimation(animZoomIn); } });
Starting with play-services-maps 9.4.0 you can simply use GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnCameraMoveListener and GoogleMap.OnCameraIdleListener. If, for some reason, you want to use the older API which is now deprecated you can use onCameraChangeListener. But you have to be aware of two things: onCameraChange() might be called many times while you drag the map OR only once (when dragging stopped). The camera position in the last call of onCameraChange() can be slightly different from the final camera position. The following code takes both issues into account: private static final int MESSAGE_ID_SAVE_CAMERA_POSITION = 1; private static final int MESSAGE_ID_READ_CAMERA_POSITION = 2; private CameraPosition lastCameraPosition; private Handler handler; private GoogleMap googleMap; public void onMapReady(GoogleMap theGoogleMap) { googleMap = theGoogleMap; handler = new Handler() { public void handleMessage(Message msg) { if (msg.what == MESSAGE_ID_SAVE_CAMERA_POSITION) { lastCameraPosition = googleMap.getCameraPosition(); } else if (msg.what == MESSAGE_ID_READ_CAMERA_POSITION) { if (lastCameraPosition.equals(googleMap.getCameraPosition())) { Log.d(LOG, "Camera position stable"); } } } }; googleMap.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() { #Override public void onCameraChange(CameraPosition cameraPosition) { handler.removeMessages(MESSAGE_ID_SAVE_CAMERA_POSITION); handler.removeMessages(MESSAGE_ID_READ_CAMERA_POSITION); handler.sendEmptyMessageDelayed(MESSAGE_ID_SAVE_CAMERA_POSITION, 300); handler.sendEmptyMessageDelayed(MESSAGE_ID_READ_CAMERA_POSITION, 600); } }); }
On camera idle is what you should use now googleMap.setOnCameraIdleListener(new GoogleMap.OnCameraIdleListener() { #Override public void onCameraIdle() { //Called when camera movement has ended, there are no pending animations and the user has stopped interacting with the map. } });
I have to animate my marker to center as long as the user drag the map. I have implemented it using Stas Shakirov answer MapDragListenerFragment.class public class MapDragListenerFragment extends Fragment implements OnMapReadyCallback, GoogleMap.OnMapLoadedCallback { private Context mContext; private SupportMapFragment supportMapFragment; private Marker centerMarker; private LatLng mapCenterLatLng; private TextView tvLocationName; private Button btnFinalizeDestination; private GoogleMap mGoogleMap; #Nullable #Override public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_map_drag_listener, container, false); } #Override public void onViewCreated(View view, #Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mContext = getActivity(); tvLocationName = (TextView) view.findViewById(R.id.tv_location_name); } #Override public void onActivityCreated(#Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); FragmentManager fm = getActivity().getSupportFragmentManager();//getChildFragmentManager();// supportMapFragment = (SupportMapFragment) fm.findFragmentById(R.id.map_container); if (supportMapFragment == null) { //// FIXME: 2/13/2017 crashes at casting to TouchableMapFragment supportMapFragment = SupportMapFragment.newInstance(); fm.beginTransaction().replace(R.id.map_container, supportMapFragment).commit(); } supportMapFragment.getMapAsync(this); } #Override public void onMapReady(GoogleMap googleMap) { if (googleMap != null) { mGoogleMap = googleMap; centerMarker = mGoogleMap.addMarker(new MarkerOptions().position(mGoogleMap.getCameraPosition().target) .title("Center of Map") .icon(BitmapDescriptorFactory.fromResource(R.drawable.end_green))); mGoogleMap.setOnCameraIdleListener(new GoogleMap.OnCameraIdleListener() { #Override public void onCameraIdle() { mapCenterLatLng = mGoogleMap.getCameraPosition().target; animateMarker(centerMarker,mapCenterLatLng,false); Toast.makeText(mContext, "The camera has stopped moving.", Toast.LENGTH_SHORT).show(); String address = getCompleteAddressString(mapCenterLatLng.longitude,mapCenterLatLng.longitude); tvLocationName.setText(address); } }); mGoogleMap.setOnCameraMoveStartedListener(new GoogleMap.OnCameraMoveStartedListener() { #Override public void onCameraMoveStarted(int reason) { if (reason == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE) { ///tvLocationName.setText("Lat " + mapCenterLatLng.latitude + " Long :" + mapCenterLatLng.longitude); Toast.makeText(mContext, "The user gestured on the map.", Toast.LENGTH_SHORT).show(); } else if (reason == GoogleMap.OnCameraMoveStartedListener .REASON_API_ANIMATION) { Toast.makeText(mContext, "The user tapped something on the map.", Toast.LENGTH_SHORT).show(); } else if (reason == GoogleMap.OnCameraMoveStartedListener .REASON_DEVELOPER_ANIMATION) { Toast.makeText(mContext, "The app moved the camera.", Toast.LENGTH_SHORT).show(); } } }); mGoogleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() { #Override public void onCameraMove() { Toast.makeText(mContext, "The camera is moving.", Toast.LENGTH_SHORT).show(); } }); mGoogleMap.setOnCameraMoveCanceledListener(new GoogleMap.OnCameraMoveCanceledListener() { #Override public void onCameraMoveCanceled() { Toast.makeText(mContext, "Camera movement canceled.", Toast.LENGTH_SHORT).show(); } }); mapCenterLatLng = mGoogleMap.getCameraPosition().target;// it should be done on MapLoaded. if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } mGoogleMap.setMyLocationEnabled(true); mGoogleMap.animateCamera(CameraUpdateFactory.zoomTo(15)); mGoogleMap.setOnMapLoadedCallback(this); mGoogleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() { #Override public void onCameraMove() { } }); } } public void animateMarker(final Marker marker, final LatLng toPosition, final boolean hideMarker) { final Handler handler = new Handler(); final long start = SystemClock.uptimeMillis(); Projection proj = mGoogleMap.getProjection(); Point startPoint = proj.toScreenLocation(marker.getPosition()); final LatLng startLatLng = proj.fromScreenLocation(startPoint); final long duration = 500; final Interpolator interpolator = new LinearInterpolator(); handler.post(new Runnable() { #Override public void run() { long elapsed = SystemClock.uptimeMillis() - start; float t = interpolator.getInterpolation((float) elapsed / duration); double lng = t * toPosition.longitude + (1 - t) * startLatLng.longitude; double lat = t * toPosition.latitude + (1 - t) * startLatLng.latitude; marker.setPosition(new LatLng(lat, lng)); if (t < 1.0) { // Post again 16ms later. handler.postDelayed(this, 16); } else { if (hideMarker) { marker.setVisible(false); } else { marker.setVisible(true); } } } }); } } where fragment_map_drag_listener.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <fragment android:id="#+id/map_container" android:name="com.google.android.gms.maps.SupportMapFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> <ImageView android:id="#+id/iv_center_overlay" android:layout_width="25dp" android:layout_height="25dp" android:visibility="gone" android:layout_centerInParent="true" android:src="#drawable/start_blue" /> </RelativeLayout> <TextView android:id="#+id/tv_location_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="4dp" android:text="Location Name" /> </LinearLayout> where MapDragListenerActivity public class MapDragListenerActivity extends AppCompatActivity { private Context mContext; private static final String TAG = MapDragListenerFragment.class.getSimpleName(); private MapDragListenerFragment mapDragListenerFragment; private Button selectPlaceBtn; public static final int PLACE_AUTOCOMPLETE_REQUEST_CODE = 1219; #Override protected void onCreate(#Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_map_drag_listener); mContext = MapDragListenerActivity.this; mapDragListenerFragment = new MapDragListenerFragment(); getSupportFragmentManager().beginTransaction() .replace(R.id.frame_container,//where frame_container is a FrameLayout mapDragListenerFragment, MapyFragment.class.getSimpleName()).commit(); selectPlaceBtn = (Button) findViewById(R.id.btn_select_place); selectPlaceBtn.setOnClickListener(new View.OnClickListener() { #Override public void onClick(View v) { try { Intent intent = new PlaceAutocomplete.IntentBuilder( PlaceAutocomplete.MODE_FULLSCREEN).build(MapDragListenerActivity.this); startActivityForResult(intent, PLACE_AUTOCOMPLETE_REQUEST_CODE); } catch (GooglePlayServicesRepairableException e) { e.printStackTrace(); } catch (GooglePlayServicesNotAvailableException e) { e.printStackTrace(); } } }); } #Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if(requestCode == PLACE_AUTOCOMPLETE_REQUEST_CODE){ if (resultCode == RESULT_OK) { Place place = PlaceAutocomplete.getPlace(mContext, data); if(mapDragListenerFragment != null && mapDragListenerFragment.isVisible()) mapDragListenerFragment.updateMarkerAtPosition( place.getLatLng() ,place.getName().toString()); Log.i(TAG, "Place:" + place.toString()); } else if (resultCode == PlaceAutocomplete.RESULT_ERROR) { Status status = PlaceAutocomplete.getStatus(mContext, data); Log.i(TAG, status.getStatusMessage()); } else if (requestCode == RESULT_CANCELED) { } } } } activity_map_drag_listener.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="#+id/btn_select_place" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Select Place" /> <FrameLayout android:id="#+id/frame_container" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
#Override public boolean onTouchEvent(MotionEvent event, MapView mapView){ if(event.getAction() == MotionEvent.ACTION_MOVE) return true; return false; }
I think the event onclick in the map is: map.setOnMapClick... But event drag is: map.onCameraChangeListener because I call a log.e in both of that functions and it shown like onClick view and onDrag view . So just using them for you.
Enhanced solution with an Handler inner Class in Xamarin Android, based on Tobus answer: public void OnMapReady(GoogleMap googleMap) { _googleMap = googleMap; if (_googleMap != null) { _cameraPositionHandler = new CameraPositionlHandler(_googleMap); _googleMap.CameraChange += OnCameraChanged; } } void OnCameraChanged (object sender, GoogleMap.CameraChangeEventArgs e) { _cameraPositionHandler.RemoveMessages(MESSAGE_ID_SAVE_CAMERA_POSITION); _cameraPositionHandler.RemoveMessages(MESSAGE_ID_READ_CAMERA_POSITION); _cameraPositionHandler.SendEmptyMessageDelayed(MESSAGE_ID_SAVE_CAMERA_POSITION, 300); _cameraPositionHandler.SendEmptyMessageDelayed(MESSAGE_ID_READ_CAMERA_POSITION, 600); } With the following inner Class: private class CameraPositionlHandler : Handler { private CameraPosition _lastCameraPosition; private GoogleMap _googleMap; public CameraPositionlHandler (GoogleMap googleMap) { _googleMap = googleMap; } public override void HandleMessage(Message msg) { if (_googleMap != null) { if (msg.What == MESSAGE_ID_SAVE_CAMERA_POSITION) { _lastCameraPosition = _googleMap.CameraPosition; } else if (msg.What == MESSAGE_ID_READ_CAMERA_POSITION) { if (_lastCameraPosition.Equals(_googleMap.CameraPosition)) { Console.WriteLine("Camera position stable"); //do what you want } } } } }