Often, when drafting a project a situation arises where the assigned tasks are difficult to solve by existing standard development tools. Then third-party resources come to the aid in the form of libraries and other goodies. So it happened with our newly developed application MegaSOS for Android platform. But let me cover each thing in its turn.
Android is the fastest growing mobile operating system based on the Linux kernel, and having its own Java implementation from Google. It’s the brainchild of the search giant, which not only has released an open source platform, but also created the Open Handset Alliance, through which we have SDK at our disposal.
Google has adapted its many services under the mobile platform, and Google Maps being among them. Just what we will be talking about today.
Android Google Maps APIv2 is quite topical at the moment. I think it’s needless to talk about how to connect the service, get the keys and start to work - you can easily find such information on the Internet. So we'll go straight to the point.
When designing the project the task was to display the map markers, which are divided into several types (with help of Google Maps markers clustering). But, for example, nearby markers when zoomed out overlapped the way it all looked like one continuous blur. They were to be grouped - clustered - with regard to the type of each marker. Standard toolkit looked poorly for solving such problems. Of course, you could create your own class that inherits markers, and extending it at the same time. But why reinvent the wheel if a similar problem could have being solved before you. After a careful search we found two libraries designed if not completely, then to a great extent to simplify our lives without reading huge tutorial. This is unpretentious Android Maps Extensions and sometimes matchless Clusterkraft.
Android Maps Extensions
This library extends the service possibilities of GoogleMap and related facilities for Android API v2. Its main advantage is so necessary for us clustering of overlapping markers. It can be seen on the following screenshots:
The developers have indicated a whole list of classes and methods that can be used for the mentioned tasks:
- Marker Clustering with
GoogleMap.setClustering(ClusteringSettings)
ClusteringSettings.clusterOptionsProvider(ClusterOptionsProvider)
is required for it all to workClusteringSettings.addMarkersDynamically
when having too many markersClusteringSettings.clusterSize
to control... cluster sizeObject getData()
andsetData(Object)
onMarker, Circle, GroundOverlay, Polygon, Polyline
orTileOverlay
List
andGoogleMap.getMarkers() List
GoogleMap.getCircles() List
etc.GoogleMap.getGroundOverlays() boolean Circle.contains(LatLng)
Marker GoogleMap.getMarkerShowingInfoWindow()
List
GoogleMap.getDisplayedMarkers() float GoogleMap.getMinZoomLevelNotClustered(Marker)
Marker.animatePosition(LatLng target, AnimationSettings settings)
Marker.setClusterGroup(int)
At the time we were developing our project, this library was unable to refer to different groups of markers. This led to a clustering markers in Google Maps absolutely everywhere, so finally we decided not to use it. We reported the problem to developers of the library, which in turn do not leave us without attention, and now we have a wonderful and convenient method Marker.setClusterGroup(int)
.
The library is easy to use and for its integration you are just to import it as an external module. Then, using the main activity SupportMapFragment
or MapView
в xml-файле, in xml-file, cause getExtendedMap
instead of getMap
, which is used in the standard library.
Clusterkraft
This library was used in this project instead of Maps Extensions, and immediately proved to be a positive side. Firstly, its work turned to be much more stable, dynamic addition of markers worked much faster, and clustering was performed with a nice animation. Secondly, it showed the class that provides the opportunity to replace the icon of the marker cluster, depending on its content.
For integration the library was imported as a module.
Activity lay-out, which displays a map looks like this:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".Map"> <FrameLayout android:id="@+id/frame_map" android:layout_width="match_parent" android:layout_height="match_parent" class="com.stfalcon.megasos.views.MySupportMapFragment"> </FrameLayout> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent" /> </RelativeLayout>
In this case, for the card FrameLayout - frame_map
was used. Empty FrameLayout
with the transparent background acted as a «magic spell», which solved the problem with drawing the map on devices with versions prior to Ice Cream Sandwich (when opening / closing the pop-up menu, or slide a black area in the form of the window remained on the map).
onCreate
method is as follows:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_map); SupportMapFragment mapFragment = SupportMapFragment.newInstance(); getSupportFragmentManager().beginTransaction().add(R.id.frame_map, mapFragment, "MAP").commit(); }
In onPostCreate
method a card is initialized and we can put a marker:
@Override public void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); GoogleMap myMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentByTag("MAP")).getMap(); myMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); initMap(); MarkerOptions options = new MarkerOptions() .anchor(0.5f, 0.5f) .position(new LatLng(latitude, longitude)) .draggable(false) .icon(BitmapDescriptorFactory .fromResource(marker_resource)); myMap.addMarker(options); updateMap(); initMap(); }
Initialization of a card:
private void initMap() { if (map == null) { SupportMapFragment mapFragment = ((SupportMapFragment) getSupportFragmentManager().findFragmentByTag("MAP")); if (mapFragment != null) { map = mapFragment.getMap(); if (map != null) { UiSettings uiSettings = map.getUiSettings(); uiSettings.setAllGesturesEnabled(false); uiSettings.setScrollGesturesEnabled(true); uiSettings.setZoomGesturesEnabled(true); map.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() { @Override public void onCameraChange(CameraPosition arg0) { moveMapCameraToBoundsAndInitClusterkraf(); MapPointsProvider.getInstance().generate(mCallback, check_sos, check_company); initMap(); } }); } } } else { moveMapCameraToBoundsAndInitClusterkraf(); } }
Setting the camera to the previous position when the orientation was changed:
private void moveMapCameraToBoundsAndInitClusterkraf() { if (map != null && options != null && inputPoints != null) { try { if (restoreCameraPosition != null) { /** * if a restoreCameraPosition is available, move the camera * there */ map.moveCamera(CameraUpdateFactory.newCameraPosition(restoreCameraPosition)); restoreCameraPosition = null; } initClusterkraf(); } catch (IllegalStateException ise) { } } }
Initialization of Clusterkraft (with the addition of each marker / marker groups you need to re-conduct clustering):
private void initClusterkraf() { if (map != null && inputPoints != null && inputPoints.size() > 0) { com.twotoasters.clusterkraf.Options options = new com.twotoasters.clusterkraf.Options(); applyDemoOptionsToClusterkrafOptions(options); if (clusterkraf == null) { clusterkraf = new Clusterkraf(map, options, inputPoints); } else { clusterkraf.clear(); //map.clear(); clusterkraf.addAll(inputPoints); } if (!check_sos && !check_company) { clusterkraf.clear(); } } if (map != null && inputPoints.size() == 0 && clusterkraf != null) { if (!check_sos && !check_company) { clusterkraf.clear(); } } }
A class that describes the markers clustering rules uses for them the corresponding pin icons. If this is a cluster, then the class appends the number of elements in Pin with corresponding color:
public class MapMarkerOptionsChooser extends MarkerOptionsChooser { private final WeakReferencecontextRef; private final Paint clusterPaintLarge; private final Paint clusterPaintMedium; private final Paint clusterPaintSmall; public MapMarkerOptionsChooser(Context context) { this.contextRef = new WeakReference (context); Resources res = context.getResources(); clusterPaintMedium = new Paint(); clusterPaintMedium.setColor(Color.BLACK); clusterPaintMedium.setAlpha(255); clusterPaintMedium.setTextAlign(Paint.Align.CENTER); clusterPaintMedium.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)); clusterPaintMedium.setTextSize(res.getDimension(R.dimen.cluster_text_size_medium)); clusterPaintSmall = new Paint(clusterPaintMedium); clusterPaintSmall.setTextSize(res.getDimension(R.dimen.cluster_text_size_small)); clusterPaintLarge = new Paint(clusterPaintMedium); clusterPaintLarge.setTextSize(res.getDimension(R.dimen.cluster_text_size_large)); } @Override public void choose(MarkerOptions markerOptions, ClusterPoint clusterPoint) { Context context = contextRef.get(); if (context != null) { Resources res = context.getResources(); boolean isCluster = clusterPoint.size() > 1; BitmapDescriptor icon; String title; if (isCluster) { int company = 0; int sos = 0; int clusterSize = clusterPoint.size(); for (int i = 0; i < clusterSize; i++) { if (((MarkerData) clusterPoint.getPointAtOffset(i).getTag()).getType() == MarkerData.SOS) { sos++; } if (((MarkerData) clusterPoint.getPointAtOffset(i).getTag()).getType() == MarkerData.COMPANY) { company++; } } if (sos > 0 && company == 0) { icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group, clusterSize, Color.WHITE)); } else if (sos == 0 && company > 0) { icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group_companies, clusterSize, Color.BLACK)); } else { icon = BitmapDescriptorFactory.fromBitmap(getClusterBitmap(res, R.drawable.ic_map_group_of_groups, clusterSize, Color.BLACK)); } } else { MarkerData data = (MarkerData) clusterPoint.getPointAtOffset(0).getTag(); if (data.getType() == MarkerData.SOS) { icon = BitmapDescriptorFactory.fromResource(R.drawable.ic_map_sos_pin); } else { icon = BitmapDescriptorFactory.fromResource(R.drawable.ic_map_company_pin); } } markerOptions.icon(icon); markerOptions.anchor(0.5f, 1.0f); } } @SuppressLint("NewApi") private Bitmap getClusterBitmap(Resources res, int resourceId, int clusterSize, int color) { BitmapFactory.Options options = new BitmapFactory.Options(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { options.inMutable = true; } Bitmap bitmap = BitmapFactory.decodeResource(res, resourceId, options); if (!bitmap.isMutable()) { bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true); } Canvas canvas = new Canvas(bitmap); Paint paint = null; float originY; if (clusterSize < 100) { paint = clusterPaintLarge; originY = bitmap.getHeight() * 0.64f; } else if (clusterSize < 1000) { paint = clusterPaintMedium; originY = bitmap.getHeight() * 0.6f; } else { paint = clusterPaintSmall; originY = bitmap.getHeight() * 0.56f; } paint.setColor(color); canvas.drawText(String.valueOf(clusterSize), bitmap.getWidth() * 0.5f, originY, paint); return bitmap; } }
Still we couldn’t avoid some problems. When using the library the event onCameraChangePosition
ceased to work. As a solution, we used View.OnTouchListener
with some conditions.
And finally, here is a tip on standard info-windows («pop-ups» over markers). If you want to display any process in them or assign certain events to their individual elements, you can use pop-up for this, as the standard info window displays «not alive» View
(i.e. like screenshot, that has no animation within the window)and processes only click on its entire surface.