我也曾经为此问题苦苦寻找解决方案,但一直没有成功,所以我自己动手实现了一个并想在这里与大家分享。(请原谅我的糟糕英语)(用英语回答其他捷克人的问题有点疯狂 :-) )
我尝试的第一件事是使用老旧的PopupWindow。它非常简单 - 只需要监听OnMarkerClickListener,然后在标记上方显示一个自定义的PopupWindow。这个解决方案看起来很不错,所以StackOverflow上的其他一些人建议使用它。但是,当您开始移动地图时,此解决方案的问题就会出现。您必须自己移动PopupWindow,这可能是可能的(通过侦听某些onTouch事件),但在我看来,您无法使其足够好看,尤其是在某些慢速设备上。如果您以简单的方式做到这一点,它将从一个位置“跳跃”到另一个位置。您还可以使用一些动画来优化这些跳跃,但是这样,PopupWindow始终会比应该出现在地图上的位置“慢一步”,这是我不喜欢的。
此时,我在考虑其他解决方案。我意识到我实际上并不需要那么多自由-显示具有所有可能性(例如动画进度条等)的自定义视图。我认为甚至Google工程师在Google Maps应用程序中也不是这样做的一个很好的原因。我需要的只是InfoWindow上的一个或两个按钮,在点击时显示按下状态并触发一些操作。因此,我想出了另一个解决方案,它分为两部分:
第一部分:
第一部分是能够捕获按钮上的单击以触发某些操作。我的想法如下:
1.保留对在InfoWindowAdapter中创建的自定义infoWindow的引用。
2.将MapFragment(或MapView)包装在自定义ViewGroup中(我的称为MapWrapperLayout)
3.重写MapWrapperLayout的dispatchTouchEvent,并且(如果当前正在显示InfoWindow),首先将MotionEvents路由到先前创建的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 {
private GoogleMap map;
private int bottomOffsetPixels;
private Marker marker;
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);
}
public void init(GoogleMap map, int bottomOffsetPixels) {
this.map = map;
this.bottomOffsetPixels = bottomOffsetPixels;
}
public void setMarkerWithInfoWindow(Marker marker, View infoWindow) {
this.marker = marker;
this.infoWindow = infoWindow;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean ret = false;
if (marker != null && marker.isInfoWindowShown() && map != null && infoWindow != null) {
Point point = map.getProjection().toScreenLocation(marker.getPosition());
MotionEvent copyEv = MotionEvent.obtain(ev);
copyEv.offsetLocation(
-point.x + (infoWindow.getWidth() / 2),
-point.y + infoWindow.getHeight() + bottomOffsetPixels);
ret = infoWindow.dispatchTouchEvent(copyEv);
}
return ret || super.dispatchTouchEvent(ev);
}
}
所有这些都将使InfoView内部的视图“活”起来 - OnClickListeners将开始触发等。
第二部分:
剩下的问题是,显然,您无法在屏幕上看到InfoWindow的任何UI更改。要做到这一点,您必须手动调用Marker.showInfoWindow。现在,如果您对InfoWindow进行了某些永久更改(例如将按钮标签更改为其他内容),那么这就足够了。
但是,显示按下状态的按钮或类似的东西更加复杂。首先的问题是,我至少无法使InfoWindow显示普通按钮的按下状态。即使我长时间按下按钮,它也仍然未被按下。我相信这是地图框架本身处理的内容,它可能确保不在信息窗口中显示任何瞬态状态。但我可能是错的,我没有尝试找出这个问题。
我所做的是另一种恶心的黑客方式-我将一个OnTouchListener附加到按钮上,并在按钮被按下或释放时手动切换其背景到两个自定义可绘制对象-一个处于正常状态的按钮和另一个处于按下状态的按钮。这不是很好,但有效:)。现在,我能够看到按钮在屏幕上在正常状态和按下状态之间切换。
仍然有一个小问题-如果您过快地单击按钮,它不会显示按下状态-它仅保持在正常状态(尽管单击本身已被触发,因此按钮“有效”)。至少这是在我的Galaxy Nexus上的情况。所以我最后做的一件事是稍微延迟了按钮的按下状态。这也相当丑陋,我不确定它在一些旧设备上是否有效,但我怀疑即使地图框架本身也会这样做。您可以自己尝试-当您单击整个InfoWindow时,它会保持在按下状态较长时间,然后再变回正常状态的按钮(同样-至少在我的手机上)。这实际上是在原始的Google Maps应用程序上运行的方式。
无论如何,我写了一个自定义类来处理按钮状态更改和我提到的所有其他事情,所以这里是代码:
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;
case MotionEvent.ACTION_UP: handler.postDelayed(confirmClickRunnable, 150); break;
case MotionEvent.ACTION_CANCEL: endPress(); break;
default: break;
}
}
else {
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);
}
}
};
protected abstract void onClickConfirmed(View v, Marker marker);
}
这是我使用的自定义信息窗口布局文件:
<?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.init(map, getPixelsFromDp(this, 39 + 20));
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);
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) {
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) {
infoTitle.setText(marker.getTitle());
infoSnippet.setText(marker.getSnippet());
infoButtonListener.setMarker(marker);
mapWrapperLayout.setMarkerWithInfoWindow(marker, infoWindow);
return infoWindow;
}
});
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)上测试过,我将在有机会时尝试在一些Gingerbread手机上进行测试。到目前为止我发现的一个限制是,您无法从屏幕上的按钮所在位置拖动地图并移动地图。它可能可以通过某种方式克服,但现在,我可以接受这种情况。
我知道这是一个丑陋的hack,但我没有找到更好的方法,而且我非常需要这种设计模式,以至于这真的是回到地图v1框架的原因(顺便说一下,对于具有片段等内容的新应用程序,我真的很想避免这种情况)。我只是不明白为什么Google不向开发人员提供一种官方的方式来在InfoWindows上放置一个按钮。这是如此普遍的设计模式,而且甚至在官方的Google Maps应用程序中也使用了这种模式 :)。我理解他们不能让您的视图在InfoWindows中“活动”的原因 - 这可能会在移动和滚动地图时导致性能下降。但应该有一些方法可以在不使用视图的情况下实现此效果。
View.draw(Canvas)
)...这意味着对视图进行的任何后续更改都不会反映在地图上的信息窗口中。要稍后更新信息窗口,此外,信息窗口将不会遵循任何正常视图的交互性,例如触摸或手势事件。但是,您可以按照下面的部分描述监听整个信息窗口的通用单击事件......在您的示例中只有TextView。 - user1943012RelativeLayout
。 - Driss Bounouar