Monday, October 24, 2011

Custom MapView Popup in Android

I have seen many people asking for the google map application like popup balloon in iPhone.


1) MyMap activity (in which we do set map overlay icon alogn with some data to display). 
2) MyItemizedOverlay class to handle overlays nothing special in it.
3) BalloonOverlayView.
4) BalloonItemizedOverlay.



First layout main.xml



<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainlayout" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<com.google.android.maps.MapView
android:id="@+id/mapview" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:clickable="true"
android:apiKey="YOUR_API_KEY" />
</RelativeLayout>

Second Layout balloon_map_overlay.xml

<?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="horizontal" 
android:paddingBottom="35dip"
android:paddingLeft="10dip" 
android:minWidth="200dip" 
android:id="@+id/balloon_main_layout"
android:background="@drawable/balloon_overlay_bg_selector" 
android:paddingTop="0dip"
android:paddingRight="0dip">


<LinearLayout 
android:layout_width="wrap_content" 
android:layout_height="wrap_content"
android:orientation="vertical" 
android:layout_weight="1"
android:paddingTop="10dip" 
android:id="@+id/balloon_inner_layout">
<TextView android:layout_height="wrap_content"
android:layout_width="fill_parent" 
android:id="@+id/balloon_item_title"
android:text="balloon_item_title" 
android:textSize="16dip"
android:textColor="#FF000000"></TextView>
<TextView android:layout_height="wrap_content"
android:layout_width="fill_parent" 
android:id="@+id/balloon_item_snippet"
android:text="balloon_item_snippet" 
android:textSize="12dip"></TextView>
</LinearLayout>

<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content" 
android:src="@drawable/balloon_overlay_close"
android:id="@+id/close_img_button" 
android:paddingLeft="10dip"
android:paddingBottom="10dip" 
android:paddingRight="8dip"
android:paddingTop="8dip"></ImageView>
</LinearLayout>


Selector xml file drawable folder balloon_overlay_bg_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
     android:state_pressed="true"
     android:drawable="@drawable/balloon_overlay_focused" />
    <item
     android:state_pressed="false"
     android:drawable="@drawable/balloon_overlay_unfocused" />
</selector>


Manifest file name AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

      android:versionCode="1"

      android:versionName="1.0" package="mapviewballoons.example">

    <application android:icon="@drawable/icon" android:label="@string/app_name">

        <activity android:name=".MyMap"

                  android:label="@string/app_name">

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>
    <uses-library android:name="com.google.android.maps" />
</application>
<uses-sdk android:minSdkVersion="3" android:targetSdkVersion="8"/>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest> 

BalloonItemizedOverlay.java


package com.readystatesoftware.mapviewballoons;
import mapviewballoons.example.R; 
import java.lang.reflect.Method;
import java.util.List;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup.LayoutParams;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.OverlayItem;

public abstract class BalloonItemizedOverlay<Item> extends ItemizedOverlay<OverlayItem> {
private MapView mapView;
private BalloonOverlayView balloonView;
private View clickRegion;
private int viewOffset;
final MapController mc;

public BalloonItemizedOverlay(Drawable defaultMarker, MapView mapView) {
super(defaultMarker);
this.mapView = mapView;
viewOffset = 0;
mc = mapView.getController();
}


public void setBalloonBottomOffset(int pixels) {
viewOffset = pixels;
}

protected boolean onBalloonTap(int index) {
return false;
}

@Override
protected final boolean onTap(int index) {

boolean isRecycled;
final int thisIndex;
GeoPoint point;

thisIndex = index;
point = createItem(index).getPoint();

if (balloonView == null) {
balloonView = new BalloonOverlayView(mapView.getContext(), viewOffset);
clickRegion = (View) balloonView.findViewById(R.id.balloon_inner_layout);
isRecycled = false;
} else {
isRecycled = true;
}

balloonView.setVisibility(View.GONE);

List<Overlay> mapOverlays = mapView.getOverlays();
if (mapOverlays.size() > 1) {
hideOtherBalloons(mapOverlays);
}

balloonView.setData(createItem(index));

MapView.LayoutParams params = new MapView.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, point,
MapView.LayoutParams.BOTTOM_CENTER);
params.mode = MapView.LayoutParams.MODE_MAP;

setBalloonTouchListener(thisIndex);

balloonView.setVisibility(View.VISIBLE);

if (isRecycled) {
balloonView.setLayoutParams(params);
} else {
mapView.addView(balloonView, params);
}

mc.animateTo(point);

return true;
}

/**
* Sets the visibility of this overlay's balloon view to GONE. 
*/
private 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 void setBalloonTouchListener(final int thisIndex) {

try {
@SuppressWarnings("unused")
Method m = this.getClass().getDeclaredMethod("onBalloonTap", int.class);

clickRegion.setOnTouchListener(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();
}
// call overridden method
onBalloonTap(thisIndex);
return true;
} else {
return false;
}

}
});

} catch (SecurityException e) {
Log.e("BalloonItemizedOverlay", "setBalloonTouchListener reflection SecurityException");
return;
} catch (NoSuchMethodException e) {
// method not overridden - do nothing
return;
}

}

}


BalloonOverlayView.java

package com.readystatesoftware.mapviewballoons;

import mapviewballoons.example.R;

import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.google.android.maps.OverlayItem;


public class BalloonOverlayView extends FrameLayout {

private LinearLayout layout;
private TextView title;
private TextView snippet;

public BalloonOverlayView(Context context, int balloonBottomOffset) {

super(context);

setPadding(10, 0, 10, balloonBottomOffset);
layout = new LinearLayout(context);
layout.setVisibility(VISIBLE);

LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflater.inflate(R.layout.balloon_map_overlay, layout);
title = (TextView) v.findViewById(R.id.balloon_item_title);
snippet = (TextView) v.findViewById(R.id.balloon_item_snippet);

ImageView close = (ImageView) v.findViewById(R.id.close_img_button);
close.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(OverlayItem item) {
layout.setVisibility(VISIBLE);
if (item.getTitle() != null) {
title.setVisibility(VISIBLE);
title.setText(item.getTitle());
} else {
title.setVisibility(GONE);
}
if (item.getSnippet() != null) {
snippet.setVisibility(VISIBLE);
snippet.setText(item.getSnippet());
} else {
snippet.setVisibility(GONE);
}
}

}





MyItemizedOverlay.java

package mapviewballoons.example;

import java.util.ArrayList;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.widget.Toast;

import com.google.android.maps.MapView;
import com.google.android.maps.OverlayItem;

import com.readystatesoftware.mapviewballoons.BalloonItemizedOverlay;

public class MyItemizedOverlay extends BalloonItemizedOverlay<OverlayItem> {

private ArrayList<OverlayItem> m_overlays = new ArrayList<OverlayItem>();
private Context c;
public MyItemizedOverlay(Drawable defaultMarker, MapView mapView) {
super(boundCenter(defaultMarker), mapView);
c = mapView.getContext();
}

public void addOverlay(OverlayItem overlay) {
   m_overlays.add(overlay);
   populate();
}

@Override
protected OverlayItem createItem(int i) {
return m_overlays.get(i);
}

@Override
public int size() {
return m_overlays.size();
}

@Override
protected boolean onBalloonTap(int index) {
Toast.makeText(c, "onBalloonTap for overlay index " + index,
Toast.LENGTH_LONG).show();
return true;
}
}

Main Launcher class MyMap.java

package mapviewballoons.example;

import java.util.List;

import android.graphics.drawable.Drawable;
import android.os.Bundle;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.OverlayItem;

public class MyMap extends MapActivity {

MapView mapView;
List<Overlay> mapOverlays;
Drawable drawable;
Drawable drawable2;
MyItemizedOverlay itemizedOverlay;
MyItemizedOverlay itemizedOverlay2;
@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mapView = (MapView) findViewById(R.id.mapview);
mapView.setBuiltInZoomControls(true);
mapOverlays = mapView.getOverlays();
// first overlay
drawable = getResources().getDrawable(R.drawable.marker);
itemizedOverlay = new MyItemizedOverlay(drawable, mapView);
GeoPoint point = new GeoPoint((int)(51.5174723*1E6),(int)(-0.0899537*1E6));
OverlayItem overlayItem = new OverlayItem(point, "Umar Shafique", 
"(here goes 1)");
itemizedOverlay.addOverlay(overlayItem);
GeoPoint point2 = new GeoPoint((int)(51.515259*1E6),(int)(-0.086623*1E6));
OverlayItem overlayItem2 = new OverlayItem(point2, "Abdul Karim", 
"here goes 2");
itemizedOverlay.addOverlay(overlayItem2);
mapOverlays.add(itemizedOverlay);
// second overlay
drawable2 = getResources().getDrawable(R.drawable.marker2);
itemizedOverlay2 = new MyItemizedOverlay(drawable2, mapView);
GeoPoint point3 = new GeoPoint((int)(51.513329*1E6),(int)(-0.08896*1E6));
OverlayItem overlayItem3 = new OverlayItem(point3, "Arslan Ilyas", 
"here goes 3");
itemizedOverlay2.addOverlay(overlayItem3);
GeoPoint point4 = new GeoPoint((int)(51.51738*1E6),(int)(-0.08186*1E6));
OverlayItem overlayItem4 = new OverlayItem(point4, "Ahsan", 
"here goes 4");
itemizedOverlay2.addOverlay(overlayItem4);
mapOverlays.add(itemizedOverlay2);
final MapController mc = mapView.getController();
mc.animateTo(point2);
mc.setZoom(16);
    }
@Override
protected boolean isRouteDisplayed() {
return false;
}

}







Enjoy coding
Download code: