58

I am trying to put a Google map inside a scroll view, so that the user can scroll down other contents to see the map. The problem is that this scroll view is eating up all the vertical touching events, so the UI experience of map becomes very weird.

I know that in V1 of the google map, you could override onTouch, or setOnTouchListener to call requestDisallowInterceptTouchEvent upon MotionEvent.ACTION_DOWN. I have tried to implement the similar trick with V2 to no avail.

So far I have tried:

  • Override SupportMapFragment, and inside onCreateView, set a on touch listener for the View
  • call .getView() of a SupportMapFragment instance, then setOnTouchListener
  • Wrap around relative layout or frame layout, mask the fragment with a transparent view or imageview

None of these remedied the scrolling problem. Am I missing something here? If anyone has a working example of a map inside scrolling view, could you please kindly share code example?

Francis Kim
  • 4,235
  • 4
  • 36
  • 51
In-Ho Yi
  • 978
  • 1
  • 8
  • 12
  • 1
    You can capture all the touch events using `onInterceptTouchEvent` and use `dispatchTouchEvent` to dispatch the touches to necessary views. Try capturing touch events and dispatch them to the map fragment once uses touches the map. – JiTHiN Jun 07 '13 at 03:27
  • Thanks for the suggestion. Guess now I have to intercept touch events from scroll view and dispatch it to the map if the touch was happening on the map? – In-Ho Yi Jun 07 '13 at 04:26
  • Yes. this is somewhat similar -http://stackoverflow.com/questions/2646028/android-horizontalscrollview-within-scrollview-touch-handling – JiTHiN Jun 07 '13 at 05:22
  • For some reason, map view wasn't responding to `dispatchTouchEvent`. I ended up testing bounds and returning true/false based on it (see my answer below) – In-Ho Yi Jun 07 '13 at 05:27

9 Answers9

153

Apply a transparent image over the mapview fragment.

<RelativeLayout
    android:id="@+id/map_layout"
    android:layout_width="match_parent"
    android:layout_height="300dp">
            
    <fragment
        android:id="@+id/mapview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="-100dp"
        android:layout_marginBottom="-100dp"
        android:name="com.google.android.gms.maps.MapFragment"/>
            
    <ImageView
        android:id="@+id/transparent_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@color/transparent" />
        
</RelativeLayout>   

Then set requestDisallowInterceptTouchEvent(true) for the main ScrollView. When the user touches the transparent image and moves disable the touch on the transparent image for MotionEvent.ACTION_DOWN and MotionEvent.ACTION_MOVE so that map fragment can take Touch Events.

ScrollView mainScrollView = (ScrollView) findViewById(R.id.main_scrollview);
ImageView transparentImageView = (ImageView) findViewById(R.id.transparent_image);

transparentImageView.setOnTouchListener(new View.OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        switch (action) {
           case MotionEvent.ACTION_DOWN:
                // Disallow ScrollView to intercept touch events.
                mainScrollView.requestDisallowInterceptTouchEvent(true);
                // Disable touch on transparent view
                return false;
                       
           case MotionEvent.ACTION_UP:
                // Allow ScrollView to intercept touch events.
                mainScrollView.requestDisallowInterceptTouchEvent(false);
                return true;
                
           case MotionEvent.ACTION_MOVE:
                mainScrollView.requestDisallowInterceptTouchEvent(true);
                return false;
                
           default: 
                return true;
        }   
    }
});

This worked for me.

starball
  • 20,030
  • 7
  • 43
  • 238
Laksh
  • 6,717
  • 6
  • 33
  • 34
18

I encountered a similar problem and came up with a more general working solution based on In-Ho Yi and Данаил Димитров answers above.

public class CustomScrollView extends ScrollView {

    List<View> mInterceptScrollViews = new ArrayList<View>();

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

    public void addInterceptScrollView(View view) {
        mInterceptScrollViews.add(view);
    }

    public void removeInterceptScrollView(View view) {
        mInterceptScrollViews.remove(view);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        // check if we have any views that should use their own scrolling
        if (mInterceptScrollViews.size() > 0) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            Rect bounds = new Rect();

            for (View view : mInterceptScrollViews) {
                view.getHitRect(bounds);
                if (bounds.contains(x, y + scrollY)) {
                    //were touching a view that should intercept scrolling
                    return false;
                }
            }
        }

        return super.onInterceptTouchEvent(event);
    }
}
Community
  • 1
  • 1
guest
  • 181
  • 1
  • 2
  • 2
    I think that this is the most complete and clean answer. It works very well and it should be the accepted answer. If you have a map as a inside layout, you should pass the fragment's view to CustomScrollView by fragmentObject.getView() – Emil Pana Nov 18 '14 at 10:21
  • This solution works well and is reusable, but it won't work if you have an intermediate layout because getHitRect is relative to the parent. I fixed that by walking up the hierarchy using getParent and adding offset with getLeft/getTop on the way. – youen Jan 13 '15 at 14:48
  • Its a better solution as far as you dont have other views like recycler views and viewpagers inside scrollview. – Mehvish Ali Jan 17 '17 at 06:56
9

Thank you for suggestions,

After much try-and-error, pulling off my hairs and swearing at monitor and my poor Android test phone, I've figured that if I customise ScrollView, override onInterceptTouchEvent in which we return false when the event is on a map view no matter what, then the scrolling on a map does happen as expected.

class MyScrollView(c:Context, a:AttributeSet) extends ScrollView(c,a) {
  val parent = c.asInstanceOf[MyActivity]
  override def onInterceptTouchEvent(ev:MotionEvent):Boolean = {
    var bound:Rect = new Rect()
    parent.mMap.getHitRect(bound)
    if(bound.contains(ev.getX.toInt,ev.getY.toInt))
      false
    else
      super.onInterceptTouchEvent(ev)
  }
}

This code is in Scala but you get the idea.

Note I've ended up using a raw map view (as shown in android-sdks\extras\google\google_play_services\samples\maps\src\com\example\mapdemoRawMapViewDemoActivity.java). Guess you can do the pretty much same thing with fragments, I just never liked fragments in the first place.

I think Google owes me an apology.

In-Ho Yi
  • 978
  • 1
  • 8
  • 12
  • can u explain in detail how to use the above code thanks in advance – Yerram Naveen Jun 11 '13 at 11:50
  • So, basically, you need to extend ScrollView, override onInterceptTouchEvent and inside there, return false if the touch event happens to overlap the bounds of the google map view – In-Ho Yi Jun 11 '13 at 13:55
  • what i need to write in onInterceptTouchEvent – Yerram Naveen Jun 15 '13 at 06:27
  • I think is does not work properly, because it gets the initial position of the map and maintains it even after scrolling, so in the end, some parts of the map react to the touch, while others don't. Maybe it was my own implementation, though. – AlvaroSantisteban Jul 14 '13 at 22:27
  • @AlvaroSantisteban you want to evaluate the rect each time the event was fired (as shown in my example parent.mMap.getHitRect(bound). storing the bound at one time could cause the problem you have described here, I believe. – In-Ho Yi Jul 15 '13 at 06:36
  • @In-HoYi I think you are right. I'm not use to Scala and did not know how to implement that line, so I did it in another way that, yes, has that problem. My bad. Do you know how would that line in Java look like? – AlvaroSantisteban Jul 15 '13 at 09:22
  • For anyone that might read this in the future. In the end what I did is to take into account the offset of the scrollview, so the if looks like this: if(bound.contains(Math.round(ev.getX()), Math.round(ev.getY())+getScrollY())){ – AlvaroSantisteban Jul 16 '13 at 17:43
  • [The answer by guest](http://stackoverflow.com/a/20959132/1288725) user should be the accepted. It is more standard way to achieve it. – Mehul Joisar Jan 02 '15 at 09:29
  • I am already using pull-to-refresh-scrollview. How to customize in that case? – Narendra Singh Nov 30 '15 at 10:58
5

I had the same issue, so here is how I used the solution as java code just in case anyone needs it. You just have to set the mapView field when using it.

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

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;

public class ScrollViewWithMap extends ScrollView
{
    public MapView mapView;

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev)
    {
        if (mapView == null)
            return super.onInterceptTouchEvent(ev);

        if (inRegion(ev.getRawX(), ev.getRawY(), mapView))
            return false;

        return super.onInterceptTouchEvent(ev);
    }

    private boolean inRegion(float x, float y, View v)
    {
        int[] mCoordBuffer = new int[]
        { 0, 0 };

        v.getLocationOnScreen(mCoordBuffer);

        return mCoordBuffer[0] + v.getWidth() > x && // right edge
                mCoordBuffer[1] + v.getHeight() > y && // bottom edge
                mCoordBuffer[0] < x && // left edge
                mCoordBuffer[1] < y; // top edge
    }
}
4

Use custom Google map fragment in your XML.

Here is the complete code that worked for me. If you guys have any questions, please let me know.

In your XML file, add the following as map fragment

<fragment
        android:id="@+id/map_with_scroll_fix"
        android:name="com.myapplication.maputil.GoogleMapWithScrollFix"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

Here is the custom class for map

package com.myapplication.maputil;

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 GoogleMapWithScrollFix extends SupportMapFragment {
        private OnTouchListener mListener;

        @Override
        public View onCreateView(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle savedInstance) {
            View layout = super.onCreateView(layoutInflater, viewGroup, savedInstance);

            TouchableWrapper touchableWrapper = new TouchableWrapper(getActivity());

            touchableWrapper.setBackgroundColor(getResources().getColor(android.R.color.transparent));

            ((ViewGroup) layout).addView(touchableWrapper,
                    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

            return layout;
        }

        public void setListener(OnTouchListener listener) {
            mListener = listener;
        }

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

Add the following in your activity class, to initialize mapview. That's it. Tada :)

((GoogleMapWithScrollFix) getSupportFragmentManager()
                .findFragmentById(R.id.map_with_scroll_fix)).getMapAsync(new OnMapReadyCallback() {
            @Override
            public void onMapReady(GoogleMap googleMap) {
                ScrollView mScrollView = findViewById(R.id.scrollview); //parent scrollview in xml, give your scrollview id value
                ((GoogleMapWithScrollFix) getSupportFragmentManager()
                        .findFragmentById(R.id.map_with_scroll_fix)).setListener(new GoogleMapWithScrollFix.OnTouchListener() {
                    @Override
                    public void onTouch() {
                        //Here is the magic happens.
                        //we disable scrolling of outside scroll view here
                        mScrollView.requestDisallowInterceptTouchEvent(true);
                    }
                });
            }
        });
Saamzzz
  • 256
  • 1
  • 14
1

Improving code if you do not need transparent Image again:

// gmap hack for touch and scrollview
        final ScrollView mainScrollView = (ScrollView) rootView.findViewById(R.id.scrollView);
        (rootView.findViewById(R.id.fixTouchMap)).setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        // Disallow ScrollView to intercept touch events.
                        mainScrollView.requestDisallowInterceptTouchEvent(true);
                        // Disable touch on transparent view
                        return false;

                    case MotionEvent.ACTION_UP:
                        // Allow ScrollView to intercept touch events.
                        mainScrollView.requestDisallowInterceptTouchEvent(false);
                        return true;

                    case MotionEvent.ACTION_MOVE:
                        mainScrollView.requestDisallowInterceptTouchEvent(true);
                        return false;

                    default:
                        return true;
                }
            }
        });
Ninja Coding
  • 1,357
  • 1
  • 16
  • 26
1

The accepted answer did not work in my case. The Guest's answer did neither (but almost). If this is the case for someone else try this edited version of the Guest's answer.

I have commented out the action bar height if someone needs to use it when calculating the hitbox.

public class InterceptableScrollView extends ScrollView {

    List<View> mInterceptScrollViews = new ArrayList<View>();

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

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

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

    public void addInterceptScrollView(View view) {
        mInterceptScrollViews.add(view);
    }

    public void removeInterceptScrollView(View view) {
        mInterceptScrollViews.remove(view);
    }

    private int getRelativeTop(View myView) {
        if (myView.getParent() == this)
            return myView.getTop();
        else
            return myView.getTop() + getRelativeTop((View) myView.getParent());
    }
    private int getRelativeLeft(View myView) {
        if (myView.getParent() == this)
            return myView.getLeft();
        else
            return myView.getLeft() + getRelativeLeft((View) myView.getParent());
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        // check if we have any views that should use their own scrolling
        if (mInterceptScrollViews.size() > 0) {
            int x = (int) event.getX();
            int y = (int) event.getY();


            /*
            int actionBarHeight = 0;

            TypedValue tv = new TypedValue();
            if (getContext().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true))
            {
                actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data,getResources().getDisplayMetrics());
            }
            */

            int viewLocationY = 0;
            int viewLocationX = 0;
            int relativeTop = 0;
            int relativeLeft = 0;

            for (View view : mInterceptScrollViews) {

                relativeTop = getRelativeTop((View) view.getParent());
                relativeLeft = getRelativeLeft((View) view.getParent());
                viewLocationY = relativeTop - getScrollY();
                viewLocationX = relativeLeft - getScrollX();

                if (view.getHeight() + viewLocationY > y && y > viewLocationY && view.getWidth() + viewLocationX > x && x > viewLocationX)
                {
                    return false;
                }
            }
        }

        return super.onInterceptTouchEvent(event);
    }
}
0

Most of the options listed above did not work for me but the following proved to be a great solution to the problem for me:

Cheese Barons Solution

I also had to implement is slightly differently for my implementation, as I am using the map in a fragment which made things slightly more complicated but easy to get working.

Kyle
  • 1,013
  • 8
  • 16
  • Have you tried this https://stackoverflow.com/questions/16974983/google-maps-api-v2-supportmapfragment-inside-scrollview-users-cannot-scroll-th/53411743#53411743 – Saamzzz Nov 21 '18 at 12:13
0

The Kotlin version of @Laksh 's answer: a) Implement View.OnTouchListener and, b) Add the onTouch listener to the transparent view in the activity's onCreate. c) Override the function as follows:

override fun onTouch(v: View?, event: MotionEvent?): Boolean {
    when (v?.id) {
        R.id.transparent_image -> {

            when (event?.action) {
                MotionEvent.ACTION_DOWN -> {
                    reportScrollView.requestDisallowInterceptTouchEvent(true)
                    return false
                }
                MotionEvent.ACTION_UP -> {
                    reportScrollView.requestDisallowInterceptTouchEvent(false)
                    return true
                }
                MotionEvent.ACTION_MOVE -> {
                    reportScrollView.requestDisallowInterceptTouchEvent(true)
                    return false
                }
                else -> return true
            }
        }
        else -> {
            return true
        }
    }
}
Justin Ngan
  • 1,050
  • 12
  • 20