Google Maps Android API v2 – 交互式InfoWindow(就像在原来的android谷歌地图)

我试图通过新的Google Maps API v2单击标记后创build自定义InfoWindow 。 我希望它看起来像在谷歌的原始地图应用程序。 喜欢这个:

示例图像

当我有ImageButton里面,它不工作 – 整个InfoWindow被选中,而不仅仅是ImageButton 。 我读到,这是因为没有一个View本身,但它是快照,所以单个项目不能相互区分。

编辑:在文档 (感谢迪斯科S2 ):

正如前面的信息窗口部分所述,信息窗口不是实时视图,而是视图呈现为地图上的图像。 因此,您在视图上设置的任何监听器都将被忽略,并且无法区分视图各个部分的点击事件。 build议您不要在您的自定义信息窗口中放置交互式组件(如button,checkbox或文本input)。

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

我一直在寻找这个问题的解决scheme,所以我不得不把我自己的,我想在这里与你分享。 (请原谅我英语不好)(回答另一个捷克英语的人有点疯狂:-))

我尝试的第一件事是使用一个很好的老PopupWindow。 这很容易 – 只需要听OnMarkerClickListener,然后在标记上方显示一个自定义的PopupWindow。 StackOverflow上的其他一些人提出了这个解决scheme,乍一看看起来相当不错。 但是当你开始移动地图的时候,这个解决scheme的问题就出现了。 你必须自己移动PopupWindow,这是可能的(通过听一些onTouch事件),但恕我直言,你不能让它看起来不够好,特别是在一些慢的设备上。 如果你这样做,它从一个地方“跳”到另一个地方的简单方法。 你也可以使用一些animation来擦亮这些跳跃,但这样PopupWindow将永远是“一步之后”,它应该在地图上,我只是不喜欢。

在这一点上,我正在考虑一些其他的解决scheme。 我意识到,我其实并不需要那么多的自由 – 显示我的自定义视图与它的所有可能性(如animation进度栏等)。 我认为有一个很好的理由,甚至谷歌工程师不这样做在谷歌地图应用程序中。 我所需要的只是InfoWindow中的一个或两个button,它将显示按下的状态,并在点击时触发一些操作。 所以我想出了另一个解决scheme,它分为两部分:

第一部分:
第一部分是能够捕捉button上的点击来触发某些操作。 我的想法如下:

  1. 保留对InfoWindowAdapter中创build的自定义infoWindow的引用。
  2. 将MapFragment(或MapView)封装在自定义的ViewGroup(我的名为MapWrapperLayout)
  3. 覆盖MapWrapperLayout的dispatchTouchEvent和(如果当前显示InfoWindow)首先将MotionEvents路由到先前创build的InfoWindow。 如果它不消耗MotionEvents(就像因为你没有点击InfoWindow内的任何可点击区域),那么(并且只有这样)才会让事件下到MapWrapperLayout的超类,这样它最终会被传递到地图。

这里是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将开始触发等等。

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

但是显示一个button的按下状态或者这种性质的东西是比较复杂的。 第一个问题是,(至less)我无法使InfoWindow显示正常button的按下状态。 即使长时间按下button,屏幕上仍未按下button。 我相信这是由地图框架本身处理,可能确保不会在信息窗口中显示任何瞬态。 但是我可能是错的,我没有试图找出答案。

我做的是另一个讨厌的黑客 – 我附加了一个OnTouchListener的button,并手动切换它的背景,当button被按下或释放到两个自定义的drawables – 一个button在正常状态,另一个在按下状态。 这不是很好,但它的作品:)。 现在,我可以看到屏幕上button在正常状态和按下状态之间切换。

还有最后一个小故障 – 如果你点击button太快,它不显示按下状态 – 它只是保持在正常状态(虽然点击本身被触发,所以button“工作”)。 至less这是它在我的Galaxy Nexus上显示的方式。 所以我做的最后一件事就是我把button按下了一些状态。 这也是相当丑陋的,我不确定在一些较老,慢的设备上它将如何工作,但我怀疑即使是地图框架本身也是这样的。 你可以自己尝试一下 – 当你点击整个InfoWindow时,它会保持按下状态,然后普通的button(至less在我的手机上)。 实际上,即使在原来的Google地图应用中,这也是如此。

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

 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> 

testing活动布局文件(位于MapWrapperLayout中的MapFragment):

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

最后是一个testing活动的源代码,它将所有这一切粘合在一起:

 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)上testing过,我有机会尝试一些姜饼手机。 我目前发现的限制是,您不能从屏幕上的button位置拖动地图,然后移动地图。 它可能会以某种方式克服,但现在我可以忍受。

我知道这是一个丑陋的黑客,但我只是没有find更好的东西,我需要这个devise模式如此糟糕,这真的是一个回到地图v1框架(这顺便说一句,我真的很想避免为一个新的应用程序与片段等)。 我只是不明白为什么Google不会为开发人员提供一些正式的方法在InfoWindows上有一个button。 这是一个常见的devise模式,而且这种模式甚至可以在官方的Google地图应用中使用:)。 我理解他们不能仅仅让InfoWindows中的视图“活”的原因 – 当移动和滚动地图时,这可能会导致性能下降。 但是如何在不使用视图的情况下实现这种效果应该有一些方法。

我看到这个问题已经很老了,但仍然…

我们在我们公司build立了一个s library图书馆,以实现所需要的东西 – 一个交互式的信息窗口,包含了视图和一切。 你可以在github上看看。

我希望它有助于:)

这是我对这个问题的看法。 我创build了包含信息窗口的AbsoluteLayout覆盖图(一个具有交互性和绘图能力的常规视图)。 然后我启动Handler ,每隔16 ms将信息窗口的位置与地图上的点位置同步。 听起来很疯狂,但实际上工作。

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

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

提供细节的文章(俄语): http : //habrahabr.ru/post/213415/

对于那些不能selectchoose007's答案的人来说,

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

 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(全局variables)。 我在片段内。 还要确保infowindow布局中的根视图是线性布局(由于某种原因,相关布局在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; 

然后在onMapReady谷歌地图android apicallback(如果你不知道什么onMapReady是https://developers.google.com/maps/documentation/android-api/start

  @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 initialization http://stackoverflow.com/questions/14123243/google-maps-android-api-v2- interactive-infowindow-like-in-original-android-go/15040761#15040761 39 - default marker height 20 - offset between the default InfoWindow bottom edge and it's content bottom edge */ 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; } 

只是一个猜测,我没有足够的经验来尝试…) – :

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

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

我已经为这个问题构build了一个示例android studio项目。

输出屏幕截图: –

在这里输入图像描述

在这里输入图像描述

在这里输入图像描述

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

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

这真的很简单。

 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.comfind一个自定义infowindow的工作示例。;

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