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

时间:2013-01-02 13:47:43

标签: android google-maps infowindow google-maps-android-api-2

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

Example Image

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

修改documentation(感谢Disco S2):

  

正如上一节关于信息窗口中所提到的,一个信息窗口   不是实时视图,而是将视图渲染为图像   地图。因此,您在视图上设置的任何侦听器都将被忽略   并且你无法区分各个部分的点击事件   风景。建议您不要放置交互式组件 - 例如   作为按钮,复选框或文本输入 - 在您的自定义信息中   窗口。

但如果谷歌使用它,必须有一些方法来实现它。有没有人有任何想法?

7 个答案:

答案 0 :(得分:322)

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

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

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

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

  1. 保留对InfoWindowAdapter中创建的自定义infoWindow的引用。
  2. MapFragment(或MapView)换入自定义ViewGroup(我的名为MapWrapperLayout)
  3. 覆盖MapWrapperLayout的dispatchTouchEvent和(如果当前显示InfoWindow)首先将MotionEvents路由到先前创建的InfoWindow。如果它没有消耗MotionEvents(就像你没有点击InfoWindow里面的任何可点击区域那样)那么(并且只有那时)让事件发生在MapWrapperLayout的超类中,所以它最终会被送到地图上。
  4. 这是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内部的视图生活在#34;再次 - OnClickListeners将开始触发等。

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

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

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

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

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

    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>
    

    测试活动布局文件(MapFragment内的MapWrapperLayout):

    <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)上测试了这个,我有机会在姜饼手机上尝试一下。到目前为止我发现的一个限制是,您无法从屏幕上的按钮位置拖动地图并移动地图。它可能会以某种方式克服,但就目前而言,我可以忍受它。

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

答案 1 :(得分:11)

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

我们在公司设立了一个sipmle图书馆,以实现所需的目标 - 一个包含视图和所有内容的交互式信息窗口。您可以查看on github

我希望它有所帮助:)

答案 2 :(得分:8)

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

演示视频:https://www.youtube.com/watch?v=bT9RpH4p9mU(考虑到因同时运行模拟器和视频录制而降低了性能)。

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

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

答案 3 :(得分:2)

对于那些无法choose007's回答并运行

的人

如果在clickListener解决方案中chose007's始终无法正常运行,请尝试实施View.onTouchListener而不是clickListener。使用任何动作ACTION_UPACTION_DOWN处理触摸事件。出于某种原因,地图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;
          }

编辑:这是我一步一步做到的方式

首先在您的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;

然后在谷歌地图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-                 交互式信息窗口样的,原始的android-去/ 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方法

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

处理标记单击

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

答案 4 :(得分:0)

只是猜测,我没有足够的经验来尝试...) - :

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

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

答案 5 :(得分:-2)

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

输出屏幕截图: -

enter image description here

enter image description here

enter image description here

下载完整的项目源代码点击 here

请注意:您必须在Androidmanifest.xml中添加API密钥

答案 6 :(得分:-5)

这很简单。

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的班级中添加上述代码即可。 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上添加自定义视图。此代码未处理自定义视图项目的点击。所以它接近回答但不完全是答案,这就是为什么它不被接受为答案。