Skip to content

Commit c1edbaf

Browse files
authored
feat: implementation of diff (#1438)
* feat: implementation of diff * feat: headers * feat: headers * feat: removed comments * feat: commented diff in sample * feat: removed render condition * feat: removed render condition * feat: handling individual markers * feat: removed comment * feat: header * feat: canceling animation if it is updated * fix: animation
1 parent f543241 commit c1edbaf

File tree

11 files changed

+1600
-16
lines changed

11 files changed

+1600
-16
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ project.properties
1111
.DS_Store
1212
.java-version
1313
secrets.properties
14-
.kotlin
14+
.kotlin

demo/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ dependencies {
6464
implementation(libs.kotlin.stdlib.jdk8)
6565
implementation(libs.kotlinx.coroutines.android)
6666
implementation(libs.kotlinx.coroutines.core)
67+
implementation(libs.material)
6768

6869
testImplementation(libs.junit)
6970
testImplementation(libs.truth)

demo/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@
9191
<activity
9292
android:name=".CustomAdvancedMarkerClusteringDemoActivity"
9393
android:exported="true" />
94+
<activity
95+
android:name=".ClusteringDiffDemoActivity"
96+
android:exported="true" />
9497
<activity
9598
android:name=".ZoomClusteringDemoActivity"
9699
android:exported="true" />
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*
2+
* Copyright 2025 Google LLC
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.annotation.SuppressLint;
20+
import android.graphics.Bitmap;
21+
import android.graphics.drawable.Drawable;
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+
import androidx.core.content.res.ResourcesCompat;
30+
31+
import com.google.android.gms.maps.CameraUpdateFactory;
32+
import com.google.android.gms.maps.GoogleMap;
33+
import com.google.android.gms.maps.model.BitmapDescriptor;
34+
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
35+
import com.google.android.gms.maps.model.LatLng;
36+
import com.google.android.gms.maps.model.LatLngBounds;
37+
import com.google.android.gms.maps.model.Marker;
38+
import com.google.android.gms.maps.model.MarkerOptions;
39+
import com.google.maps.android.clustering.Cluster;
40+
import com.google.maps.android.clustering.ClusterItem;
41+
import com.google.maps.android.clustering.ClusterManager;
42+
import com.google.maps.android.clustering.view.ClusterRendererMultipleItems;
43+
import com.google.maps.android.ui.IconGenerator;
44+
import com.google.maps.android.utils.demo.model.Person;
45+
46+
import java.util.ArrayList;
47+
import java.util.List;
48+
49+
/**
50+
* Demonstrates how to apply a diff to the current Cluster
51+
*/
52+
public class ClusteringDiffDemoActivity extends BaseDemoActivity implements ClusterManager.OnClusterClickListener<Person>, ClusterManager.OnClusterInfoWindowClickListener<Person>, ClusterManager.OnClusterItemClickListener<Person>, ClusterManager.OnClusterItemInfoWindowClickListener<Person> {
53+
private ClusterManager<Person> mClusterManager;
54+
private Person itemtoUpdate = new Person(ENFIELD, "Teach", R.drawable.teacher);
55+
56+
private static final LatLng ENFIELD = new LatLng(51.6524, -0.0838);
57+
private static final LatLng ILFORD = new LatLng(51.5590, -0.0815);
58+
59+
private static final LatLng LONDON = new LatLng(51.5074, -0.1278);
60+
LatLng midpoint = getMidpoint();
61+
private int currentLocationIndex = 0;
62+
63+
protected int getLayoutId() {
64+
return R.layout.map_with_floating_button;
65+
}
66+
67+
@Override
68+
public void onMapReady(@NonNull GoogleMap map) {
69+
super.onMapReady(map);
70+
findViewById(R.id.fab_rotate_location).setOnClickListener(v -> rotateLocation());
71+
getMap().animateCamera(CameraUpdateFactory.newLatLngZoom(midpoint, 12));
72+
}
73+
74+
75+
private LatLng getMidpoint() {
76+
double latitude = (ClusteringDiffDemoActivity.ENFIELD.latitude + ClusteringDiffDemoActivity.ILFORD.latitude + ClusteringDiffDemoActivity.LONDON.latitude) / 3;
77+
double longitude = (ClusteringDiffDemoActivity.ENFIELD.longitude + ClusteringDiffDemoActivity.ILFORD.longitude + ClusteringDiffDemoActivity.LONDON.longitude) / 3;
78+
return new LatLng(latitude, longitude);
79+
}
80+
81+
/**
82+
* Draws profile photos inside markers (using IconGenerator).
83+
* When there are multiple people in the cluster, draw multiple photos (using MultiDrawable).
84+
*/
85+
@SuppressLint("InflateParams")
86+
private class PersonRenderer extends ClusterRendererMultipleItems<Person> {
87+
private final IconGenerator mIconGenerator = new IconGenerator(getApplicationContext());
88+
private final IconGenerator mClusterIconGenerator = new IconGenerator(getApplicationContext());
89+
private final ImageView mImageView;
90+
private final ImageView mClusterImageView;
91+
private final int mDimension;
92+
93+
public PersonRenderer() {
94+
super(getApplicationContext(), getMap(), mClusterManager);
95+
96+
View multiProfile = getLayoutInflater().inflate(R.layout.multi_profile, null);
97+
mClusterIconGenerator.setContentView(multiProfile);
98+
mClusterImageView = multiProfile.findViewById(R.id.image);
99+
100+
mImageView = new ImageView(getApplicationContext());
101+
mDimension = (int) getResources().getDimension(R.dimen.custom_profile_image);
102+
mImageView.setLayoutParams(new ViewGroup.LayoutParams(mDimension, mDimension));
103+
int padding = (int) getResources().getDimension(R.dimen.custom_profile_padding);
104+
mImageView.setPadding(padding, padding, padding, padding);
105+
mIconGenerator.setContentView(mImageView);
106+
}
107+
108+
@Override
109+
protected void onBeforeClusterItemRendered(@NonNull Person person, @NonNull MarkerOptions markerOptions) {
110+
// Draw a single person - show their profile photo and set the info window to show their name
111+
markerOptions
112+
.icon(getItemIcon(person))
113+
.title(person.name);
114+
}
115+
116+
@Override
117+
protected void onClusterItemUpdated(@NonNull Person person, @NonNull Marker marker) {
118+
// Same implementation as onBeforeClusterItemRendered() (to update cached markers)
119+
marker.setIcon(getItemIcon(person));
120+
marker.setTitle(person.name);
121+
}
122+
123+
/**
124+
* Get a descriptor for a single person (i.e., a marker outside a cluster) from their
125+
* profile photo to be used for a marker icon
126+
*
127+
* @param person person to return an BitmapDescriptor for
128+
* @return the person's profile photo as a BitmapDescriptor
129+
*/
130+
private BitmapDescriptor getItemIcon(Person person) {
131+
mImageView.setImageResource(person.profilePhoto);
132+
Bitmap icon = mIconGenerator.makeIcon();
133+
return BitmapDescriptorFactory.fromBitmap(icon);
134+
}
135+
136+
@Override
137+
protected void onBeforeClusterRendered(@NonNull Cluster<Person> cluster, @NonNull MarkerOptions markerOptions) {
138+
// Draw multiple people.
139+
// Note: this method runs on the UI thread. Don't spend too much time in here (like in this example).
140+
markerOptions.icon(getClusterIcon(cluster));
141+
}
142+
143+
@Override
144+
protected void onClusterUpdated(@NonNull Cluster<Person> cluster, @NonNull Marker marker) {
145+
// Same implementation as onBeforeClusterRendered() (to update cached markers)
146+
marker.setIcon(getClusterIcon(cluster));
147+
}
148+
149+
/**
150+
* Get a descriptor for multiple people (a cluster) to be used for a marker icon. Note: this
151+
* method runs on the UI thread. Don't spend too much time in here (like in this example).
152+
*
153+
* @param cluster cluster to draw a BitmapDescriptor for
154+
* @return a BitmapDescriptor representing a cluster
155+
*/
156+
private BitmapDescriptor getClusterIcon(Cluster<Person> cluster) {
157+
List<Drawable> profilePhotos = new ArrayList<>(Math.min(4, cluster.getSize()));
158+
int width = mDimension;
159+
int height = mDimension;
160+
161+
for (Person p : cluster.getItems()) {
162+
// Draw 4 at most.
163+
if (profilePhotos.size() == 4) break;
164+
Drawable drawable = ResourcesCompat.getDrawable(getBaseContext().getResources(), p.profilePhoto, null);
165+
if (drawable != null) {
166+
drawable.setBounds(0, 0, width, height);
167+
}
168+
profilePhotos.add(drawable);
169+
}
170+
MultiDrawable multiDrawable = new MultiDrawable(profilePhotos);
171+
multiDrawable.setBounds(0, 0, width, height);
172+
173+
mClusterImageView.setImageDrawable(multiDrawable);
174+
Bitmap icon = mClusterIconGenerator.makeIcon(String.valueOf(cluster.getSize()));
175+
return BitmapDescriptorFactory.fromBitmap(icon);
176+
}
177+
178+
@Override
179+
protected boolean shouldRenderAsCluster(@NonNull Cluster<Person> cluster) {
180+
return cluster.getSize() >= 2;
181+
}
182+
}
183+
184+
185+
@Override
186+
public boolean onClusterClick(Cluster<Person> cluster) {
187+
// Show a toast with some info when the cluster is clicked.
188+
String firstName = cluster.getItems().iterator().next().name;
189+
Toast.makeText(this, cluster.getSize() + " (including " + firstName + ")", Toast.LENGTH_SHORT).show();
190+
191+
// Zoom in the cluster. Need to create LatLngBounds and including all the cluster items
192+
// inside of bounds, then animate to center of the bounds.
193+
194+
// Create the builder to collect all essential cluster items for the bounds.
195+
LatLngBounds.Builder builder = LatLngBounds.builder();
196+
for (ClusterItem item : cluster.getItems()) {
197+
builder.include(item.getPosition());
198+
}
199+
// Get the LatLngBounds
200+
final LatLngBounds bounds = builder.build();
201+
202+
// Animate camera to the bounds
203+
try {
204+
getMap().animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100));
205+
} catch (Exception e) {
206+
e.printStackTrace();
207+
}
208+
209+
return true;
210+
}
211+
212+
@Override
213+
public void onClusterInfoWindowClick(Cluster<Person> cluster) {
214+
// Does nothing, but you could go to a list of the users.
215+
}
216+
217+
@Override
218+
public boolean onClusterItemClick(Person item) {
219+
// Does nothing, but you could go into the user's profile page, for example.
220+
return false;
221+
}
222+
223+
@Override
224+
public void onClusterItemInfoWindowClick(Person item) {
225+
// Does nothing, but you could go into the user's profile page, for example.
226+
}
227+
228+
@Override
229+
protected void startDemo(boolean isRestore) {
230+
if (!isRestore) {
231+
getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(51.503186, -0.126446), 6));
232+
}
233+
234+
mClusterManager = new ClusterManager<>(this, getMap());
235+
mClusterManager.setRenderer(new PersonRenderer());
236+
getMap().setOnCameraIdleListener(mClusterManager);
237+
getMap().setOnMarkerClickListener(mClusterManager);
238+
getMap().setOnInfoWindowClickListener(mClusterManager);
239+
mClusterManager.setOnClusterClickListener(this);
240+
mClusterManager.setOnClusterInfoWindowClickListener(this);
241+
mClusterManager.setOnClusterItemClickListener(this);
242+
mClusterManager.setOnClusterItemInfoWindowClickListener(this);
243+
244+
addItems();
245+
mClusterManager.cluster();
246+
}
247+
248+
private void addItems() {
249+
// Marker in Enfield
250+
mClusterManager.addItem(new Person(ENFIELD, "John", R.drawable.john));
251+
252+
// Marker in the center of London
253+
itemtoUpdate = new Person(LONDON, "Teach", R.drawable.teacher);
254+
mClusterManager.addItem(itemtoUpdate);
255+
}
256+
257+
private void rotateLocation() {
258+
// Update the current index to cycle through locations (0 = Enfield, 1 = Olford, 2 = London)
259+
currentLocationIndex = (currentLocationIndex + 1) % 3;
260+
261+
262+
LatLng newLocation = switch (currentLocationIndex) {
263+
case 0 -> ENFIELD;
264+
case 1 -> ILFORD;
265+
default -> LONDON;
266+
};
267+
268+
String cityName = getCityName(newLocation);
269+
270+
Log.d("ClusterTest", "Item rotated to: " + newLocation.toString() + ", City: " + cityName);
271+
272+
if (itemtoUpdate != null) {
273+
itemtoUpdate = new Person(newLocation, "Teach", R.drawable.teacher);
274+
mClusterManager.updateItem(itemtoUpdate); // Update the marker
275+
mClusterManager.cluster();
276+
}
277+
}
278+
279+
// Method to map LatLng to city name
280+
private String getCityName(LatLng location) {
281+
if (areLocationsEqual(location, ENFIELD)) {
282+
return "Enfield";
283+
} else if (areLocationsEqual(location, ILFORD)) {
284+
return "Ilford";
285+
} else if (areLocationsEqual(location, LONDON)) {
286+
return "London";
287+
} else {
288+
return "Unknown City"; // Default case if location is not recognized
289+
}
290+
}
291+
292+
// Method to compare LatLng objects with a tolerance
293+
private boolean areLocationsEqual(LatLng loc1, LatLng loc2) {
294+
return Math.abs(loc1.latitude - loc2.latitude) < 1E-5 &&
295+
Math.abs(loc1.longitude - loc2.longitude) < 1E-5;
296+
}
297+
}

demo/src/main/java/com/google/maps/android/utils/demo/MainActivity.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ protected void onCreate(Bundle savedInstanceState) {
4747
addDemo("Clustering", ClusteringDemoActivity.class);
4848
addDemo("Advanced Markers Clustering Example", CustomAdvancedMarkerClusteringDemoActivity.class);
4949
addDemo("Clustering: Custom Look", CustomMarkerClusteringDemoActivity.class);
50+
addDemo("Clustering: Diff", ClusteringDiffDemoActivity.class);
5051
addDemo("Clustering: 2K markers", BigClusteringDemoActivity.class);
5152
addDemo("Clustering: 20K only visible markers", VisibleClusteringDemoActivity.class);
5253
addDemo("Clustering: ViewModel", ClusteringViewModelDemoActivity.class);

demo/src/main/java/com/google/maps/android/utils/demo/model/Person.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import androidx.annotation.NonNull;
2323
import androidx.annotation.Nullable;
2424

25+
import java.util.Objects;
26+
27+
2528
public class Person implements ClusterItem {
2629
public final String name;
2730
public final int profilePhoto;
@@ -54,4 +57,18 @@ public String getSnippet() {
5457
public Float getZIndex() {
5558
return null;
5659
}
60+
61+
@Override
62+
public int hashCode() {
63+
return Objects.hashCode(name);
64+
}
65+
66+
// If we use the diff() operation, we need to implement an equals operation, to determine what
67+
// makes each ClusterItem unique (which is probably not the position)
68+
@Override
69+
public boolean equals(@Nullable Object obj) {
70+
if (obj != null && getClass() != obj.getClass()) return false;
71+
Person myObj = (Person) obj;
72+
return this.name.equals(myObj.name);
73+
}
5774
}

0 commit comments

Comments
 (0)