Google Maps Android API v2 - detect touch on map

AndroidGoogle MapsMapsTouchGoogle Maps-Api-2

Android Problem Overview


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).

Android Solutions


Solution 1 - Android

@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, https://stackoverflow.com/questions/13722869/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);
    }
}

Solution 2 - Android

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);
  }
});

Solution 3 - Android

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();
    }
}

Solution 4 - Android

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.
        }
    });

Solution 5 - Android

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);
        }
    }
}

Solution 6 - Android

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
    }
}

Solution 7 - Android

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
            }
        }

Solution 8 - Android

  // 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
            }
        }
    });
    
    

Solution 9 - Android

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);
        }
    }
}

Solution 10 - Android

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!

Solution 11 - Android

@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?

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGauchoView Question on Stackoverflow
Solution 1 - AndroidGauchoView Answer on Stackoverflow
Solution 2 - AndroidSampath KumarView Answer on Stackoverflow
Solution 3 - AndroidA.AlqadomiView Answer on Stackoverflow
Solution 4 - AndroidFlyviewView Answer on Stackoverflow
Solution 5 - AndroidBjörn KechelView Answer on Stackoverflow
Solution 6 - AndroidadarshView Answer on Stackoverflow
Solution 7 - AndroidIan NewsonView Answer on Stackoverflow
Solution 8 - AndroidPratibha SarodeView Answer on Stackoverflow
Solution 9 - AndroidLéon PelletierView Answer on Stackoverflow
Solution 10 - AndroidSander RitoView Answer on Stackoverflow
Solution 11 - Androiduser2201332View Answer on Stackoverflow