Ответ 1
Я искал решение этой проблемы самостоятельно без везения, поэтому мне пришлось сворачивать свое собственное, что я хотел бы поделиться с вами. (Пожалуйста, извините мой плохой английский) (Немного сумасшедший, чтобы ответить другому чешскому парню по-английски:-))
Первое, что я пробовал, - это использовать старый добрый PopupWindow
. Это довольно просто - нужно только прослушать OnMarkerClickListener
, а затем показать пользовательский PopupWindow
над маркером. Некоторые другие ребята здесь, на StackOverflow, предложили это решение, и на первый взгляд это выглядит неплохо. Но проблема с этим решением проявляется, когда вы начинаете перемещать карту. Вы должны каким-то образом перемещать PopupWindow
самостоятельно, что возможно (прослушивая некоторые события onTouch), но IMHO вы не можете сделать его достаточно хорошим, особенно на некоторых медленных устройствах. Если вы сделаете это простым способом, он "прыгает" с одного места на другое. Вы также можете использовать некоторые анимации, чтобы отполировать эти прыжки, но таким образом PopupWindow
всегда будет "шагом вперед", где он должен быть на карте, которую мне просто не нравится.
В этот момент я думал о каком-то другом решении. Я понял, что на самом деле мне не нужна такая большая свобода - показать свои пользовательские представления со всеми возможными возможностями (такими как анимированные индикаторы прогресса и т.д.). Я думаю, что есть веская причина, почему даже инженеры Google не делают этого так в приложении Google Maps. Все, что мне нужно, это кнопка или два в InfoWindow, которые будут показывать нажатое состояние и запускать некоторые действия при нажатии. Поэтому я придумал другое решение, которое разбивается на две части:
Первая часть:
Первая часть - уловить щелчки на кнопках, чтобы вызвать какое-то действие. Моя идея такова:
- Сохраняйте ссылку на пользовательский инфо-окно, созданный в InfoWindowAdapter.
- Оберните
MapFragment
(илиMapView
) внутри пользовательской ViewGroup (моя называется MapWrapperLayout) - Переопределите
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 {
/**
* 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 bottom edge too).
* It 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 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 result
return ret || super.dispatchTouchEvent(ev);
}
}
Все это заставит представления внутри InfoView "жить" снова - OnClickListeners начнут запускать и т.д.
Вторая часть: Оставшаяся проблема заключается в том, что, очевидно, вы не видите никаких изменений пользовательского интерфейса вашего InfoWindow на экране. Для этого вам нужно вручную вызвать 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;
// 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 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 content bottom edge
mapWrapperLayout.init(map, getPixelsFromDp(this, 39 + 20));
// We want to reuse the info window for all the markers,
// so let 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() + " 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 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 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), я попробую его на телефоне Gingerbread, когда у меня будет шанс. Ограничение, которое я нашел до сих пор, заключается в том, что вы не можете перетаскивать карту, где находится ваша кнопка на экране, и перемещать карту вокруг. Вероятно, это можно было бы преодолеть, но пока я могу жить с этим.
Я знаю, что это уродливый хак, но я просто не нашел ничего лучше, и мне нужен такой шаблон дизайна настолько плохо, что это действительно было бы поводом вернуться к каркасу v1 (что, кстати, я действительно как избежать для нового приложения с фрагментами и т.д.). Я просто не понимаю, почему Google не предлагает разработчикам некоторый официальный способ иметь кнопку в InfoWindows. Это такой общий шаблон дизайна, причем этот шаблон используется даже в официальном приложении Google Maps:). Я понимаю причины, по которым они не могут просто сделать ваши представления "живыми" в InfoWindows - это, вероятно, убьет производительность при перемещении и прокрутке карты. Но должен быть каким-то образом, как добиться этого эффекта без использования представлений.