1+ /*
2+ * Copyright 2013 Google Inc.
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+
17+ package com .google .maps .android .utils .demo ;
18+
19+ import android .graphics .Bitmap ;
20+ import android .graphics .drawable .Drawable ;
21+ import android .os .Handler ;
22+ import android .util .Log ;
23+ import android .view .View ;
24+ import android .view .ViewGroup ;
25+ import android .widget .ImageView ;
26+ import android .widget .Toast ;
27+
28+ import androidx .annotation .NonNull ;
29+
30+ import com .google .android .gms .maps .CameraUpdateFactory ;
31+ import com .google .android .gms .maps .GoogleMap ;
32+ import com .google .android .gms .maps .model .BitmapDescriptor ;
33+ import com .google .android .gms .maps .model .BitmapDescriptorFactory ;
34+ import com .google .android .gms .maps .model .LatLng ;
35+ import com .google .android .gms .maps .model .LatLngBounds ;
36+ import com .google .android .gms .maps .model .Marker ;
37+ import com .google .android .gms .maps .model .MarkerOptions ;
38+ import com .google .maps .android .clustering .Cluster ;
39+ import com .google .maps .android .clustering .ClusterItem ;
40+ import com .google .maps .android .clustering .ClusterManager ;
41+ import com .google .maps .android .clustering .view .ClusterRendererMultipleItems ;
42+ import com .google .maps .android .ui .IconGenerator ;
43+ import com .google .maps .android .utils .demo .model .Person ;
44+
45+ import java .util .ArrayList ;
46+ import java .util .Collections ;
47+ import java .util .List ;
48+ import java .util .Random ;
49+
50+ /**
51+ * Demonstrates how to apply a diff to the current Cluster
52+ */
53+ public class ClusteringDiffDemoActivity extends BaseDemoActivity implements ClusterManager .OnClusterClickListener <Person >, ClusterManager .OnClusterInfoWindowClickListener <Person >, ClusterManager .OnClusterItemClickListener <Person >, ClusterManager .OnClusterItemInfoWindowClickListener <Person > {
54+ private ClusterManager <Person > mClusterManager ;
55+ private final Random mRandom = new Random (1984 );
56+ private Person itemtoUpdate = new Person (position (), "Teach" , R .drawable .teacher );
57+
58+ private final Random random = new Random ();
59+ private final Handler handler = new Handler ();
60+
61+ @ Override
62+
63+ public void onMapReady (@ NonNull GoogleMap map ) {
64+ super .onMapReady (map );
65+ startRandomCalls ();
66+ }
67+
68+
69+ /**
70+ * Draws profile photos inside markers (using IconGenerator).
71+ * When there are multiple people in the cluster, draw multiple photos (using MultiDrawable).
72+ */
73+ private class PersonRenderer extends ClusterRendererMultipleItems <Person > {
74+ private final IconGenerator mIconGenerator = new IconGenerator (getApplicationContext ());
75+ private final IconGenerator mClusterIconGenerator = new IconGenerator (getApplicationContext ());
76+ private final ImageView mImageView ;
77+ private final ImageView mClusterImageView ;
78+ private final int mDimension ;
79+
80+ public PersonRenderer () {
81+ super (getApplicationContext (), getMap (), mClusterManager );
82+
83+ View multiProfile = getLayoutInflater ().inflate (R .layout .multi_profile , null );
84+ mClusterIconGenerator .setContentView (multiProfile );
85+ mClusterImageView = multiProfile .findViewById (R .id .image );
86+
87+ mImageView = new ImageView (getApplicationContext ());
88+ mDimension = (int ) getResources ().getDimension (R .dimen .custom_profile_image );
89+ mImageView .setLayoutParams (new ViewGroup .LayoutParams (mDimension , mDimension ));
90+ int padding = (int ) getResources ().getDimension (R .dimen .custom_profile_padding );
91+ mImageView .setPadding (padding , padding , padding , padding );
92+ mIconGenerator .setContentView (mImageView );
93+ }
94+
95+ public void setUpdateMarker (Person person ) {
96+ Marker marker = getMarker (person );
97+ if (marker != null ) {
98+ marker .setIcon (getItemIcon (person ));
99+ }
100+ }
101+
102+ @ Override
103+ protected void onBeforeClusterItemRendered (@ NonNull Person person , @ NonNull MarkerOptions markerOptions ) {
104+ // Draw a single person - show their profile photo and set the info window to show their name
105+ markerOptions
106+ .icon (getItemIcon (person ))
107+ .title (person .name );
108+ }
109+
110+ @ Override
111+ protected void onClusterItemUpdated (@ NonNull Person person , @ NonNull Marker marker ) {
112+ // Same implementation as onBeforeClusterItemRendered() (to update cached markers)
113+ marker .setIcon (getItemIcon (person ));
114+ marker .setTitle (person .name );
115+ }
116+
117+ /**
118+ * Get a descriptor for a single person (i.e., a marker outside a cluster) from their
119+ * profile photo to be used for a marker icon
120+ *
121+ * @param person person to return an BitmapDescriptor for
122+ * @return the person's profile photo as a BitmapDescriptor
123+ */
124+ private BitmapDescriptor getItemIcon (Person person ) {
125+ mImageView .setImageResource (person .profilePhoto );
126+ Bitmap icon = mIconGenerator .makeIcon ();
127+ return BitmapDescriptorFactory .fromBitmap (icon );
128+ }
129+
130+ @ Override
131+ protected void onBeforeClusterRendered (@ NonNull Cluster <Person > cluster , @ NonNull MarkerOptions markerOptions ) {
132+ // Draw multiple people.
133+ // Note: this method runs on the UI thread. Don't spend too much time in here (like in this example).
134+ markerOptions .icon (getClusterIcon (cluster ));
135+ }
136+
137+ @ Override
138+ protected void onClusterUpdated (@ NonNull Cluster <Person > cluster , Marker marker ) {
139+ // Same implementation as onBeforeClusterRendered() (to update cached markers)
140+ marker .setIcon (getClusterIcon (cluster ));
141+ }
142+
143+ /**
144+ * Get a descriptor for multiple people (a cluster) to be used for a marker icon. Note: this
145+ * method runs on the UI thread. Don't spend too much time in here (like in this example).
146+ *
147+ * @param cluster cluster to draw a BitmapDescriptor for
148+ * @return a BitmapDescriptor representing a cluster
149+ */
150+ private BitmapDescriptor getClusterIcon (Cluster <Person > cluster ) {
151+ List <Drawable > profilePhotos = new ArrayList <>(Math .min (4 , cluster .getSize ()));
152+ int width = mDimension ;
153+ int height = mDimension ;
154+
155+ for (Person p : cluster .getItems ()) {
156+ // Draw 4 at most.
157+ if (profilePhotos .size () == 4 ) break ;
158+ Drawable drawable = getResources ().getDrawable (p .profilePhoto );
159+ drawable .setBounds (0 , 0 , width , height );
160+ profilePhotos .add (drawable );
161+ }
162+ MultiDrawable multiDrawable = new MultiDrawable (profilePhotos );
163+ multiDrawable .setBounds (0 , 0 , width , height );
164+
165+ mClusterImageView .setImageDrawable (multiDrawable );
166+ Bitmap icon = mClusterIconGenerator .makeIcon (String .valueOf (cluster .getSize ()));
167+ return BitmapDescriptorFactory .fromBitmap (icon );
168+ }
169+
170+ @ Override
171+ protected boolean shouldRenderAsCluster (@ NonNull Cluster <Person > cluster ) {
172+ // Always render clusters.
173+ return cluster .getSize () >= 1 ;
174+ }
175+ }
176+
177+ private void startRandomCalls () {
178+ // Initial call to the random update.
179+ callUpdateRandom ();
180+ }
181+
182+ private void callUpdateRandom () {
183+ // Generate a random delay between 1 and 5 seconds
184+ int delay = random .nextInt (5000 ) + 1000 ; // Random delay in milliseconds (1000ms to 5000ms)
185+
186+ // Post the next call with the random delay
187+ handler .postDelayed (this ::callUpdateRandom , delay );
188+ updateRandom ();
189+ }
190+
191+ @ Override
192+ public boolean onClusterClick (Cluster <Person > cluster ) {
193+ // Show a toast with some info when the cluster is clicked.
194+ String firstName = cluster .getItems ().iterator ().next ().name ;
195+ Toast .makeText (this , cluster .getSize () + " (including " + firstName + ")" , Toast .LENGTH_SHORT ).show ();
196+
197+ // Zoom in the cluster. Need to create LatLngBounds and including all the cluster items
198+ // inside of bounds, then animate to center of the bounds.
199+
200+ // Create the builder to collect all essential cluster items for the bounds.
201+ LatLngBounds .Builder builder = LatLngBounds .builder ();
202+ for (ClusterItem item : cluster .getItems ()) {
203+ builder .include (item .getPosition ());
204+ }
205+ // Get the LatLngBounds
206+ final LatLngBounds bounds = builder .build ();
207+
208+ // Animate camera to the bounds
209+ try {
210+ getMap ().animateCamera (CameraUpdateFactory .newLatLngBounds (bounds , 100 ));
211+ } catch (Exception e ) {
212+ e .printStackTrace ();
213+ }
214+
215+ return true ;
216+ }
217+
218+ @ Override
219+ public void onClusterInfoWindowClick (Cluster <Person > cluster ) {
220+ // Does nothing, but you could go to a list of the users.
221+ }
222+
223+ @ Override
224+ public boolean onClusterItemClick (Person item ) {
225+ // Does nothing, but you could go into the user's profile page, for example.
226+ return false ;
227+ }
228+
229+ @ Override
230+ public void onClusterItemInfoWindowClick (Person item ) {
231+ // Does nothing, but you could go into the user's profile page, for example.
232+ }
233+
234+ @ Override
235+ protected void startDemo (boolean isRestore ) {
236+ if (!isRestore ) {
237+ getMap ().moveCamera (CameraUpdateFactory .newLatLngZoom (new LatLng (51.503186 , -0.126446 ), 6 ));
238+ }
239+
240+ mClusterManager = new ClusterManager <>(this , getMap ());
241+ mClusterManager .setRenderer (new PersonRenderer ());
242+ getMap ().setOnCameraIdleListener (mClusterManager );
243+ getMap ().setOnMarkerClickListener (mClusterManager );
244+ getMap ().setOnInfoWindowClickListener (mClusterManager );
245+ mClusterManager .setOnClusterClickListener (this );
246+ mClusterManager .setOnClusterInfoWindowClickListener (this );
247+ mClusterManager .setOnClusterItemClickListener (this );
248+ mClusterManager .setOnClusterItemInfoWindowClickListener (this );
249+
250+ addItems ();
251+ mClusterManager .cluster ();
252+ }
253+
254+ private void addItems () {
255+
256+ // http://www.flickr.com/photos/sdasmarchives/5036231225/
257+ mClusterManager .addItem (new Person (position (), "John" , R .drawable .john ));
258+
259+
260+ // http://www.flickr.com/photos/usnationalarchives/4726892651/
261+ itemtoUpdate = new Person (position (), "Teach" , R .drawable .teacher );
262+ mClusterManager .addItem (itemtoUpdate );
263+ }
264+
265+
266+ private void updateRandom () {
267+ itemtoUpdate = new Person (position (), "Teach" , R .drawable .teacher );
268+ Log .d ("ClusterTest" , "We start updating the item. New position: " + itemtoUpdate .getPosition ().toString ());
269+
270+ mClusterManager .updateItem (this .itemtoUpdate );
271+
272+ //We could also call the diff() method to add, remove and update at once.
273+ mClusterManager .diff (null , null , new ArrayList <>(Collections .singleton (this .itemtoUpdate )));
274+ mClusterManager .setAnimation (true );
275+
276+ // Cluster needs to be called, to force an update of the cluster.
277+ mClusterManager .cluster ();
278+ }
279+
280+ private LatLng position () {
281+ return new LatLng (random (51.6723432 , 51.38494009999999 ), random (0.148271 , -0.3514683 ));
282+ }
283+
284+ private double random (double min , double max ) {
285+ return mRandom .nextDouble () * (max - min ) + min ;
286+ }
287+ }
0 commit comments