首页 文章

Google Maps Android API v2 - 交互式InfoWindow(与原版Android谷歌 Map 一样)

提问于
浏览
178

点击带有新Google Maps API v2的标记后,我正在尝试制作自定义 InfoWindow . 我希望它看起来像谷歌的原始 Map 应用程序 . 像这样:

Example Image

当我在里面有 ImageButton 时,它不起作用 - 整个_225701被选中而不仅仅是 ImageButton . 我读到这是因为没有 View 本身,但它是快照,因此无法区分各个项目 .

EDIT:documentation(感谢Disco S2):

如上一节关于信息窗口所述,信息窗口不是实时视图,而是视图在 Map 上呈现为图像 . 因此,您在视图上设置的任何侦听器都将被忽略,并且您无法区分视图各个部分上的单击事件 . 建议您不要在自定义信息窗口中放置交互式组件(如按钮,复选框或文本输入) .

但如果谷歌使用它,必须有一些方法来实现它 . 有谁有想法吗?

7 回答

  • 8

    这是我对这个问题的看法 . 我创建 AbsoluteLayout 覆盖,其中包含信息窗口(具有每一点交互性和绘图功能的常规视图) . 然后我开始 Handler ,它每隔16毫秒将信息窗口的位置与 Map 上的点位置同步 . 听起来很疯狂,但确实有效 .

    演示视频:https://www.youtube.com/watch?v=bT9RpH4p9mU(考虑到由于模拟器和视频录制同时运行而导致性能下降) .

    演示代码:https://github.com/deville/info-window-demo

    提供详细信息的文章(俄文):http://habrahabr.ru/post/213415/

  • -5

    For those who couldn't get choose007's answer up and running

    如果在 chose007's 解决方案中 clickListener 始终无法正常工作,请尝试实施 View.onTouchListener 而不是 clickListener . 使用任何操作 ACTION_UPACTION_DOWN 处理触摸事件 . 出于某种原因,map infoWindow 在调度到 clickListeners 时会导致一些奇怪的行为 .

    infoWindow.findViewById(R.id.my_view).setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
              int action = MotionEventCompat.getActionMasked(event);
              switch (action){
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG,"a view in info window clicked" );
                        break;
                    }
                    return true;
              }
    

    Edit : This is how I did it step by step

    首先在您的activity / fragment中的某个位置膨胀您自己的infowindow(全局变量) . 我的是碎片 . 还要确保infowindow布局中的根视图是linearlayout(由于某种原因,relativelayout在infowindow中占据了整个屏幕宽度)

    infoWindow = (ViewGroup) getActivity().getLayoutInflater().inflate(R.layout.info_window, null);
    /* Other global variables used in below code*/
    private HashMap<Marker,YourData> mMarkerYourDataHashMap = new HashMap<>();
    private GoogleMap mMap;
    private MapWrapperLayout mapWrapperLayout;
    

    然后在谷歌 Map android api的onMapReady回调中(如果你不知道onMapReady是Maps > Documentation - Getting Started,请关注这个)

    @Override
        public void onMapReady(GoogleMap googleMap) {
           /*mMap is global GoogleMap variable in activity/fragment*/
            mMap = googleMap;
           /*Some function to set map UI settings*/ 
            setYourMapSettings();
    

    MapWrapperLayout 初始化http://stackoverflow.com/questions/14123243/google-maps-android-api-v2- interactive-infowindow-like-in-original-android-go / 15040761#15040761 39 - 默认标记高度20 - 默认InfoWindow底边与其内容底边之间的偏移* /

    mapWrapperLayout.init(mMap, Utils.getPixelsFromDp(mContext, 39 + 20));
    
            /*handle marker clicks separately - not necessary*/
           mMap.setOnMarkerClickListener(this);
    
           mMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
                    @Override
                    public View getInfoWindow(Marker marker) {
                        return null;
                    }
    
                @Override
                public View getInfoContents(Marker marker) {
                    YourData data = mMarkerYourDataHashMap.get(marker);
                    setInfoWindow(marker,data);
                    mapWrapperLayout.setMarkerWithInfoWindow(marker, infoWindow);
                    return infoWindow;
                }
            });
        }
    

    SetInfoWindow method

    private void setInfoWindow (final Marker marker, YourData data)
                throws NullPointerException{
            if (data.getVehicleNumber()!=null) {
                ((TextView) infoWindow.findViewById(R.id.VehicelNo))
                        .setText(data.getDeviceId().toString());
            }
            if (data.getSpeed()!=null) {
                ((TextView) infoWindow.findViewById(R.id.txtSpeed))
                        .setText(data.getSpeed());
            }
    
            //handle dispatched touch event for view click
            infoWindow.findViewById(R.id.any_view).setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    int action = MotionEventCompat.getActionMasked(event);
                    switch (action) {
                        case MotionEvent.ACTION_UP:
                            Log.d(TAG,"any_view clicked" );
                            break;
                    }
                    return true;
                }
            });
    

    Handle marker click separately

    @Override
        public boolean onMarkerClick(Marker marker) {
            Log.d(TAG,"on Marker Click called");
            marker.showInfoWindow();
            CameraPosition cameraPosition = new CameraPosition.Builder()
                    .target(marker.getPosition())      // Sets the center of the map to Mountain View
                    .zoom(10)
                    .build();
            mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition),1000,null);
            return true;
        }
    
  • -2

    我为这个问题构建了一个示例android工作室项目 .

    output screen shots :-

    Download full project source code Click here

    Please note: 您必须在Androidmanifest.xml中添加API密钥

  • 2

    这很简单 .

    googleMap.setInfoWindowAdapter(new InfoWindowAdapter() {
    
                // Use default InfoWindow frame
                @Override
                public View getInfoWindow(Marker marker) {              
                    return null;
                }           
    
                // Defines the contents of the InfoWindow
                @Override
                public View getInfoContents(Marker marker) {
    
                    // Getting view from the layout file info_window_layout
                    View v = getLayoutInflater().inflate(R.layout.info_window_layout, null);
    
                    // Getting reference to the TextView to set title
                    TextView note = (TextView) v.findViewById(R.id.note);
    
                    note.setText(marker.getTitle() );
    
                    // Returning the view containing InfoWindow contents
                    return v;
    
                }
    
            });
    

    只需在您使用GoogleMap的 class 中添加上述代码即可 . R.layout.info_window_layout是我们的自定义布局,显示将取代infowindow的视图 . 我刚刚在这里添加了textview . 您可以在此处添加附加视图,使其像示例快照一样 . 我的info_window_layout是

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:orientation="vertical" 
        >
    
            <TextView 
            android:id="@+id/note"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
    </LinearLayout>
    

    我希望它会有所帮助 . 我们可以在http://wptrafficanalyzer.in/blog/customizing-infowindow-contents-in-google-map-android-api-v2-using-infowindowadapter/#comment-39731找到自定义信息窗口的工作示例

    编辑:此代码显示了我们如何在infoWindow上添加自定义视图 . 此代码未处理自定义视图项目的点击 . 所以它接近回答但不完全是答案,这就是为什么它不被接受为答案 .

  • 11

    我看到这个问题已经陈旧但仍然......

    我们在公司 Build 了一个sipmle库,以实现所需的目标 - 一个包含视图和所有内容的交互式信息窗口 . 你可以看一下on github .

    我希望它有帮助:)

  • 317

    只是一个推测,我没有足够的经验来尝试它...) - :

    由于GoogleMap是一个片段,因此应该可以捕获标记onClick事件并显示自定义 fragment 视图 . Map 片段仍将在背景上可见 . 有人试过吗?有什么理由不起作用?

    缺点是 Map 片段将在背景上冻结,直到自定义信息片段将控制权返回给它 .

  • 0

    我一直在寻找解决这个问题的方法,没有运气,所以我不得不自己滚动,我想和你分享 . (请原谅我的英语不好)(用英语回答另一个捷克人有点疯狂:-))

    我尝试的第一件事是使用一个好旧的 PopupWindow . 这很简单 - 只需要听取 OnMarkerClickListener ,然后在标记上方显示自定义 PopupWindow . StackOverflow上的其他一些人提出了这个解决方案,乍一看它实际上看起来很不错 . 但是,当您开始移动 Map 时,此解决方案的问题就出现了 . 你必须以某种方式自己移动 PopupWindow (通过听一些onTouch事件),但恕我直言,你不能让它看起来不够好,特别是在一些慢速设备上 . 如果你以简单的方式做到这一点"jumps"从一个地方到另一个地方 . 你也可以使用一些动画来修饰那些跳跃,但这样 PopupWindow 总是"a step behind"它应该在 Map 上,我不喜欢 .

    此时,我正在考虑其他一些解决方案 . 我意识到我实际上并不需要那么多的自由 - 用它带来的所有可能性来展示我的自定义视图(比如动画进度条等) . 我认为有一个很好的理由,即使谷歌工程师也不会这样在谷歌 Map 应用程序中这样做 . 我需要的只是InfoWindow上的一两个按钮,它将显示一个按下状态并在单击时触发一些操作 . 所以我提出了另一个解决方案,它分为两部分:

    First part:
    第一部分是能够捕捉按钮上的点击以触发某些动作 . 我的想法如下:

    • 保留对InfoWindowAdapter中创建的自定义infoWindow的引用 .

    • MapFragment (或 MapView )包装在自定义ViewGroup中(我的名为MapWrapperLayout)

    • 覆盖 MapWrapperLayout 's dispatchTouchEvent and (if the InfoWindow is currently shown) first route the MotionEvents to the previously created InfoWindow. If it doesn' t消耗MotionEvents(因为你没有't click on any clickable area inside InfoWindow etc.) then (and only then) let the events go down to the MapWrapperLayout'的超类,所以它最终将被传递到 Map .

    这是MapWrapperLayout的源代码:

    package com.circlegate.tt.cg.an.lib.map;
    
    import com.google.android.gms.maps.GoogleMap;
    import com.google.android.gms.maps.model.Marker;
    
    import android.content.Context;
    import android.graphics.Point;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    import android.widget.RelativeLayout;
    
    public class MapWrapperLayout extends RelativeLayout {
        /**
         * Reference to a GoogleMap object 
         */
        private GoogleMap map;
    
        /**
         * Vertical offset in pixels between the bottom edge of our InfoWindow 
         * and the marker position (by default it's bottom edge too).
         * It's a good idea to use custom markers and also the InfoWindow frame, 
         * because we probably can't rely on the sizes of the default marker and frame. 
         */
        private int bottomOffsetPixels;
    
        /**
         * A currently selected marker 
         */
        private Marker marker;
    
        /**
         * Our custom view which is returned from either the InfoWindowAdapter.getInfoContents 
         * or InfoWindowAdapter.getInfoWindow
         */
        private View infoWindow;    
    
        public MapWrapperLayout(Context context) {
            super(context);
        }
    
        public MapWrapperLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public MapWrapperLayout(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        /**
         * Must be called before we can route the touch events
         */
        public void init(GoogleMap map, int bottomOffsetPixels) {
            this.map = map;
            this.bottomOffsetPixels = bottomOffsetPixels;
        }
    
        /**
         * Best to be called from either the InfoWindowAdapter.getInfoContents 
         * or InfoWindowAdapter.getInfoWindow. 
         */
        public void setMarkerWithInfoWindow(Marker marker, View infoWindow) {
            this.marker = marker;
            this.infoWindow = infoWindow;
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            boolean ret = false;
            // Make sure that the infoWindow is shown and we have all the needed references
            if (marker != null && marker.isInfoWindowShown() && map != null && infoWindow != null) {
                // Get a marker position on the screen
                Point point = map.getProjection().toScreenLocation(marker.getPosition());
    
                // Make a copy of the MotionEvent and adjust it's location
                // so it is relative to the infoWindow left top corner
                MotionEvent copyEv = MotionEvent.obtain(ev);
                copyEv.offsetLocation(
                    -point.x + (infoWindow.getWidth() / 2), 
                    -point.y + infoWindow.getHeight() + bottomOffsetPixels);
    
                // Dispatch the adjusted MotionEvent to the infoWindow
                ret = infoWindow.dispatchTouchEvent(copyEv);
            }
            // If the infoWindow consumed the touch event, then just return true.
            // Otherwise pass this event to the super class and return it's result
            return ret || super.dispatchTouchEvent(ev);
        }
    }
    

    所有这些将使InfoView中的视图再次“实时” - OnClickListeners将开始触发等 .

    Second part: 剩下的问题是,显然,您无法在屏幕上看到InfoWindow的任何UI更改 . 为此,您必须手动调用Marker.showInfoWindow . 现在,如果您在InfoWindow中执行一些永久性更改(例如将按钮的标签更改为其他内容),这就足够了 .

    但是显示按钮按下状态或某种性质的东西更复杂 . 第一个问题是,(至少)我无法使InfoWindow显示正常按钮的按下状态 . 即使我按下按钮很长时间,它仍然在屏幕上保持未按下状态 . 我相信这是由 Map 框架本身处理的东西,它可能确保不在信息窗口中显示任何瞬态 . 但我可能错了,我没有试图找到它 .

    我做的是另一个令人讨厌的黑客 - 我在按钮上附加了一个 OnTouchListener 并在按下或释放到两个自定义绘图时手动切换它的背景 - 一个按钮处于正常状态而另一个处于按下状态 . 这不是很好,但它的工作原理:) . 现在我能够在屏幕上看到按钮在正常状态和按下状态之间切换 .

    还有一个最后一个小故障 - 如果你点击按钮太快,它没有显示按下状态 - 它只是保持正常状态(虽然点击本身被激活所以按钮“工作”) . 至少这是它出现在我的Galaxy Nexus上的方式 . 所以我做的最后一件事是我按下了按钮状态的按钮 . 这也很丑陋,我不确定它会如何在一些较旧的,慢速的设备上运行,但我怀疑即使是 Map 框架本身也是这样的 . 您可以自己尝试 - 当您单击整个InfoWindow时,它会再次处于按下状态,然后是普通按钮(再次 - 至少在我的手机上) . 这实际上就是它在原始Google Map 应用中的工作原理 .

    无论如何,我自己写了一个自定义类来处理按钮状态更改和我提到的所有其他事情,所以这里是代码:

    package com.circlegate.tt.cg.an.lib.map;
    
    import android.graphics.drawable.Drawable;
    import android.os.Handler;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.View.OnTouchListener;
    
    import com.google.android.gms.maps.model.Marker;
    
    public abstract class OnInfoWindowElemTouchListener implements OnTouchListener {
        private final View view;
        private final Drawable bgDrawableNormal;
        private final Drawable bgDrawablePressed;
        private final Handler handler = new Handler();
    
        private Marker marker;
        private boolean pressed = false;
    
        public OnInfoWindowElemTouchListener(View view, Drawable bgDrawableNormal, Drawable bgDrawablePressed) {
            this.view = view;
            this.bgDrawableNormal = bgDrawableNormal;
            this.bgDrawablePressed = bgDrawablePressed;
        }
    
        public void setMarker(Marker marker) {
            this.marker = marker;
        }
    
        @Override
        public boolean onTouch(View vv, MotionEvent event) {
            if (0 <= event.getX() && event.getX() <= view.getWidth() &&
                0 <= event.getY() && event.getY() <= view.getHeight())
            {
                switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN: startPress(); break;
    
                // We need to delay releasing of the view a little so it shows the pressed state on the screen
                case MotionEvent.ACTION_UP: handler.postDelayed(confirmClickRunnable, 150); break;
    
                case MotionEvent.ACTION_CANCEL: endPress(); break;
                default: break;
                }
            }
            else {
                // If the touch goes outside of the view's area
                // (like when moving finger out of the pressed button)
                // just release the press
                endPress();
            }
            return false;
        }
    
        private void startPress() {
            if (!pressed) {
                pressed = true;
                handler.removeCallbacks(confirmClickRunnable);
                view.setBackground(bgDrawablePressed);
                if (marker != null) 
                    marker.showInfoWindow();
            }
        }
    
        private boolean endPress() {
            if (pressed) {
                this.pressed = false;
                handler.removeCallbacks(confirmClickRunnable);
                view.setBackground(bgDrawableNormal);
                if (marker != null) 
                    marker.showInfoWindow();
                return true;
            }
            else
                return false;
        }
    
        private final Runnable confirmClickRunnable = new Runnable() {
            public void run() {
                if (endPress()) {
                    onClickConfirmed(view, marker);
                }
            }
        };
    
        /**
         * This is called after a successful click 
         */
        protected abstract void onClickConfirmed(View v, Marker marker);
    }
    

    这是我使用的自定义InfoWindow布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_vertical" >
    
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_marginRight="10dp" >
    
            <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="18sp"
                android:text="Title" />
    
            <TextView
                android:id="@+id/snippet"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="snippet" />
    
        </LinearLayout>
    
        <Button
            android:id="@+id/button" 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button" />
    
    </LinearLayout>
    

    测试活动布局文件( MapFragmentMapWrapperLayout 内):

    <com.circlegate.tt.cg.an.lib.map.MapWrapperLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/map_relative_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity" >
    
        <fragment
            android:id="@+id/map"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            class="com.google.android.gms.maps.MapFragment" />
    
    </com.circlegate.tt.cg.an.lib.map.MapWrapperLayout>
    

    最后是测试活动的源代码,它将所有这些粘合在一起:

    package com.circlegate.testapp;
    
    import com.circlegate.tt.cg.an.lib.map.MapWrapperLayout;
    import com.circlegate.tt.cg.an.lib.map.OnInfoWindowElemTouchListener;
    import com.google.android.gms.maps.GoogleMap;
    import com.google.android.gms.maps.GoogleMap.InfoWindowAdapter;
    import com.google.android.gms.maps.MapFragment;
    import com.google.android.gms.maps.model.LatLng;
    import com.google.android.gms.maps.model.Marker;
    import com.google.android.gms.maps.model.MarkerOptions;
    
    import android.os.Bundle;
    import android.app.Activity;
    import android.content.Context;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.Button;
    import android.widget.TextView;
    import android.widget.Toast;
    
    public class MainActivity extends Activity {    
        private ViewGroup infoWindow;
        private TextView infoTitle;
        private TextView infoSnippet;
        private Button infoButton;
        private OnInfoWindowElemTouchListener infoButtonListener;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            final MapFragment mapFragment = (MapFragment)getFragmentManager().findFragmentById(R.id.map);
            final MapWrapperLayout mapWrapperLayout = (MapWrapperLayout)findViewById(R.id.map_relative_layout);
            final GoogleMap map = mapFragment.getMap();
    
            // MapWrapperLayout initialization
            // 39 - default marker height
            // 20 - offset between the default InfoWindow bottom edge and it's content bottom edge 
            mapWrapperLayout.init(map, getPixelsFromDp(this, 39 + 20)); 
    
            // We want to reuse the info window for all the markers, 
            // so let's create only one class member instance
            this.infoWindow = (ViewGroup)getLayoutInflater().inflate(R.layout.info_window, null);
            this.infoTitle = (TextView)infoWindow.findViewById(R.id.title);
            this.infoSnippet = (TextView)infoWindow.findViewById(R.id.snippet);
            this.infoButton = (Button)infoWindow.findViewById(R.id.button);
    
            // Setting custom OnTouchListener which deals with the pressed state
            // so it shows up 
            this.infoButtonListener = new OnInfoWindowElemTouchListener(infoButton,
                    getResources().getDrawable(R.drawable.btn_default_normal_holo_light),
                    getResources().getDrawable(R.drawable.btn_default_pressed_holo_light)) 
            {
                @Override
                protected void onClickConfirmed(View v, Marker marker) {
                    // Here we can perform some action triggered after clicking the button
                    Toast.makeText(MainActivity.this, marker.getTitle() + "'s button clicked!", Toast.LENGTH_SHORT).show();
                }
            }; 
            this.infoButton.setOnTouchListener(infoButtonListener);
    
    
            map.setInfoWindowAdapter(new InfoWindowAdapter() {
                @Override
                public View getInfoWindow(Marker marker) {
                    return null;
                }
    
                @Override
                public View getInfoContents(Marker marker) {
                    // Setting up the infoWindow with current's marker info
                    infoTitle.setText(marker.getTitle());
                    infoSnippet.setText(marker.getSnippet());
                    infoButtonListener.setMarker(marker);
    
                    // We must call this to set the current marker and infoWindow references
                    // to the MapWrapperLayout
                    mapWrapperLayout.setMarkerWithInfoWindow(marker, infoWindow);
                    return infoWindow;
                }
            });
    
            // Let's add a couple of markers
            map.addMarker(new MarkerOptions()
                .title("Prague")
                .snippet("Czech Republic")
                .position(new LatLng(50.08, 14.43)));
    
            map.addMarker(new MarkerOptions()
                .title("Paris")
                .snippet("France")
                .position(new LatLng(48.86,2.33)));
    
            map.addMarker(new MarkerOptions()
                .title("London")
                .snippet("United Kingdom")
                .position(new LatLng(51.51,-0.1)));
        }
    
        public static int getPixelsFromDp(Context context, float dp) {
            final float scale = context.getResources().getDisplayMetrics().density;
            return (int)(dp * scale + 0.5f);
        }
    }
    

    而已 . 到目前为止,我只在我的Galaxy Nexus(4.2.1)和Nexus 7(也是4.2.1)上测试了这个,我有机会在姜饼手机上尝试一下 . 到目前为止我发现的限制是你无法从屏幕上的按钮位置拖动 Map 并移动 Map . 它可能会以某种方式克服,但就目前而言,我可以忍受 .

    我知道这是一个丑陋的黑客,但我没有找到更好的东西,我需要这个设计模式如此糟糕,这真的是回到 Map v1框架的原因(顺便说一下 . 我真的很想避免对于带有碎片等的新应用程序 . 我只是不明白为什么谷歌不向开发人员提供一些在InfoWindows上有按钮的官方方式 . 这是一种常见的设计模式,而且这种模式甚至可以在官方谷歌 Map 应用程序中使用:) . 我理解他们为什么不能在InfoWindows中“实时”显示你的观点的原因 - 这可能会在移动和滚动 Map 时扼杀性能 . 但是应该有一些方法如何在不使用视图的情况下实现此效果 .

相关问题