Back to project page google-maps-blog-android-sample-code.
The source code is released under:
MIT License
If you think the Android project google-maps-blog-android-sample-code listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * The MIT License (MIT)//w w w. ja v a 2 s . co m * * Copyright (c) 2013 OpenTable, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.example.mapsv2; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import android.annotation.SuppressLint; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.ArrayAdapter; import android.widget.FrameLayout; import android.widget.ListView; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener; import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener; import com.google.android.gms.maps.Projection; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; /* * Exercises for the reader * * 1. Make the info window appear only after the map and marker have moved * into place. Appearing beforehand is confusing and awkward. And maybe * fade it in while you're at it. * 2. Limit the width of the info window in landscape mode so that it's not * so overwhelmingly long. */ public class ClustersAreListsActivity extends FragmentActivity implements OnMarkerClickListener, OnCameraChangeListener { private GoogleMap map; private List<Marker> markers; private List<Cluster> clusters; private int clusterToleranceDIP = 8; private float mapZoom; // For Part 2 private ListView listView; private View fullScreenOverlay, infoWindow; private int markerSize, defaultMargin; private CameraPosition previousCameraPosition; // For Part 3 private GradientDrawable spotDrawable; private SpotlightAnimation spotAnimation; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_clustersarelists); setUpMapIfNeeded(); setupPart2(); setupPart3(); } @Override protected void onResume() { super.onResume(); setUpMapIfNeeded(); } private void setUpMapIfNeeded() { // Do a null check to confirm that we have not already instantiated the map. if (map == null) { // Try to obtain the map from the SupportMapFragment. map = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)) .getMap(); // Check if we were successful in obtaining the map. if (map != null) { setUpMap(); } } } private void setUpMap() { // Create some map markers to work with createMarkerDataSet(); // Set listeners for marker events. See the bottom of this class for their behavior map.setOnMarkerClickListener(this); // Listen for camera changes so we can detect zoom level change map.setOnCameraChangeListener(this); // Pan to see all markers in view. // Cannot zoom to bounds until the map has a size. final View mapView = getSupportFragmentManager().findFragmentById(R.id.map).getView(); if (mapView.getViewTreeObserver().isAlive()) { mapView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @SuppressWarnings("deprecation") // We use the new method when supported @SuppressLint("NewApi") // We check which build version we are using. @Override public void onGlobalLayout() { LatLngBounds.Builder bld = new LatLngBounds.Builder(); for (Marker m : markers) bld.include(m.getPosition()); LatLngBounds bounds = bld.build(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) mapView.getViewTreeObserver().removeGlobalOnLayoutListener(this); else mapView.getViewTreeObserver().removeOnGlobalLayoutListener(this); // Move the camera so that all markers are visible // NOTE: This camera change will trigger a zoom level change which // will cause a re-evaluation of the clusters map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50)); } }); } } private void createMarkerDataSet() { // Creates a marker rainbow demonstrating how to create default marker icons of different // hues (colors). int numMarkersInRainbow = 12; markers = new ArrayList<Marker>(numMarkersInRainbow); for (int i = 0; i < numMarkersInRainbow; i++) { Marker m = map.addMarker(new MarkerOptions() .position(new LatLng( -23.7003593445 + 0.1 * Math.sin(i * Math.PI / (numMarkersInRainbow - 1)), 133.8808898926 - 0.1 * Math.cos(i * Math.PI / (numMarkersInRainbow - 1)))) .title("Marker " + i) .icon(BitmapDescriptorFactory.defaultMarker(i * 360 / numMarkersInRainbow))); markers.add(m); } } /* ----------------------------------------------------------------------- * Part 1: Clustering */ private void computeClusters() { if (clusters == null) clusters = new LinkedList<Cluster>(); else clusters.clear(); Projection proj = map.getProjection(); for (Marker m : markers) { // Project lat/lng into screen space, given map's current zoom, etc. Point p = proj.toScreenLocation(m.getPosition()); // Find the first cluster near point p Cluster cluster = null; for (Cluster c : clusters) { if (c.contains(p)) { cluster = c; break; } } // Create a new Cluster if there were none nearby if (cluster == null) { cluster = new Cluster(p); clusters.add(cluster); } cluster.add(m); } } private void addClustersToMap() { // Remove any Markers already on the map map.clear(); // Put a marker on the map for each Cluster, with appropriate icon for (Cluster c : clusters) { Marker m = c.get(0); int resId = (c.size() > 1 ? R.drawable.marker_multi : R.drawable.marker_single); Marker mapMarker = map.addMarker(new MarkerOptions() .position(m.getPosition()) .title(m.getTitle()) .icon(BitmapDescriptorFactory.fromResource(resId))); // FIXME: This is confusing, I think - the difference between Markers in the cluster // and Markers actually rendered on the map... c.mapMarkerId = mapMarker.getId(); } } class Cluster { Rect bounds; List<Marker> markers = new LinkedList<Marker>(); String mapMarkerId; public Cluster(Point p) { // Delta should be in DIP units, so scale by screen density! int delta = getClusterTolerance(); bounds = new Rect(p.x - delta, p.y - delta, p.x + delta, p.y + delta); } public boolean contains(Point p) { return bounds.contains(p.x, p.y); } public void add(Marker m) { markers.add(m); } public int size() { return markers.size(); } public Marker get(int i) { return markers.get(i); } } private int getClusterTolerance() { // Compute the maximum distance (in screen space pixels) between two // items on the map that will be displayed with one combined icon final float f = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, clusterToleranceDIP, getResources().getDisplayMetrics()); return (int) (f + 0.5); // round up } /** * Re-evaluate the clustering of map markers if the zoom level has * changed enough. */ @Override public void onCameraChange(CameraPosition position) { float zoom = position.zoom; if (Math.abs(zoom - mapZoom) > 0.6f) { mapZoom = zoom; // Re-compute the clustering given the current projection computeClusters(); // Add the new clusters to the map addClustersToMap(); } } /* * -- end Part 1 */ /* ----------------------------------------------------------------------- * Part 2: The nougaty center * Clusters are lists * */ private void setupPart2() { fullScreenOverlay = findViewById(R.id.fullscreen_overlay); // Set the full screen overlay to be invisible but not GONE initially // because otherwise it's won't get width/height set in a layout pass fullScreenOverlay.setVisibility(View.INVISIBLE); fullScreenOverlay.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { hideInfoWindow(); } }); listView = (ListView) findViewById(android.R.id.list); listView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new LinkedList<String>())); // infowindow is the parent of the ListView and we'll position it to // appear to "point at" the selected map marker infoWindow = findViewById(R.id.info_window); // All our markers are the same width & height and are scaled exactly // for each density. YMMV. markerSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32, getResources().getDisplayMetrics()); defaultMargin = getResources().getDimensionPixelOffset(R.dimen.activity_horizontal_margin); } @Override public boolean onMarkerClick(final Marker marker) { // Find the cluster containing the Marker Cluster cluster = null; for (Cluster c : clusters) { if (marker.getId().equals(c.mapMarkerId)) { cluster = c; break; } } showInfoWindow(cluster); // Consume the event, don't trigger the default behavior return true; } private void showInfoWindow(Cluster cluster) { Marker m = cluster.get(0); // Save the current camera so we can restore it later previousCameraPosition = map.getCameraPosition(); // Get the screen-space position of the Cluster int yoffset = markerSize / 2; Projection proj = map.getProjection(); Point p = proj.toScreenLocation(m.getPosition()); int markerX = p.x; int markerY = p.y - yoffset; // Get the position where we'll ultimately show the marker & spotlight final Point newMarkerPos = getZoneCenter(m); // Animate the spotlight from the current marker position to its final position showSpotlight(markerX, markerY, newMarkerPos.x, newMarkerPos.y); // Position the selected marker in the top, right, bottom, or left zone final CameraUpdate camUpdate = calculateCameraChange(newMarkerPos.x, newMarkerPos.y, markerX, markerY); map.animateCamera(camUpdate, 400, null); // Show the popup list next to the marker showInfo(newMarkerPos.x + markerSize/2, newMarkerPos.y + yoffset, cluster); } private Point getZoneCenter(final Marker marker) { final int orientation = getResources().getConfiguration().orientation; final int w = fullScreenOverlay.getWidth(); // this is full screen width final int h = fullScreenOverlay.getHeight(); // this is full screen height final int cx, cy; if (orientation == Configuration.ORIENTATION_LANDSCAPE) { // Left Zone: Leftmost 1/4 of screen cx = (w / 4) / 2; cy = h / 2; } else { // Top Zone: Upper 1/4 of screen cx = w / 2; cy = (h / 4) / 2; } return new Point(cx, cy); } private CameraUpdate calculateCameraChange(final int newX, final int newY, final int oldX, final int oldY) { // WARNING: This is broken when the map is tilted! Projection proj = map.getProjection(); Point cameraPos = proj.toScreenLocation(map.getCameraPosition().target); int dx = newX - oldX; int dy = newY - oldY; cameraPos.x -= dx; cameraPos.y -= dy; return CameraUpdateFactory.newLatLng(proj.fromScreenLocation(cameraPos)); } private void showInfo(final int x, int y, final Cluster cluster) { final int orientation = getResources().getConfiguration().orientation; // (re-)Load cluster data into the ListView @SuppressWarnings("unchecked") ArrayAdapter<String> adapter = (ArrayAdapter<String>) listView.getAdapter(); adapter.clear(); for (Marker m : cluster.markers) { adapter.add(m.getTitle()); } adapter.notifyDataSetChanged(); // Reconfigure the layout params to position the info window on screen FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) infoWindow.getLayoutParams(); if (orientation == Configuration.ORIENTATION_PORTRAIT) { lp.topMargin = y; lp.leftMargin = defaultMargin; lp.rightMargin = defaultMargin; lp.width = LayoutParams.MATCH_PARENT; lp.height = LayoutParams.WRAP_CONTENT; lp.gravity = Gravity.LEFT | Gravity.TOP; infoWindow.setBackgroundResource(R.drawable.info_window_bg_up); } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { lp.leftMargin = x + defaultMargin; lp.topMargin = defaultMargin; lp.bottomMargin = defaultMargin; lp.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; lp.width = LayoutParams.WRAP_CONTENT; lp.height = LayoutParams.WRAP_CONTENT; infoWindow.setBackgroundResource(R.drawable.info_window_bg_left); } infoWindow.setLayoutParams(lp); fullScreenOverlay.setVisibility(View.VISIBLE); } private void hideInfoWindow() { if (previousCameraPosition != null) map.animateCamera(CameraUpdateFactory.newCameraPosition(previousCameraPosition)); previousCameraPosition = null; // Hide the info window (a fade out would be nicer...) fullScreenOverlay.setVisibility(View.GONE); } /* * -- end Part 2 */ /* ----------------------------------------------------------------------- * Part 3: Turn the lights down low * A spotlight effect for drawing focus to the content. * */ private void setupPart3() { fullScreenOverlay.setBackgroundResource(R.drawable.spotlight_gradient); spotDrawable = (GradientDrawable) fullScreenOverlay.getBackground(); spotAnimation = new SpotlightAnimation(0, 0, 0, 0); spotAnimation.setDuration(400); } private void showSpotlight(int startX, int startY, int endX, int endY) { // Show the spotlight first at the start position final float cx = startX / (float) fullScreenOverlay.getWidth(); final float cy = startY / (float) fullScreenOverlay.getHeight(); setSpotlight(cx, cy); // Configure and start the animation spotAnimation.set(startX, startY, endX, endY); fullScreenOverlay.startAnimation(spotAnimation); } private void setSpotlight(float cx, float cy) { spotDrawable.setGradientCenter(cx, cy); // The following line is needed for Android 2.2 to flag it's state // as dirty and pick up the new gradient center set above. spotDrawable.setGradientType(GradientDrawable.RADIAL_GRADIENT); } public class SpotlightAnimation extends Animation { float cx0, cy0, dx, dy; public SpotlightAnimation(final int startX, final int startY, final int endX, final int endY) { set(startX, startY, endX, endY); } public void set(final int startX, final int startY, final int endX, final int endY) { final float w = fullScreenOverlay.getWidth(); final float h = fullScreenOverlay.getHeight(); cx0 = (float) startX / w; cy0 = (float) startY / h; dx = (endX / w) - cx0; dy = (endY / h) - cy0; } @Override protected void applyTransformation(final float interpTime, final Transformation t) { final float cx = cx0 + interpTime * dx; final float cy = cy0 + interpTime * dy; setSpotlight(cx, cy); } @Override public boolean willChangeTransformationMatrix() { return false; } @Override public boolean willChangeBounds() { return false; } } /* * -- end Part 3 */ }