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:
- Activity— Yes, we need an activity to run our example app.
- MapView— We like to sub-class MapView and create a special custom version for Trulia.
- Overlay— We sub-class ItemizedOverlay for Trulia.
- OverlayItem— Using a custom OverlayItem we can add functionality to build our custom marker.
- Two drawablesfor the selected and unselected marker. We use 9 patch pngs in this example.
- 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
Selected 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