API Android Google Maps v2 – Интерактивный InfoWindow (как в оригинальных картах Google Android)

Я пытаюсь создать пользовательский InfoWindow после щелчка на маркере с новым API Карт Google v2. Я хочу, чтобы это выглядело в приложении с исходными картами Google. Как это:

Пример изображения

Когда у меня есть ImageButton внутри, его не работает – весь InfoWindow отбирается, а не только ImageButton . Я читал, что это потому, что нет самого представления, но это моментальный снимок, поэтому отдельные элементы нельзя отличить друг от друга.

EDIT: В документации (благодаря Disco S2 ):

Как уже упоминалось в предыдущем разделе о информационных окнах, информационное окно не представляет собой живой вид, а представление отображается как изображение на карте. В результате, любые слушатели, которые вы установили в представлении, не учитываются, и вы не можете различать события кликов в разных частях представления. Вам не рекомендуется размещать интерактивные компоненты, такие как кнопки, флажки или текстовые входы, в вашем пользовательском информационном окне.

Но если Google использует его, должен быть какой-то способ сделать это. Кто-нибудь есть идеи?

Я сам искал решение этой проблемы, и мне не пришлось отказываться от нее, и я хотел бы поделиться ею с вами. (Пожалуйста, извините мой плохой английский) (Немного сумасшедший, чтобы ответить другому чешскому парню по-английски :-))

Первое, что я пробовал, – это использовать старый добрый PopupWindow. Это довольно просто – нужно только прослушать OnMarkerClickListener, а затем показать пользовательский PopupWindow над маркером. Некоторые другие ребята здесь, в StackOverflow, предложили это решение, и на первый взгляд это выглядит неплохо. Но проблема с этим решением проявляется, когда вы начинаете перемещать карту. Вам нужно как-то переместить PopupWindow самостоятельно, что возможно (прослушивая некоторые события onTouch), но IMHO вы не можете сделать его достаточно хорошим, особенно на некоторых медленных устройствах. Если вы сделаете это простым способом, он «прыгает» с одного места на другое. Вы также можете использовать некоторые анимации, чтобы полировать эти прыжки, но таким образом PopupWindow всегда будет «шагом вперед», где он должен быть на карте, которую мне просто не нравится.

В этот момент я подумал о другом решении. Я понял, что мне действительно не нужна такая большая свобода – показать свои пользовательские представления со всеми возможными возможностями (например, анимированные индикаторы прогресса и т. Д.). Я думаю, что есть веская причина, почему даже инженеры Google не делают этого так в приложении Google Maps. Все, что мне нужно, это кнопка или два в InfoWindow, которые будут показывать нажатое состояние и запускать некоторые действия при нажатии. Поэтому я придумал другое решение, которое разбивается на две части:

Первая часть:
Первая часть – уловить щелчки на кнопках, чтобы вызвать какое-то действие. Моя идея такова:

  1. Сохраните ссылку на пользовательский инфо-окно, созданное в InfoWindowAdapter.
  2. Оберните MapFragment (или MapView) внутри пользовательской группы ViewGroup (моя называется MapWrapperLayout)
  3. Переопределите dispatchTouchEvent MapWrapperLayout и (если в настоящее время отображается 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), я попробую его на телефоне Gingerbread, когда у меня будет шанс. Ограничение, которое я нашел до сих пор, заключается в том, что вы не можете перетаскивать карту, откуда находится ваша кнопка на экране, и перемещать карту. Вероятно, это можно было бы преодолеть, но пока я могу жить с этим.

Я знаю, что это уродливый хак, но я просто не нашел ничего лучше, и мне нужен такой дизайн так плохо, что это действительно было бы поводом вернуться к каркасу v1 (что, кстати, мне действительно очень хотелось бы избежать Для нового приложения с фрагментами и т. Д.). Я просто не понимаю, почему Google не предлагает разработчикам некоторый официальный способ иметь кнопку в InfoWindows. Это такой общий шаблон дизайна, причем этот шаблон используется даже в официальном приложении Google Maps :). Я понимаю причины, по которым они не могут просто сделать ваши взгляды «живыми» в InfoWindows – это, вероятно, убьет производительность при перемещении и прокрутке карты. Но должен быть каким-то образом, как добиться этого эффекта без использования представлений.

Я вижу, что этот вопрос уже старый, но все же …

Мы создали библиотеку sipmle в нашей компании для достижения желаемого – интерактивное информационное окно с представлениями и всем остальным. Вы можете проверить это на github .

Я надеюсь, что это помогает 🙂

Вот мое решение проблемы. Я создаю AbsoluteLayout которая содержит Info Window (регулярное представление с каждым битом интерактивности и возможностей рисования). Затем я запускаю Handler который синхронизирует положение информационного окна с положением точки на карте каждые 16 мс. Звучит безумно, но на самом деле работает.

Демо-видео: https://www.youtube.com/watch?v=bT9RpH4p9mU (учтите, что производительность снижается из-за одновременного запуска эмулятора и видеозаписи).

Код демонстрации: https://github.com/deville/info-window-demo

Статья с подробными сведениями: http://habrahabr.ru/post/213415/

Для тех, кто не смог получить ответ choose007's и работает

Если clickListener не работает должным образом в chose007's , попробуйте реализовать View.onTouchListener вместо clickListener . Обработайте событие касания, используя любое действие ACTION_UP или ACTION_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; } 

Изменить: так я сделал это шаг за шагом

Сначала раздуйте свой собственный 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; 

Затем в onMapReady обратный вызов google maps android api (следуйте этому, если вы не знаете, что такое 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-событие и показать пользовательский фрагмент . Фрагмент карты будет по-прежнему отображаться на заднем плане. Кто-нибудь пробовал это? Любая причина, по которой она не может работать?

Недостатком является то, что фрагмент карты будет заморожен на backgroud, пока пользовательский фрагмент информации не вернет ему контроль.

Я построил образец проекта студии Android для этого вопроса.

Выходные снимки экрана: –

Введите описание изображения здесь

Введите описание изображения здесь

Введите описание изображения здесь

Загрузить полный исходный код проекта Нажмите здесь

Обратите внимание: вы должны добавить свой ключ API в Androidmanifest.xml

Это очень просто.

 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. Я просто добавил текст здесь. Вы можете добавить дополнительный вид здесь, чтобы сделать его похожим на образец. Мой 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> 

Надеюсь, это поможет. Мы можем найти рабочий пример пользовательского infowindow по адресу http://wptrafficanalyzer.in/blog/customizing-infowindow-contents-in-google-map-android-api-v2-using-infowindowadapter/#comment-39731

EDITED: этот код показывает, как добавить пользовательский вид в infoWindow. Этот код не обрабатывал клики на элементах пользовательского просмотра. Так что это близко к ответу, но не совсем ответ, поэтому он не принят в качестве ответа.

Intereting Posts
Невозможно создать экземпляр приемника в SMS-сообщении BroadcastReceiver Не удается сохранить ParseUser, который не аутентифицирован Отношение активности Android и службы – после паузы, после остановки Не удалось создать экземпляр android.gms.maps.MapFragment Что является фоном макета по умолчанию Диалоговое окно «Выбор цвета» Завершение выполнения задания пользовательского интерфейса RecognitionListener в JellyBean Замораживает, если не говорить сразу Анимация и поворот изображения в виде поверхности Построить проект Android с Jenkins: не удалось инициализировать аналитику Могу ли я узнать ширину экрана программно в приложении Android? Как извлечь объекты из ListView – getItemAtPosition не будет работать Есть ли какая-то конкретная причина использовать Amazon SNS вместо прямого сервиса Baidu Кинжал 2 – Внедрение библиотек третьих сторон в службы и действия Android SQLiteDatabase.openDatabase vs SQLiteOpenHelper.getReadableDatabase