Я сам шукав рішення цієї проблеми, не маючи везіння, тому мені довелося розгорнути своє, яким я хотів би поділитися тут з вами. (Вибачте, будь ласка, мою погану англійську)
Перше, що я спробував - це використовувати добрий старий PopupWindow
. Це досить просто - потрібно лише прослухати, OnMarkerClickListener
а потім показати звичай PopupWindow
над маркером. Деякі інші хлопці тут, на StackOverflow, запропонували це рішення, і воно насправді виглядає непогано на перший погляд. Але проблема з цим рішенням з’являється, коли ви починаєте рухати карту. Ви повинні PopupWindow
якось самостійно пересуватись, що можливо (прослуховуючи деякі події OnTouch), але IMHO ви не можете зробити так, щоб він виглядав досить добре, особливо на деяких повільних пристроях. Якщо ви це зробите простим способом, він "стрибає" з однієї точки на іншу. Ви також можете використовувати деякі анімації для відшліфування цих стрибків, але таким чином PopupWindow
воля завжди буде «крок позаду», де вона повинна бути на карті, що мені просто не подобається.
У цей момент я думав про якесь інше рішення. Я зрозумів, що насправді мені не потрібно стільки свободи - показувати власні власні погляди з усіма можливостями, які поставляються з нею (як, наприклад, анімовані смужки прогресу тощо). Я думаю, що є вагома причина, чому навіть інженери Google не роблять цього в додатку Карти Google. Все, що мені потрібно, - це кнопка чи дві в Інфо вікні, які показуватимуть натиснутий стан та запускатимуть деякі дії при натисканні. Тому я придумав інше рішення, яке розпадається на дві частини:
Перша частина:
Перша частина полягає в тому, щоб можна було зафіксувати натискання на кнопки, щоб викликати певну дію. Моя ідея така:
- Зберігайте посилання на користувальницьке інформаційне вікно, створене в 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'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 на екрані. Для цього вам потрібно вручну зателефонувати в 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'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. Це така поширена модель дизайну, більше того, що ця модель використовується навіть в офіційному додатку Google Maps :). Я розумію причини, через які вони не можуть просто зробити ваш погляд "живим" в InfoWindows - це, ймовірно, призведе до вбивства при переміщенні та прокрутці карти навколо. Але має бути певний спосіб, як досягти цього ефекту без використання поглядів.