Skip Navigation
Trulia Logo

Trulia Blog

Custom Map Markers for Android Google Maps

When we first implemented map markers on our Android apps we were just using an image to highlight each location. Over time we wanted to add the specific price as well. As I started working on an implementation of that feature I was hard pressed to find any articles or examples of what we wanted to accomplish. So I had to start from scratch and figure out a method by myself. The initial implementation is currently in use on our Trulia Rentals android app. However, there are some deficiencies with that version and some of the code is sub-optimal. In this post I will detail the new method I am working on (which will be ported into all of our android apps).

Here are the pieces that we will need to make this custom marker happen:

  1. Activity— Yes, we need an activity to run our example app.
  2. MapView— We like to sub-class MapView and create a special custom version for Trulia.
  3. Overlay— We sub-class ItemizedOverlay for Trulia.
  4. OverlayItem— Using a custom OverlayItem we can add functionality to build our custom marker.
  5. Two drawablesfor the selected and unselected marker. We use 9 patch pngs in this example.
  6. Views — We have a main layout container which we inject our CustomMapView. We also have a custom view that we use for the marker itself.

View Layouts

main.xml — This is the view we use to setContentView() in our Activity


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

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

    android:id="@+id/map_container"

    android:layout_width="fill_parent"

    android:layout_height="wrap_content"

    android:orientation="vertical" />

marker.xml — This is the base layout for an individual marker used to create the custom marker


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

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

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:gravity="center" >

    <TextView

        android:id="@+id/text"

        android:layout_width="fill_parent"

        android:layout_height="fill_parent"

        android:background="@drawable/ic_map_marker_unviewed"

        android:paddingBottom="15sp"

        android:paddingLeft="10sp"

        android:paddingRight="10sp"

        android:paddingTop="10sp"

        android:textColor="@color/black"

        android:textSize="18dp" />

</FrameLayout>

Drawables

Here are the two drawables I am using for the selected and unselected state:

Unselected Marker

Unselected Drawable Marker

Selected Marker

Selected Drawable Marker

Custom OverlayItem

Now let’s start with the custom OverlayItem that we will use to build the custom marker.


class CustomItem extends OverlayItem {

	// text we want to overlay on top of our marker

	// for trulia this is going to be the price of the property

	private String label;

	// this is the marker we will send back to the Overlay to use when drawing

	private StateListDrawable marker = null;

	// this framelayout holds the view we will be turning into a drawable

	private FrameLayout markerLayout;

	// since we are going to use a StateListDrawable we will need to access

	// a few of the state values from android

	private int stateSelected = android.R.attr.state_selected;

	private int statePressed = android.R.attr.state_pressed;

	private int stateFocused = android.R.attr.state_focused;

	// we need a Context and Resource objects to pull in the marker drawables

	private Context context;

	private Resources res;

	// these are the resource ids for the two differnt marker states

	private static final int MARKER_DRAWABLE_UNVIEWED

										= R.drawable.ic_map_marker_unviewed;

	private static final int MARKER_DRAWABLE_SELECTED

										= R.drawable.ic_map_marker_selected;

	// Constructor which besides a GeoPoint, laben and snippet also gets pass

	// our

	// marker frame layout as well as the current application context

	CustomItem(GeoPoint pt, String label, String snippet,

			FrameLayout markerLayout, Context context) {

		super(pt, label, snippet);

		// set our class vars

		this.markerLayout = markerLayout;

		this.label = label;

		this.context = context;

		this.res = context.getResources();

		// now generate the state list drawable

		this.marker = generateStateListDrawable();

	}

	// main function where we create our Drawable by using the marker layout,

	// adding our string label, then converting to a bitmap drawable

	public Drawable generateMarker(boolean selected) {

		Bitmap viewCapture = null;

		Drawable drawOverlay = null;

		// make sure our marker layout isn't null

		if (markerLayout != null) {

			// set the text string into the view before we turn it into an image

			TextView textView = (TextView) markerLayout.findViewById(R.id.text);

			textView.setText(label);

			// calling setBackgroundResource seems to overwrite our padding in

			// the layout;

			// the following is a hack i found in this google bug report; fixes

			// gravity issue as well

			// http://code.google.com/p/android/issues/detail?id=17885

			int paddingTop = textView.getPaddingTop();

			int paddingLeft = textView.getPaddingLeft();

			int paddingRight = textView.getPaddingRight();

			int paddingBottom = textView.getPaddingBottom();

			// set the background bubble image based on whether its selected or

			// not

			if (selected) {

				textView.setBackgroundResource(MARKER_DRAWABLE_SELECTED);

			} else {

				textView.setBackgroundResource(MARKER_DRAWABLE_UNVIEWED);

			}

			// part of the hack above to reset the padding specified in the

			// marker layout

			textView.setPadding(paddingLeft, paddingTop, paddingRight,

					paddingBottom);

			// we need to enable the drawing cache

			markerLayout.setDrawingCacheEnabled(true);

			// this is the important code

			// Without it the view will have a dimension of 0,0 and the bitmap

			// will be null

			markerLayout.measure(

					MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),

					MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

			markerLayout.layout(0, 0, markerLayout.getMeasuredWidth(),

					markerLayout.getMeasuredHeight());

			// we need to build our drawing cache

			markerLayout.buildDrawingCache(true);

			// not null? then we are ready to capture our bitmap image

			if (markerLayout.getDrawingCache() != null) {

				viewCapture = Bitmap.createBitmap(markerLayout

						.getDrawingCache());

				// if the view capture is not null we should turn off the

				// drawing cache

				// and then create our marker drawable with the view capture

				if (viewCapture != null) {

					markerLayout.setDrawingCacheEnabled(false);

					drawOverlay = new BitmapDrawable(viewCapture);

					return drawOverlay;

				}

			} else {

				Log.d("CustomMapMarkers",

						"Item * generateMarker *** getDrawingCache is null");

			}

		}

		Log.d("CustomMapMarkers", "Item * generateMarker *** returning null");

		return null;

	}

	/*

	 * (copied from the Google API docs) Sets the state of a drawable to match a

	 * given state bitset. This is done by converting the state bitset bits into

	 * a state set of R.attr.state_pressed, R.attr.state_selected and

	 * R.attr.state_focused attributes, and then calling {@link

	 * Drawable.setState(int[])}.

	 */

	public static void setState(final Drawable drawable, final int stateBitset) {

		final int[] states = new int[3];

		int index = 0;

		if ((stateBitset & ITEM_STATE_PRESSED_MASK) > 0)

			states[index++] = android.R.attr.state_pressed;

		if ((stateBitset & ITEM_STATE_SELECTED_MASK) > 0)

			states[index++] = android.R.attr.state_selected;

		if ((stateBitset & ITEM_STATE_FOCUSED_MASK) > 0)

			states[index++] = android.R.attr.state_focused;

		drawable.setState(states);

	}

	@Override

	public StateListDrawable getMarker(int stateBitset) {

		if (marker == null)

			return null;

		setState((Drawable) marker, stateBitset);

		marker.setBounds(0, 0, marker.getIntrinsicWidth(),

				marker.getIntrinsicHeight());

		return (marker);

	}

	public Drawable getDefaultMarker() {

		if (marker == null)

			marker = generateStateListDrawable();

		return marker;

	}

	// We create our statelist drawable by setting various states and the marker

	// to be displayed by that state;

	// the generateMarker() function takes in whether the drawable is selected

	// or not selected

	private StateListDrawable generateStateListDrawable() {

		StateListDrawable drawables = new StateListDrawable();

		drawables.addState(new int[] { stateSelected }, generateMarker(true));

		drawables.addState(new int[] { statePressed }, generateMarker(true));

		drawables.addState(new int[] { stateFocused }, generateMarker(true));

		drawables.addState(new int[] {}, generateMarker(false));

		return drawables;

	}

}

Custom Overlay

Our custom overlay class is very simple:


class CustomOverlay extends ItemizedOverlay<CustomItem> {

    protected ArrayList<CustomItem> mOverlays = new ArrayList<CustomItem>();

    public CustomOverlay(Drawable defaultMarker) {

        super(boundCenterBottom(defaultMarker));

    }

    @Override

    protected CustomItem createItem(int i) {

        return mOverlays.get(i);

    }

    @Override

    public int size() {

        return mOverlays.size();

    }

    public void addOverlay(CustomItem overlay) {

        mOverlays.add(overlay);

        populate();

    }

    public void addOverlay(CustomItem overlay, Drawable marker) {

        overlay.setMarker(boundCenterBottom(marker));

        addOverlay(overlay);

    }

    @Override

    public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow) {

        super.draw(canvas, mapView, false);

    }

}

Custom MapView

And our custom MapView is also very simple:


public class CustomMapView extends MapView {

    private Context context;

    public CustomMapView(Context _context, AttributeSet attrs) {

        super(_context, attrs);

        context = _context;

    }

    public CustomMapView(android.content.Context context,

                                   android.util.AttributeSet attrs, int defStyle) {

        super(context, attrs, defStyle);

    }

    public CustomMapView(android.content.Context context, java.lang.String apiKey) {

        super(context, apiKey);

    }

    public void setZoomControls() {

        getZoomButtonsController().setOnZoomListener(new OnZoomListener() {

            @Override

            public void onVisibilityChanged(boolean arg0) {

            }

            @Override

            public void onZoom(boolean zoomIn) {

            }

        });

    }

}

Activity

This is what our Activity looks like to pull it all together:


public class CustomMapMarkerActivity extends MapActivity {

    private static final String DEV_MAP_API_KEY = "{ADD_YOUR_OWN_MAP_KEY}";

    private LinearLayout mapContainer;

    private CustomMapView mapView;

    private static final int CENTER_LAT = (int) (37.76474 * 1E6);

    private static final int CENTER_LON = (int) (-122.44864 * 1E6);

    private static final GeoPoint CENTER_POINT = new GeoPoint(CENTER_LAT, CENTER_LON);

    /** Called when the activity is first created. */

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        mapContainer = (LinearLayout) findViewById(R.id.map_container);

        if (mapContainer.indexOfChild(mapView) == -1) {

            mapView = generateMapView();

            mapContainer.addView(mapView);

        }

        if (mapView != null) {

            List<Overlay> mapOverlays = mapView.getOverlays();

            FrameLayout markerLayout =

                      (FrameLayout) getLayoutInflater().inflate(R.layout.marker, null);

            CustomItem overlayItem =

                      new CustomItem(CENTER_POINT, "TEST", "",

                                                markerLayout, getApplicationContext());

            CustomOverlay itemizedOverlay =

                                new CustomOverlay(overlayItem.getDefaultMarker());

            itemizedOverlay.addOverlay(overlayItem);

            mapOverlays.add(itemizedOverlay);

            mapView.invalidate();

        }

    }

    /**

     * Generate the map view last location and zoom level set. called by

     * onCreate

     */

    private CustomMapView generateMapView() {

        if (mapView != null)

            return mapView;

        CustomMapView mapView = new CustomMapView(this, DEV_MAP_API_KEY);

        mapView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,

                                                  LayoutParams.WRAP_CONTENT));

        mapView.getController().setZoom(13);

        mapView.setBuiltInZoomControls(true);

        mapView.setClickable(true);

        mapView.setZoomControls();

        mapView.getController().animateTo(CENTER_POINT);

        return mapView;

    }

    @Override

    protected boolean isRouteDisplayed() {

        return false;

    }

}

SOURCE CODE: ZIP