Google Maps API v2中SupportMapFragment在ScrollView中-用户无法在地图上垂直滚动

59
我想把Google地图放在滚动视图中,这样用户可以向下滚动查看其他内容来查看地图。问题是,这个滚动视图吃掉了所有垂直触摸事件,所以地图的UI体验变得非常奇怪。
我知道在Google地图的V1版中,您可以重写onTouch,或者设置setOnTouchListener来调用MotionEvent.ACTION_DOWN上的requestDisallowInterceptTouchEvent。我尝试使用V2实现类似的技巧,但无济于事。
到目前为止,我已经尝试过:
  • 重写SupportMapFragment,并在onCreateView内部为View设置触摸侦听器
  • 调用SupportMapFragment实例的.getView(),然后设置OnTouchListener
  • 围绕相对布局或框架布局,用透明视图或ImageView屏蔽片段
这些方法都没有解决滚动问题。我有什么遗漏吗?如果有人有工作示例,将地图放在滚动视图中,请提供代码示例。

1
您可以使用onInterceptTouchEvent捕获所有触摸事件,并使用dispatchTouchEvent将触摸事件分派到必要的视图中。 尝试捕获触摸事件并将其分派到地图片段,一旦用户触摸地图。 - JiTHiN
谢谢您的建议。我猜现在我需要拦截滚动视图中的触摸事件,并将其分派到地图上,如果触摸发生在地图上? - In-Ho Yi
是的,这有点类似于 - https://dev59.com/XHE85IYBdhLWcg3wtV71 - JiTHiN
由于某些原因,地图视图没有响应 dispatchTouchEvent。最终我测试了边界并根据其返回了 true/false(请参见下面的答案)。 - In-Ho Yi
9个回答

154

在地图视图片段上应用一个透明的图片。

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

然后为主要的ScrollView设置requestDisallowInterceptTouchEvent(true)。当用户触摸透明图像并移动时,禁用透明图像的触摸事件MotionEvent.ACTION_DOWNMotionEvent.ACTION_MOVE,以便地图片段可以接收触摸事件。

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

这对我起作用了。


嗨,Laksh,Android 2.3.3版本中存在相同的问题,地图显示黑色阴影。 - Yerram Naveen
2
简单而出色! - Taynã Bonaldo
6
救了我的最后期限:)这个方案有效,而且与我在Stack Overflow上找到的所有其他解决方案不同。谢谢。 - Silviu St
2
覆盖层不需要是一个ImageView,一个简单的View就可以。 - Pierre-Luc Paour
1
那是一个非常厉害的解决方案,它像魔法一样运作! - Bogdan Android
显示剩余9条评论

18

我遇到了类似的问题,并基于上面In-Ho Yi和Данаил Димитров的答案得出一个更通用的解决方案。

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

2
我认为这是最完整和清晰的答案。它非常有效,应该被接受为答案。如果您在布局中有一个作为<fragment>的地图,则应通过fragmentObject.getView()将片段视图传递给CustomScrollView。 - Emil Pana
这个解决方案很好且可重用,但如果您有中间布局,则无法正常工作,因为getHitRect是相对于父级的。我通过使用getParent并在途中使用getLeft/getTop添加偏移量来遍历层次结构来修复它。 - youen
只要您在ScrollView中没有其他视图,如Recycler Views和ViewPagers,这是更好的解决方案。 - Mehvish Ali

9

感谢您的建议,

经过反复尝试、拔头发、对着显示器和我的可怜的Android测试手机咒骂,我发现如果自定义ScrollView,并重写onInterceptTouchEvent方法,在地图视图上返回false,无论如何,那么地图上的滚动将按预期发生。

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

这段代码是用Scala编写的,但你可以理解其中的思想。

注意,我最终使用了原始地图视图(如android-sdks\extras\google\google_play_services\samples\maps\src\com\example\mapdemoRawMapViewDemoActivity.java所示)。我猜你可以使用片段来完成几乎相同的事情,只是我一开始就不喜欢片段。

我认为谷歌欠我一个道歉。


你能详细解释一下如何使用上述代码吗?先感谢了。 - Yerram Naveen
我需要在onInterceptTouchEvent中编写什么? - Yerram Naveen
@AlvaroSantisteban 您希望每次事件触发时都评估矩形(就像我在示例中所显示的parent.mMap.getHitRect(bound))。仅在某个时间存储绑定可能会引起您在此处描述的问题,我相信。 - In-Ho Yi
@In-HoYi 我认为你是对的。我不习惯使用Scala,也不知道如何实现那一行代码,所以我用另一种方式来实现,但是确实存在问题。我的错。你知道在Java中那一行代码会是什么样子吗? - AlvaroSantisteban
对于未来可能阅读此内容的任何人。最终我所做的是考虑到滚动视图的偏移量,因此if语句看起来像这样:如果(bound.contains(Math.round(ev.getX()), Math.round(ev.getY())+getScrollY())){ - AlvaroSantisteban
显示剩余4条评论

5
我也遇到了同样的问题,以下是我将解决方案转化为Java代码的方法,以防有需要的人。在使用时,只需设置mapView字段即可。
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

在XML中使用自定义的Google地图片段。

以下是我使用的完整代码。如果您有任何问题,请让我知道。

在您的XML文件中,将以下内容作为地图碎片添加:

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

这里是地图的自定义类:
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);
            }
        }
    }

在您的活动类中添加以下内容,以初始化地图视图。就是这样。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);
                    }
                });
            }
        });

1
这之后,捏合缩放功能无法使用。但还是谢谢。 :) - Joe

1

在我的情况下,被接受的答案没有起作用。访客的答案也没有(但几乎有用)。如果其他人遇到同样的问题,请尝试使用访客答案的编辑版本。

我已经注释掉了操作栏高度,如果有人需要在计算命中箱时使用它。

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

1

如果您不再需要透明图像,则可以改进代码:

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

1
@Laksh 的答案的 Kotlin 版本: a)实现 View.OnTouchListener, b)在活动的 onCreate 中将 onTouch 监听器添加到透明视图中。 c)按如下所示重写该函数:
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
        }
    }
}

0

上面列出的大部分选项都对我没用,但以下方案对我解决问题非常有帮助:

奶酪大亨的解决方案

由于我在一个片段中使用地图,所以必须稍微按照不同方式实现,这让事情变得稍微有些复杂,但容易使其正常工作。


你尝试过这个吗?https://dev59.com/5GQn5IYBdhLWcg3wP1NU#53411743 - Saamzzz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接