Google Maps en Android (III)

Buenas, estas semanas las estamos dedicando a la api de Google Maps en Android, aún nos queda algún tema por explicar así que vamos a continuar con ello. Esta semana vamos a ver como añadir nuestros propios marcadores en los mapas y como hacer el típico "balloon" o bocadillo que solemos ver en la versión web. Se puede simplificar y poner solamente el marcador (a veces solo necesitaremos esto) pero otras veces necesitaremos darle al usuario mucha más información y esta es una buena forma de hacerlo.


Vamos a continuar el ejemplo de la semana pasada donde teníamos el mapa creado y habíamos localizado nuestro dispositivo. Ahora vamos a ir haciendo unas clases y os explicare para que sirve cada una.

Lo primero que vamos a hacer es una clase que se encargará de dibujar estos balloon a la que llamaremos BalloonOverlayView y la cual va a heredar de Framelayout. A parte también vamos a necesitar alguna imagen y un layout que luego os diré como crear. Vamos a ver primero el código de esta clase.


public class BalloonOverlayView<Item extends OverlayItem> extends FrameLayout {

 protected LinearLayout layout;
 protected TextView title;
 protected TextView snippet;
 protected LayoutInflater layoutinflater;
 protected View balloonview;
 protected ImageView imgclose;

 protected int getIdView(){return R.layout.window_balloon_overlay;}
 
 public BalloonOverlayView(final Context context, int balloonBottomOffset) {
  super(context);
  
  setPadding(10, 0, 10, balloonBottomOffset);
  layout = new LinearLayout(context);
  layout.setVisibility(VISIBLE);
  layoutinflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  balloonview = layoutinflater.inflate(getIdView(), layout);
  title = (TextView) balloonview.findViewById(R.id.balloon_item_title);
  snippet = (TextView) balloonview.findViewById(R.id.balloon_item_snippet);
  
  title.setVisibility(GONE);
  snippet.setVisibility(GONE);

  imgclose = (ImageView) balloonview.findViewById(R.id.close_img_button);
  imgclose.setOnClickListener(new OnClickListener() {
   public void onClick(View v) {layout.setVisibility(GONE);}
  });
  
  FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
  params.gravity = Gravity.NO_GRAVITY;
  addView(layout, params);   
 }
 
 public void setData(Item item, Context mContext) {
  
  layout.setVisibility(VISIBLE);
  if (item.getTitle() != null && item.getTitle().length() > 0) {
   title.setVisibility(VISIBLE);
   title.setText(item.getTitle());
  } else {
   title.setVisibility(GONE);
  }
  if (item.getSnippet() != null && item.getSnippet().length() > 0) {
   snippet.setVisibility(VISIBLE);
   snippet.setText(item.getSnippet());
  } else {
   snippet.setVisibility(GONE);
  }  
 }

}


Lo primero en esta clase es declarar los elementos que vamos a utilizar, lo hago protected porque de esta forma os va a ser muy fácil crear clases que hereden de esta y hacer casos específicos si así lo necesitáis y a los que solo puedan acceder las clases que hereden. Entre los elementos tenemos 2 TextView para los textos y un ImageButton para cerrar el balloon.

Tenemos también un método getIdView que nos va a servir para obtener la referencia al layout del balloon y es protected por el mismo motivo de antes, por si heredais podáis cambiarlo fácilmente y añadir otras funcionalidades.

En el constructor a parte de inicializar los elementos con setPadding se posiciona la vista y layout lo utilizamos como contenedor del layout que crearemos más tarde y el cual se añade al final. El método setData sirve para hacer visible nuestro balloon y rellenar los TextView desde un elemento Item.

La siguiente clase va a encargarse de gestionar la forma en la que se crean y se comportan los balloon, le llamaremos BalloonItemizedOverlay y hereda de ItemizedOverlay que es una clase a su vez derivada de Overlay y está pensada para pintar los marcadores en los mapas (maneja listas de items).


public abstract class BalloonItemizedOverlay<Item extends OverlayItem> extends ItemizedOverlay<Item> {

 private MapView mapView;
 private BalloonOverlayView<Item> balloonView;
 private View clickRegion;
 private int viewOffset;
 final MapController mc;
 private Item currentFocussedItem;
 private int currentFocussedIndex;

 private Context mContext;
 
 public BalloonItemizedOverlay(Drawable defaultMarker, MapView mapView) {
  super(defaultMarker);
  mContext = mapView.getContext();
  this.mapView = mapView;
  viewOffset = 0;
  mc = mapView.getController();
 }
 
 public void setBalloonBottomOffset(int pixels) {
  viewOffset = pixels;
 }
 public int getBalloonBottomOffset() {
  return viewOffset;
 }
 
 protected boolean onBalloonTap(int index, Item item) {
  return false;
 }
 
 @Override
 protected final boolean onTap(int index) {
  
  currentFocussedIndex = index;
  currentFocussedItem = createItem(index);
  
  boolean isRecycled;
  if (balloonView == null) {
   balloonView = createBalloonOverlayView();
   clickRegion = (View) balloonView.findViewById(R.id.balloon_inner_layout);
   clickRegion.setOnTouchListener(createBalloonTouchListener());
   isRecycled = false;
  } else {
   isRecycled = true;
  }
 
  balloonView.setVisibility(View.GONE);
  
  List<Overlay> mapOverlays = mapView.getOverlays();
  if (mapOverlays.size() > 1) {
   hideOtherBalloons(mapOverlays);
  }
  
  balloonView.setData(currentFocussedItem, mContext);
  
  GeoPoint point = currentFocussedItem.getPoint();
  MapView.LayoutParams params = new MapView.LayoutParams(
    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, point,
    MapView.LayoutParams.BOTTOM_CENTER);
  params.mode = MapView.LayoutParams.MODE_MAP;
  
  balloonView.setVisibility(View.VISIBLE);
    
  if (isRecycled) {
   balloonView.setLayoutParams(params);
  } else {
   mapView.addView(balloonView, params);
  }
  
  mc.animateTo(point);
  
  return true;
 }
 
 protected BalloonOverlayView<Item> createBalloonOverlayView() {
  return new BalloonOverlayView<Item>(getMapView().getContext(), getBalloonBottomOffset());
 }
 
 protected MapView getMapView() {
  return mapView;
 }
 
 protected void hideBalloon() {
  if (balloonView != null) {
   balloonView.setVisibility(View.GONE);
  }
 }
 
 private void hideOtherBalloons(List<Overlay> overlays) {  
  for (Overlay overlay : overlays) {
   if (overlay instanceof BalloonItemizedOverlay<?> && overlay != this) {
    ((BalloonItemizedOverlay<?>) overlay).hideBalloon();
   }
  }  
 }
 
 private OnTouchListener createBalloonTouchListener() {
  return new OnTouchListener() {
   public boolean onTouch(View v, MotionEvent event) {
    
    View l =  ((View) v.getParent()).findViewById(R.id.balloon_main_layout);
    Drawable d = l.getBackground();
    
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
     int[] states = {android.R.attr.state_pressed};
     if (d.setState(states)) {
      d.invalidateSelf();
     }
     return true;
    } else if (event.getAction() == MotionEvent.ACTION_UP) {
     int newStates[] = {};
     if (d.setState(newStates)) {
      d.invalidateSelf();
     }
     onBalloonTap(currentFocussedIndex, currentFocussedItem);
     return true;
    } else {
     return false;
    }
    
   }
  };
 }
 
}


Por su longitud esta clase parece complicada pero si os fijáis tenemos métodos para crear el balloon instanciando la primera clase, otros para ocultar los balloon, también el constructor que inicializa todo lo necesario. Vamos a centrarnos en onTap y en createBalloonTouchListener. onTap se encarga de detectar cual es el marcador que se ha pulsado, comprobar si ya tenemos otros balloon abiertos y cerrarlos, obtener el nuevo balloon, pasarle los datos y mostrarlo.

CreateBalloonTouchListener setea el método onTouchListener, verifica cuando empezamos a presionar con el dedo la pantalla (ACTION_DOWN) y cuando dejamos de presionar (ACTION_UP), en ese momento llamamos al método onBalloonTap de nuestro balloon.

La última clase es muy simple, le vamos a llamar MapItemizedOverlay y hereda de la anterior, BalloonItemizedOverlay. Se podría intentar unir estas dos en una sola, pero con lo que está en esta clase y sustituyendo la clase de la que hereda por ItemizedOverlay obtenemos una clase para gestionar marcadores sin balloon. Veamos que tiene:


public class MapItemizedOverlay extends BalloonItemizedOverlay<OverlayItem> {

 private ArrayList<OverlayItem> mOverlays = new ArrayList<OverlayItem>();
 
 public MapItemizedOverlay(Drawable defaultMarker, Context context, MapView mapview) {
    super(boundCenter(defaultMarker),mapview);
    
 }
 
 public void addOverlay(OverlayItem overlay) {
     mOverlays.add(overlay);
     populate();     
 }
 
 @Override
 protected OverlayItem createItem(int i) {
   return mOverlays.get(i);
 }
  
 @Override
 protected boolean onBalloonTap(int index, OverlayItem item) {
  return true;
 }
 
 @Override
 public int size() {
   return mOverlays.size();
 }

}

Esta es más simple, tiene una lista con los overlays que vamos a ir añadiendo, un método para añadir, constructor y poco más. Ahora vamos a ver que imágenes y layouts necesitamos.


Vamos a necesitar 3 imágenes, una para el botón de cerrar a la que llamaremos close_img_button.png y otra que será el balloon, nosotros le llamaremos balloon_overlay_bg.png y a la cual abriremos con el programa draw9patch y le aplicaremos los patch necesarios para que se pueda ampliar al gusto de cada uno. Os dejo una imagen de ejemplo para que veáis el efecto final. La última es el marcador que será lo que se va a ver en el mapa antes de abrir el balloon y sobre el que tenéis que pulsar, le vamos a llamar marker.




Una vez listas las  imágenes vamos a crear el layout de nuestro balloon al cual llamaremos window_balloon_overlay.xml y va a tener los siguientes elementos. Fijaros que he puesto los estilos en el mismo layout pero es recomendable que os hagáis unos estilos en el archivo styles.xml, de esta forma conseguireis simplificar el layout y verlo más claro y os será más fácil adaptarlo para diferentes tamaños de pantalla:


<?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:paddingLeft="35dip" android:paddingRight="35dip">
 <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content">     
  <LinearLayout android:id="@+id/balloon_main_layout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingBottom="35dip" android:paddingLeft="10dip" android:minWidth="200dip" android:background="@drawable/balloon_overlay_bg" android:paddingTop="0dip" android:paddingRight""0dip" android:layout_marginTop="3dip" android:layout_marginRight="3dip">
   <LinearLayout android:id="@+id/balloon_inner_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:layout_weight="1" android:paddingTop="10dip">
    <TextView android:id="@+id/balloon_item_title" android:layout_width="fill_parent" android:layout_height="wrap_content"></TextView>
    <TextView android:id="@+id/balloon_item_snippet" android:layout_width="fill_parent" android:layout_height="wrap_content"></TextView>
   </LinearLayout>
  </LinearLayout>
  <ImageView android:id="@+id/close_img_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/close_img_button" android:layout_alignParentRight="true" android:layout_alignParentTop="true" />
 </RelativeLayout>
</LinearLayout>


Hasta aquí ya tenemos todo lo necesario para mostrar los marcadores y los balloons ahora vamos a ver como utilizarlo en el código que hicimos la semana pasada. Primero debeis recolectar 3 o 4 coordenadas GPS, que estén más o menos cerca para que podáis abrir el balloon y luego otro y otro fácilmente. Ahora debemos declarar un List donde almacenar cada una de las capas o Overlays:


protected List<Overlay> mapOverlays;


En el método inicializeMap después de inicializar nuestro MapView vamos a obtener una referencia a la lista de capas o overlays del MapView en mapOverlays:


mapOverlays = mapView.getOverlays();


Justo antes del punto donde añadiremos los marcadores vamos a declarar un objeto drawable para el marcador:


Drawable drawable = getResources().getDrawable(R.drawable.marker);


Y por último, para añadir cada una de estos markers debéis añadir el siguiente código:


OverlayItem overlayItem = new OverlayItem(new GeoPoint(0,0), "titulo", "descripcion");     
MapItemizedOverlay itemizedoverlay = new MapItemizedOverlay(drawable, mapView.getContext(), mapView);
itemizedoverlay.addOverlay(overlayItem);
mapOverlays.add(itemizedoverlay);


Se define un OverlayItem que va a tener la información con su título, descripción y un GeoPoint, yo he puesto latitud 0 y longitud 0, pero vosotros poned la que necesiteis. Luego instanciamos uno de nuestros objetos MapItemizedOverlay al cual le pasamos la imagen del marcador, el context y el mapView. Luego añadimos a este el OverlayItem y por último lo añadimos a la lista de capas del MapView.

Bueno, hasta aquí llegamos hoy, que ya es bastante. Cuando intente por primera vez hacer esto todo me salió mal y eso que fui guiándome por diferentes blog y documentación, y tuve que hacer bastantes pruebas, modificaciones hasta que quedó a mi gusto. Así que no desesperéis ya que es un tema interesante.

Creo que aún tengo para un par de temas más con Google Maps así que la semana que viene volveré a hablar de Google Maps en Android.

Comments are closed.