tutorial studio google example ejemplo android google-maps infowindow google-maps-android-api-2

studio - Google Maps Android API v2-Interactive InfoWindow(como en los mapas de Google Android originales)



google maps api v2 tutorial (7)

Estoy tratando de crear una InfoWindow personalizada después de hacer clic en un marcador con la nueva API de Google Maps v2. Quiero que se vea como en la aplicación de mapas original de Google. Me gusta esto:

Cuando tengo ImageButton adentro, no funciona, se selecciona toda la InfoWindow InfoWindow y no solo el ImageButton . Leí que es porque no hay una View sí, sino que es una instantánea, por lo que los elementos individuales no pueden distinguirse entre sí.

EDITAR: En la documentation (gracias a Disco S2 ):

Como se mencionó en la sección anterior sobre ventanas de información, una ventana de información no es una Vista en vivo, sino que la vista se representa como una imagen en el mapa. Como resultado, no se tienen en cuenta los oyentes que establece en la vista y no puede distinguir entre los eventos de clic en varias partes de la vista. Se le recomienda no colocar componentes interactivos, como botones, casillas de verificación o entradas de texto, en su ventana de información personalizada.

Pero si Google lo usa, debe haber alguna forma de hacerlo. ¿Alguien tiene alguna idea?


Aquí está mi opinión sobre el problema. Creo superposición AbsoluteLayout que contiene la ventana de información (una vista normal con cada bit de interactividad y capacidades de dibujo). Luego, inicio Handler que sincroniza la posición de la ventana de información con la posición del punto en el mapa cada 16 ms. Suena loco, pero en realidad funciona.

Video de demostración: https://www.youtube.com/watch?v=bT9RpH4p9mU (tenga en cuenta que el rendimiento se reduce debido a que el emulador y la grabación de video se ejecutan simultáneamente).

Código de la demostración: https://github.com/deville/info-window-demo

Un artículo que brinda detalles (en ruso): http://habrahabr.ru/post/213415/


Es realmente simple.

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; } });

Simplemente agregue el código anterior en su clase donde está usando GoogleMap. R.layout.info_window_layout es nuestro diseño personalizado que muestra la vista que vendrá en lugar de infowindow. Acabo de agregar la vista de texto aquí. Puede agregar una vista adicional aquí para que sea como el complemento de muestra. Mi info_window_layout fue

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

Espero que ayude Podemos encontrar un ejemplo práctico de infowindow personalizado en http://wptrafficanalyzer.in/blog/customizing-infowindow-contents-in-google-map-android-api-v2-using-infowindowadapter/#comment-39731

EDITADO: Este código muestra cómo podemos agregar una vista personalizada en infoWindow. Este código no manejó los clics en los elementos de Vista personalizada. Por lo tanto, está cerca de responder, pero no es exactamente la respuesta, por eso no se acepta como respuesta.


Estaba buscando una solución a este problema yo mismo sin suerte, así que tuve que lanzar el mío que me gustaría compartir aquí contigo. (Disculpe mi mal inglés) (Es un poco loco responder a otro checo en inglés :-))

Lo primero que intenté fue utilizar una PopupWindow antigua. Es bastante fácil: uno solo tiene que escuchar OnMarkerClickListener y luego mostrar una PopupWindow personalizada encima del marcador. Algunos otros chicos aquí en sugirieron esta solución y en realidad se ve bastante bien a primera vista. Pero el problema con esta solución aparece cuando comienzas a mover el mapa. Tienes que mover el PopupWindow alguna manera, lo que es posible (escuchando algunos eventos de Touch) pero en mi humilde opinión no puedes hacer que se vea lo suficientemente bien, especialmente en algunos dispositivos lentos. Si lo haces de la manera más simple, "salta" de un lugar a otro. También puedes usar algunas animaciones para pulir esos saltos, pero de esta manera PopupWindow siempre estará "un paso atrás" donde debería estar en el mapa, lo cual no me gusta.

En este punto, estaba pensando en alguna otra solución. Me di cuenta de que en realidad no necesitaba tanta libertad, mostrar mis vistas personalizadas con todas las posibilidades que esto conlleva (como barras de progreso animadas, etc.). Creo que hay una buena razón por la que incluso los ingenieros de Google no lo hacen de esta manera en la aplicación Google Maps. Todo lo que necesito es un botón o dos en InfoWindow que mostrará un estado presionado y activará algunas acciones cuando se haga clic. Así que se me ocurrió otra solución que se divide en dos partes:

Primera parte:
La primera parte es poder captar los clics en los botones para activar alguna acción. Mi idea es la siguiente:

  1. Mantenga una referencia a la ventana de información personalizada creada en InfoWindowAdapter.
  2. Envuelva el MapFragment (o MapView ) dentro de un ViewGroup personalizado (el mío se llama MapWrapperLayout)
  3. MapWrapperLayout el MapWrapperLayout de MapWrapperLayout y (si actualmente se muestra InfoWindow) enrute primero MotionEvents a InfoWindow creado previamente. Si no consume los MotionEvents (como porque no hizo clic en ningún área clicable dentro de InfoWindow, etc.), entonces (y solo entonces) deja que los eventos pasen a la superclase de MapWrapperLayout para que eventualmente se entreguen al mapa.

Aquí está el código fuente de 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); } }

Todo esto hará que las vistas dentro del InfoView "en vivo" nuevamente - OnClickListeners comenzará a disparar, etc.

Segunda parte: el problema restante es que, obviamente, no puede ver ningún cambio de interfaz de usuario de su ventana de información en la pantalla. Para hacer eso tienes que llamar manualmente a Marker.showInfoWindow. Ahora, si realiza algún cambio permanente en su ventana de información (como cambiar la etiqueta de su botón a otra cosa), esto es suficiente.

Pero mostrar un botón presionado o algo de esa naturaleza es más complicado. El primer problema es que (al menos) no pude hacer que InfoWindow muestre el estado de los botones normales. Incluso si presioné el botón durante mucho tiempo, simplemente permaneció sin presionar en la pantalla. Creo que esto es algo que maneja el propio marco de mapa que probablemente se asegura de no mostrar ningún estado transitorio en las ventanas de información. Pero podría estar equivocado, no intenté descubrirlo.

Lo que hice fue otro hack desagradable: OnTouchListener al botón y OnTouchListener manualmente su fondo cuando el botón fue presionado o liberado en dos plantillas personalizables, una con un botón en estado normal y la otra en estado presionado. Esto no es muy bueno, pero funciona :). Ahora pude ver el botón cambiar de estado normal a presionado en la pantalla.

Todavía hay un último error: si hace clic en el botón demasiado rápido, no muestra el estado de presionar, simplemente permanece en su estado normal (aunque el clic se dispara para que el botón "funcione"). Al menos así es como aparece en mi Galaxy Nexus. Entonces, lo último que hice fue retrasar el botón en su estado de prensado un poco. Esto también es bastante feo y no estoy seguro de cómo funcionaría en algunos dispositivos más antiguos y lentos, pero sospecho que incluso el propio framework de mapas hace algo como esto. Puede intentarlo usted mismo: cuando hace clic en toda la ventana InfoWindow, permanece un poco más presionada y luego los botones normales (nuevamente, al menos en mi teléfono). Y así es como funciona incluso en la aplicación original de Google Maps.

De todos modos, me escribí una clase personalizada que maneja los botones de cambios de estado y todas las otras cosas que mencioné, así que aquí está el código:

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); }

Aquí hay un archivo de diseño de InfoWindow personalizado que utilicé:

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

Probar el archivo de diseño de la actividad ( MapFragment está dentro de 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>

Y finalmente, el código fuente de una actividad de prueba, que combina todo esto:

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); } }

Eso es. Hasta ahora solo probé esto en mi Galaxy Nexus (4.2.1) y Nexus 7 (también 4.2.1), lo probaré en algún teléfono Gingerbread cuando tenga la oportunidad. Una limitación que encontré hasta ahora es que no puedes arrastrar el mapa desde donde está tu botón en la pantalla y mover el mapa. Probablemente podría ser superado de alguna manera, pero por ahora, puedo vivir con eso.

Sé que este es un truco feo pero simplemente no encontré nada mejor y necesito este patrón de diseño tan mal que esto realmente sería una razón para volver al framework v1 (por cierto. Realmente me gustaría evitar para una nueva aplicación con fragmentos, etc.). Simplemente no entiendo por qué Google no ofrece a los desarrolladores una forma oficial de tener un botón en InfoWindows. Es un patrón de diseño tan común, además este patrón se usa incluso en la aplicación oficial de Google Maps :). Entiendo las razones por las que no pueden simplemente hacer que sus vistas estén "en vivo" en las ventanas de información: esto probablemente mataría el rendimiento al mover y desplazar el mapa. Pero debería haber alguna forma de lograr este efecto sin usar vistas.


He desarrollado un proyecto de estudio de Android para esta pregunta.

capturas de pantalla de salida: -

Descargue el código fuente completo del proyecto Haga clic here

Tenga en cuenta: debe agregar su clave de API en Androidmanifest.xml


Solo una especulación, no tengo suficiente experiencia para probarlo ...) -:

Dado que GoogleMap es un fragmento, debería ser posible capturar el marcador del evento onClick y mostrar una vista de fragmento personalizada. Un fragmento de mapa seguirá siendo visible en el fondo. ¿Alguien lo intentó? ¿Alguna razón por la cual no podría funcionar?

La desventaja es que el fragmento del mapa se congelaría en el backgroud, hasta que un fragmento de información personalizada le devuelva el control.


Veo que esta pregunta ya es vieja pero aún ...

Hicimos una biblioteca sipmle en nuestra empresa para lograr lo que se desea: una ventana de información interactiva con vistas y todo. Puedes verlo en github .

Espero que ayude :)


Para aquellos que no pudieron obtener choose007''s respuesta choose007''s y funcionando

Si clickListener no funciona correctamente en todo momento en chose007''s solución chose007''s , intente implementar View.onTouchListener lugar de clickListener . Maneje el evento táctil usando cualquiera de las acciones ACTION_UP o ACTION_DOWN . Por algún motivo, los mapas infoWindow provocan un comportamiento extraño al enviar a 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; }

Editar: Así es como lo hice paso a paso

Primero infle su propia ventana de información (variable global) en algún lugar de su actividad / fragmento. El mío está dentro de fragmento. También asegúrese de que la vista raíz en el diseño de su ventana de información sea linearlayout (por alguna razón, la relación relativa tomó el ancho total de la pantalla en la ventana de información)

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;

Luego, en la devolución de llamada deMapReady de google maps api de Android (siga esto si no sabe qué es en MapasMapa > Documentación - Introducción )

@Override public void onMapReady(GoogleMap googleMap) { /*mMap is global GoogleMap variable in activity/fragment*/ mMap = googleMap; /*Some function to set map UI settings*/ setYourMapSettings();

Inicialización de MapWrapperLayout http://.com/questions/14123243/google-maps-android-api-v2- interactive-infowindow-like-in-original-android-go / 15040761 # 15040761 39 - altura predeterminada del marcador 20 - offset entre el borde inferior predeterminado de InfoWindow y su contenido borde inferior * /

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; } }); }

Método 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; } });

Marcador de la manija haga clic por separado

@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; }