Malo
Malo

Reputation: 1576

how to set google map fragment inside scroll view

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

Upvotes: 49

Views: 32382

Answers (11)

pravingaikwad07
pravingaikwad07

Reputation: 522

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

Upvotes: 1

vtramo
vtramo

Reputation: 1

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>

Upvotes: 0

Cinzia Nicoletti
Cinzia Nicoletti

Reputation: 197

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

    }
}

Upvotes: 2

Alok Nair
Alok Nair

Reputation: 4004

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

Upvotes: 131

DeePanShu
DeePanShu

Reputation: 1326

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.

Upvotes: 13

juanjo
juanjo

Reputation: 3757

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.

Upvotes: 9

Chandler
Chandler

Reputation: 3297

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.

Upvotes: 4

javacoder123
javacoder123

Reputation: 193

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

Upvotes: 0

Hammad Tariq
Hammad Tariq

Reputation: 13461

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.

Upvotes: 3

Teimoor Alam Khan
Teimoor Alam Khan

Reputation: 548

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

Upvotes: 35

Ritesh Gajera
Ritesh Gajera

Reputation: 1

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>

Upvotes: -10

Related Questions