Reputation: 1576
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
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
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
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
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
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
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
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
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
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
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
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