48

I want to set a Google map fragment inside vertical ScrollView, when I do the map does not zoom vertically, how can I override the touch event listener on MapView over the listener of ScrollView?

This is my xml code:

  <ScrollView  android:layout_height="wrap_content"
    android:layout_width="fill_parent"
     android:layout_above="@id/footer"
    android:layout_below="@id/title_horizontalScrollView">

   ///other things



<fragment 
    android:id="@+id/map"
    android:layout_width="fill_parent"
    android:layout_height="300dp"
    class="com.google.android.gms.maps.SupportMapFragment"
    />

  //other things
Shiba Prasad J.
  • 387
  • 2
  • 7
Malo
  • 1,232
  • 2
  • 17
  • 28

11 Answers11

129

Create a custom SupportMapFragment so that we can override its touch event:

WorkaroundMapFragment.java

import android.content.Context;
import android.os.Bundle;
import android.widget.FrameLayout;

import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import com.google.android.gms.maps.SupportMapFragment;

public class WorkaroundMapFragment extends SupportMapFragment {
    private OnTouchListener mListener;

    @Override
    public View onCreateView(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle savedInstance) {
        View 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 void setListener(OnTouchListener listener) {
        mListener = listener;
    }

    public interface OnTouchListener {
        public abstract 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 this above class, we intercept the touch event by using TouchableWrapper.class that extends the FrameLayout. There is also a custom listener OnTouchListener to dispatch the touch event to the main activity named MyMapActivity that handles the map. When the touch event occurred, dispatchTouchEvent will be called and the listener mListener will handle it.

Then replace fragment class in xml with this class="packagename.WorkaroundMapFragment" instead of com.google.android.gms.maps.SupportMapFragment

Then in your activity initialize map as follows:

private GoogleMap mMap;

inside onCreate do this:

  // check if we have got the googleMap already
  if (mMap == null) {
        SupportMapFragment mapFragment = (WorkaroundMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
        mapFragment.getMapAsync(new OnMapReadyCallback() {
            Override
                public void onMapReady(GoogleMap googleMap)
                    {
                        mMap = googleMap;
                        mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
                        mMap.getUiSettings().setZoomControlsEnabled(true);
    
                        mScrollView = findViewById(R.id.scrollMap); //parent scrollview in xml, give your scrollview id value
                        ((WorkaroundMapFragment) getChildFragmentManager().findFragmentById(R.id.map))
                                .setListener(new WorkaroundMapFragment.OnTouchListener() {
                            @Override
                            public void onTouch()
                                {
                                        mScrollView.requestDisallowInterceptTouchEvent(true);
                                }
                            });
                    }
        });
    }

Update: Answer updated to getMapAsync() based on answer by @javacoder123

karllindmark
  • 6,031
  • 1
  • 26
  • 41
Alok Nair
  • 3,994
  • 3
  • 24
  • 30
  • 1
    OMG :D Its workinggg thank you a lot you are the best – Malo May 29 '15 at 09:38
  • 1
    Hi, it works! The only correction: getColor inside the WorkaroundMapFragment.java is now deprecatred, so I used ContextCompat.getColor(getActivity().getApplicationContext(), android.R.color.transparent). – AleCat83 Nov 22 '15 at 09:38
  • Spot on! Thank you very much sir! – Thalis Vilela Feb 03 '16 at 19:42
  • @AlokNair i have used your answer and it's working fine inside scrollview but now i have case like need to detect ACTION_UP of TouchableWrapper and it wont getting called. do i need to return true from touch event of touchableWrapper?? . can you help ?? – Sanket Kachhela Mar 28 '16 at 10:41
  • Very Good Answer! It works properly. But I didn't understand what is the `getMap()` method and comes from where! I simply wrote `mMap = googleMap` inside the event-oriented method: `onMapReady(GoogleMap googleMap)`. – Mir-Ismaili Aug 07 '17 at 12:27
  • Works like a charm..!! Thanks.. !! – Bhavana Oct 04 '17 at 12:19
  • It works great! But i have problem, when i just click on the map view, scroll view immediately scrolled to up. What's wrong with this? – zihadrizkyef Nov 07 '17 at 03:17
  • Can someone explain where the getMap() method has came from? – javacoder123 Apr 06 '18 at 20:31
  • I am not able to cast SupportMapFragment to WorkaroundMapFragment. Has anyone else had this issue? I am creating my fragment dynamically because the scrollview and mapfragment itself is within a nested fragment. – Peter G. Williams May 07 '20 at 15:25
  • @PeterG.Williams I had the same problem, you can see my solution: https://stackoverflow.com/a/67501105/4360290 – Cinzia Nicoletti May 12 '21 at 09:44
  • Perfect. Thanks very much !! – Julien Attard Oct 21 '21 at 11:14
35

Just make CustomScrollView.class,

public class CustomScrollView extends ScrollView {

public CustomScrollView(Context context) {
    super(context);
}

public CustomScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public CustomScrollView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            //Log.i("CustomScrollView", "onInterceptTouchEvent: DOWN super false" );
            super.onTouchEvent(ev);
            break;

        case MotionEvent.ACTION_MOVE:
            return false; // redirect MotionEvents to ourself

        case MotionEvent.ACTION_CANCEL:
            // Log.i("CustomScrollView", "onInterceptTouchEvent: CANCEL super false" );
            super.onTouchEvent(ev);
            break;

        case MotionEvent.ACTION_UP:
            //Log.i("CustomScrollView", "onInterceptTouchEvent: UP super false" );
            return false;

        default:
            //Log.i("CustomScrollView", "onInterceptTouchEvent: " + action );
            break;
    }

    return false;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    super.onTouchEvent(ev);
    //Log.i("CustomScrollView", "onTouchEvent. action: " + ev.getAction() );
    return true;
  }
}

Then in xml

<com.app.ui.views.CustomScrollView
     android:id="@+id/scrollView"
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fillViewport="true"
     android:orientation="vertical">
</com.app.ui.views.CustomScrollView>

if xml beffore didn't work change to this...

<com.myproyect.myapp.CustomScrollView
        android:id="@+id/idScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true"
        android:descendantFocusability="beforeDescendants"
        >
</com.myproyect.myapp.CustomScrollView>

how to use programmaticaly

CustomScrollView myScrollView = (CustomScrollView) findViewById(R.id.idScrollView);
DarckBlezzer
  • 4,578
  • 1
  • 41
  • 51
13

I used the google maps listeners to resolve the scrolling issue of google map inside a scroll view.

    googleMap?.setOnCameraMoveStartedListener {
        mapView.parent.requestDisallowInterceptTouchEvent(true)

    }

    googleMap?.setOnCameraIdleListener {
        mapView.parent.requestDisallowInterceptTouchEvent(false)

    }

By using this when you use start moving maps it disallows parent scrolling and when you stop moving maps then it allows scrolling.

DeePanShu
  • 1,236
  • 10
  • 23
9

This is the Kotlin version of Alok Nair's answer, I have to say that I am relatively new writing Kotlin code, any comment is appreciated.

Custom SupportMapFragmet:

class FocusMapFragment: SupportMapFragment() {
    var listener: OnTouchListener? = null
    lateinit var frameLayout: TouchableWrapper

    override fun onAttach(context: Context) {
        super.onAttach(context)

        frameLayout = TouchableWrapper(context)
        frameLayout.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent))
    }

    override fun onCreateView(inflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
        val layout = super.onCreateView(inflater, viewGroup, bundle)

        val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        (layout as? ViewGroup)?.addView(frameLayout, params)

        return layout
    }

    interface OnTouchListener {
        fun onTouch()
    }

    inner class TouchableWrapper(context: Context): FrameLayout(context) {
        override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
            when (event?.action) {
                MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> listener?.onTouch()
            }
            return super.dispatchTouchEvent(event)
        }
    }
}

Then change the fragment class in the xml:

android:name="packagename.FocusMapFragment"

And finally when you initialize the map:

override fun onMapReady(googleMap: GoogleMap) {
    this.googleMap = googleMap

    (childFragmentManager.findFragmentById(R.id.mapView) as? FocusMapFragment)?.let {
        it.listener = object: FocusMapFragment.OnTouchListener {
            override fun onTouch() {
                scrollView?.requestDisallowInterceptTouchEvent(true)
            }
        }
    }
    // ...
}

I tested this code in API 28 and 29, I couldn't find a Kotlin version anywhere so thought it could be useful for somebody.

juanjo
  • 3,737
  • 3
  • 39
  • 44
4

Please refer to this answer: https://stackoverflow.com/a/17317176/4837103

It creates a transparent view with the same size of mapFragment, at the same position with mapFragment, then invoke requestDisallowInterceptTouchEvent (true), preventing its ancestors to intercept the touch events.

Add a transparent view over the mapview fragment:

<ScrollView>
    ...
         <FrameLayout
             android:layout_width="match_parent"
             android:layout_height="300dp">
             <FrameLayout
                 android:id="@+id/fl_google_map"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent" />
             <!-- workaround to make google map scrollable -->
             <View
                 android:id="@+id/transparent_touch_panel"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent" />
         </FrameLayout>
</ScrollView>

Set touch listener for that transparent view:

    var transparentTouchPanel = view.FindViewById (Resource.Id.transparent_touch_panel);
    transparentTouchPanel.SetOnTouchListener (this);

....

public bool OnTouch (View v, MotionEvent e) {
    switch (e.Action) {
    case MotionEventActions.Up:
        v.Parent.RequestDisallowInterceptTouchEvent (false);
        break;
    default:
        v.Parent.RequestDisallowInterceptTouchEvent (true);
        break;
    }

    return false;
}

I thought RequestDisallowInterceptTouchEvent could work with being invoked just once, but apparently it has to be called for EVERY touch event, that's why it has to be put inside onTouchEvent. And the parent will propagate this request to the parent's parent.

I tried set touch event listener for fl_google_map, or for the mapFragment.getView(). Neither of them worked, so creating a transparent sibling view is a tolerable workaround for me.

I have tried, this solution works perfectly. Check the comments under the original post if you do not believe me. I prefer this way because it does not need to create a custom SupportMapFragmet.

Chandler
  • 3,061
  • 3
  • 22
  • 30
3

My suggestion is to use lighter version of google map when dealing with this situation. this resolved my issue.

add this attribute in fragment.

 map:liteMode="true"

and my issue resolved. i also added customScrollView in addition to this. but after placing liteMode property my issue resolved.

Developine
  • 12,483
  • 8
  • 38
  • 42
2

I solved it this way, add to implement this class:

public class YourNameFragmentHere extends Fragment implements OnMapReadyCallback, 
GoogleMap.OnCameraMoveStartedListener {...}

Into onMapReady:

 @Override
    public void onMapReady(GoogleMap googleMap) {
       // Your code here
       mMap = googleMap;
       mMap.setOnCameraMoveStartedListener(this);
    }

Then I created this function:

@Override
    public void onCameraMoveStarted(int reason) {

    if (reason == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE) {
        mScrollView.requestDisallowInterceptTouchEvent(true);

    } else if (reason == GoogleMap.OnCameraMoveStartedListener
            .REASON_API_ANIMATION) {
        mScrollView.requestDisallowInterceptTouchEvent(true);

    } else if (reason == GoogleMap.OnCameraMoveStartedListener
            .REASON_DEVELOPER_ANIMATION) {
        mScrollView.requestDisallowInterceptTouchEvent(true);

    }
}
Cinzia Nicoletti
  • 169
  • 1
  • 14
1

Using default SupportMapFragmentin scrollview (fix mapview scroll issue in vertical scrollview):

    val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
                mapFragment.getMapAsync { googleMap ->
                    mMap = googleMap
    

                    // simple add this interceptTouchEvents
                    googleMap.setOnCameraMoveStartedListener {
                        mapFragment.view?.parent?.requestDisallowInterceptTouchEvent(true)
    
                    }
    
                    googleMap.setOnCameraIdleListener {
                        mapFragment.view?.parent?.requestDisallowInterceptTouchEvent(false)
    
                    }
                }
pravingaikwad07
  • 482
  • 1
  • 7
  • 24
0

Update

@Alok Nair's answer is very good however, the .getMap() method no longer works from android 9.2 onwards. Change the getMap method to .getMapAsync and add code in the onMapReady method

Updated Solution, that works in Fragments

if (gMap == null)
        {
          getChildFragmentManager().findFragmentById(R.id.map);
            SupportMapFragment mapFragment = (WorkaroundMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
            mapFragment.getMapAsync(new OnMapReadyCallback()
            {
                @Override
                public void onMapReady(GoogleMap googleMap)
                {
                    gMap = googleMap;
                    gMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
                    gMap.getUiSettings().setZoomControlsEnabled(true);

                    mScrollView = mView.findViewById(R.id.advertScrollView); //parent scrollview in xml, give your scrollview id value
                    ((WorkaroundMapFragment) getChildFragmentManager().findFragmentById(R.id.map))
                            .setListener(new WorkaroundMapFragment.OnTouchListener()
                            {
                                @Override
                                public void onTouch()
                                {
                                    mScrollView.requestDisallowInterceptTouchEvent(true);
                                }
                            });
                }
            });
        }
javacoder123
  • 193
  • 3
  • 17
0

Solution in Kotlin

@Alok Nair's answer still works today (2022). I used his solution in my project in a fragment. I translated from Java to Kotlin and made minor changes (such as eliminating the OnTouchListener interface).

SupportMapFragmentWrapper.kt (WorkaroundMapFragment)

    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 androidx.core.content.ContextCompat
    import com.google.android.gms.maps.SupportMapFragment
    
    class SupportMapFragmentWrapper : SupportMapFragment() {
        private lateinit var mListener: () -> Unit
    
        fun setOnTouchListener(listener: () -> Unit) {
            mListener = listener
        }
    
        inner class TouchableWrapper(context: Context) : FrameLayout(context) {
            override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
                when(ev!!.action) {
                    MotionEvent.ACTION_DOWN -> mListener.invoke()
                    MotionEvent.ACTION_UP   -> mListener.invoke()
                }
                return super.dispatchTouchEvent(ev)
            }
        }
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            val layout = super.onCreateView(inflater, container, savedInstanceState)
    
            val frameLayout = TouchableWrapper(requireContext())
            frameLayout.setBackgroundColor(
                ContextCompat.getColor(
                    activity?.applicationContext!!,
                    android.R.color.transparent)
            )
    
            with(layout as ViewGroup) {
                addView(
                    frameLayout,
                    ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT
                    )
                )
            }
    
            return layout
        }
    }

Use only this in onCreateView(...) [Fragment]

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = DataBindingUtil.inflate(inflater, R.layout.fragment_route_creation, container, false)
    
        val mapFragment = childFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragmentWrapper
    
        mapFragment.getMapAsync(this)
    
        mapFragment.setOnTouchListener {
            binding.scrollView.requestDisallowInterceptTouchEvent(true)
        }
    
        return binding.root
    }

XML

    <ScrollView
       ...
    >
        <fragment
            android:id="@+id/map"
            android:name="com.example.natour.ui.route.SupportMapFragmentWrapper"
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:layout_marginTop="24dp"
            app:cameraTilt="30"
            app:cameraZoom="20"
            app:uiRotateGestures="true"
            app:uiZoomControls="true" />
      ...
    </ScrollView>
JustSightseeing
  • 1,460
  • 3
  • 17
  • 37
vtramo
  • 1
  • 2
-10

As Android Document suggested,
You can add like this way:

<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:orientation="horizontal" >
<fragment
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
</LinearLayout>
</ScrollView>
  • Also i cant scroll my map and zoom vertically because the scroll view scroll over the map – Malo May 29 '15 at 09:00
  • I hope you referred the mentioned document. Thi is not a good practice to put map fragment inside the scrollview. Please let me know why are you doing like this. Any reason behind that ? I have done same thing without add in scrollview. If you would like to watch the code then let me inform. – Ritesh Gajera May 29 '15 at 09:19
  • @RiteshGajera What if you have a long form and want map in the centre of it with all functionalities. Wont you use scrollview then?? – Alok Nair May 29 '15 at 09:29
  • No, I haven't yet use. But without scrollview you can same feature achieved through SupportMapFragment it. – Ritesh Gajera May 29 '15 at 11:17
  • Alok, You can perform or bind the touch event on google map. like as: googleMap.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() { @Override public void onMapLongClick(LatLng point) { //do your stuff here.. } }); Which I have done in one of my R&D Task. – Ritesh Gajera May 29 '15 at 12:02
  • @RiteshGajera you didnt understand my point correctly. read it again if you havent. Without scrollview you wont be able to scroll through a complex long xml based form for e.g a long data entry form with dozens of fields one below another and one of them being a SupportMapFragment maybe to enter location. If you add a scrollview to the root of this layout, then you can scroll the form to view all fields, else you cant. And now in this case the SupportMapFragment wont take touch events such as scroll up and down inside map or zoom in out, and so you need to do something like I answered above – Alok Nair May 29 '15 at 12:14
  • The user doesnt want long click on map, he want scrolling inside map alongwith the scroll featrue outside map in the form – Alok Nair May 29 '15 at 12:17
  • Yes, I was gone through your answer. Actually, I haven't use yet this feature (scroll feature outside map). so i also need to check. – Ritesh Gajera May 29 '15 at 12:26