Animate Symbol Layer

This example shows how to Animate Symbol Layers

  • Add Custom Symbol Layers with Properties
  • Animate Symbol layers using Value Animator
  • Update Symbol layer source
Animate Symbol Layer

For all code examples, refer to Android Maps SDK Code Examples

activity_animate_markers.xml view source

1
<?xml version="1.0" encoding="utf-8"?>
2
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
xmlns:app="http://schemas.android.com/apk/res-auto"
4
xmlns:tools="http://schemas.android.com/tools"
5
android:layout_width="match_parent"
6
android:layout_height="match_parent"
7
tools:context=".MainActivity">
8
9
<ai.nextbillion.maps.core.MapView
10
android:id="@+id/map_view"
11
android:layout_width="match_parent"
12
android:layout_height="match_parent"
13
app:nbmap_uiAttribution="false"
14
app:nbmap_cameraTargetLat="53.550813508267716"
15
app:nbmap_cameraTargetLng="9.992248999933745"
16
app:nbmap_cameraZoom="15" />
17
18
<ImageView
19
android:id="@+id/iv_back"
20
android:layout_width="40dp"
21
android:layout_height="40dp"
22
android:layout_marginLeft="16dp"
23
app:layout_constraintTop_toTopOf="parent"
24
app:layout_constraintLeft_toLeftOf="parent"
25
android:layout_marginTop="16dp"
26
android:background="@drawable/circle_white_bg"
27
android:src="@drawable/icon_back"
28
app:tint="@color/color_back_icon"/>
29
30
</androidx.constraintlayout.widget.ConstraintLayout>

AnimateSymbolActivity view source

1
package ai.nextbillion;
2
3
import android.animation.Animator;
4
import android.animation.AnimatorListenerAdapter;
5
import android.animation.TypeEvaluator;
6
import android.animation.ValueAnimator;
7
import android.graphics.drawable.BitmapDrawable;
8
import android.os.Bundle;
9
import android.view.animation.LinearInterpolator;
10
import android.widget.ImageView;
11
12
import com.google.gson.JsonObject;
13
14
import java.util.ArrayList;
15
import java.util.List;
16
import java.util.Random;
17
18
import ai.nextbillion.kits.geojson.Feature;
19
import ai.nextbillion.kits.geojson.FeatureCollection;
20
import ai.nextbillion.kits.geojson.Point;
21
import ai.nextbillion.kits.turf.TurfMeasurement;
22
import ai.nextbillion.maps.core.MapView;
23
import ai.nextbillion.maps.core.NextbillionMap;
24
import ai.nextbillion.maps.core.OnMapReadyCallback;
25
import ai.nextbillion.maps.core.Style;
26
import ai.nextbillion.maps.geometry.LatLng;
27
import ai.nextbillion.maps.geometry.LatLngBounds;
28
import ai.nextbillion.maps.style.layers.SymbolLayer;
29
import ai.nextbillion.maps.style.sources.GeoJsonSource;
30
import androidx.annotation.NonNull;
31
import androidx.appcompat.app.AppCompatActivity;
32
33
import static ai.nextbillion.maps.style.expressions.Expression.get;
34
import static ai.nextbillion.maps.style.layers.PropertyFactory.iconAllowOverlap;
35
import static ai.nextbillion.maps.style.layers.PropertyFactory.iconIgnorePlacement;
36
import static ai.nextbillion.maps.style.layers.PropertyFactory.iconImage;
37
import static ai.nextbillion.maps.style.layers.PropertyFactory.iconRotate;
38
39
public class AnimateSymbolActivity extends AppCompatActivity implements OnMapReadyCallback {
40
private static final String TAXI = "taxi";
41
private static final String TAXI_LAYER = "taxi-layer";
42
private static final String TAXI_SOURCE = "taxi-source";
43
private static final String PROPERTY_BEARING = "bearing";
44
private static final int DURATION_RANDOM_MAX = 1500;
45
private static final int DURATION_BASE = 3000;
46
private final Random random = new Random();
47
48
private MapView mapView;
49
private NextbillionMap nextbillionMap;
50
private Style style;
51
private List<Taxi> taxis = new ArrayList<>();
52
private GeoJsonSource taxiSource;
53
private List<Animator> animators = new ArrayList<>();
54
private ImageView ivBack;
55
56
@Override
57
protected void onCreate(Bundle savedInstanceState) {
58
super.onCreate(savedInstanceState);
59
setContentView(R.layout.activity_animate_markers);
60
ivBack = findViewById(R.id.iv_back);
61
mapView = findViewById(R.id.map_view);
62
mapView.onCreate(savedInstanceState);
63
mapView.getMapAsync(this);
64
ivBack.setOnClickListener(v -> finish());
65
}
66
67
@Override
68
public void onMapReady(@NonNull NextbillionMap nextbillionMap) {
69
this.nextbillionMap = nextbillionMap;
70
nextbillionMap.getStyle(new Style.OnStyleLoaded() {
71
@Override
72
public void onStyleLoaded(@NonNull Style style) {
73
AnimateSymbolActivity.this.style = style;
74
generateTaxis();
75
animateTaxis();
76
}
77
});
78
}
79
80
///////////////////////////////////////////////////////////////////////////
81
// Lifecycle
82
///////////////////////////////////////////////////////////////////////////
83
84
@Override
85
protected void onStart() {
86
super.onStart();
87
mapView.onStart();
88
}
89
90
@Override
91
protected void onResume() {
92
super.onResume();
93
mapView.onResume();
94
}
95
96
@Override
97
protected void onPause() {
98
super.onPause();
99
mapView.onPause();
100
}
101
102
@Override
103
protected void onStop() {
104
super.onStop();
105
mapView.onStop();
106
}
107
108
@Override
109
protected void onSaveInstanceState(@NonNull Bundle outState) {
110
super.onSaveInstanceState(outState);
111
mapView.onSaveInstanceState(outState);
112
}
113
114
@Override
115
protected void onDestroy() {
116
super.onDestroy();
117
for (Animator animator : animators) {
118
if (animator != null) {
119
animator.removeAllListeners();
120
animator.cancel();
121
}
122
}
123
mapView.onDestroy();
124
}
125
126
@Override
127
public void onLowMemory() {
128
super.onLowMemory();
129
mapView.onLowMemory();
130
}
131
132
///////////////////////////////////////////////////////////////////////////
133
//
134
///////////////////////////////////////////////////////////////////////////
135
136
private void generateTaxis(){
137
style.addImage(TAXI,
138
((BitmapDrawable) getResources().getDrawable(R.mipmap.beat_taxi)).getBitmap());
139
140
for (int i = 0; i < 10; i++) {
141
LatLng latLng = getRandomLatLng();
142
LatLng destination = getRandomLatLng();
143
JsonObject properties = new JsonObject();
144
145
properties.addProperty(PROPERTY_BEARING, Taxi.getBearing(latLng, destination));
146
Feature feature = Feature.fromGeometry(
147
Point.fromLngLat(
148
latLng.getLongitude(),
149
latLng.getLatitude()), properties);
150
151
Taxi taxi = new Taxi(feature, destination, getDuration());
152
taxis.add(taxi);
153
}
154
155
taxiSource = new GeoJsonSource(TAXI_SOURCE, taxiMarkerFeatures());
156
style.addSource(taxiSource);
157
158
SymbolLayer symbolLayer = new SymbolLayer(TAXI_LAYER, TAXI_SOURCE);
159
style.addLayer(symbolLayer);
160
symbolLayer.withProperties(
161
iconImage(TAXI),
162
iconAllowOverlap(true),
163
iconRotate(get(PROPERTY_BEARING)),
164
iconIgnorePlacement(true)
165
);
166
}
167
168
private FeatureCollection taxiMarkerFeatures() {
169
List<Feature> features = new ArrayList<>();
170
for (Taxi taxi : taxis) {
171
features.add(taxi.feature);
172
}
173
return FeatureCollection.fromFeatures(features);
174
}
175
176
private void animateTaxis(){
177
final Taxi longestDrive = getLongestDrive();
178
final Random random = new Random();
179
for (final Taxi taxi : taxis) {
180
final boolean isLongestDrive = longestDrive.equals(taxi);
181
ValueAnimator valueAnimator = ValueAnimator.ofObject(new LatLngEvaluator(), taxi.current, taxi.next);
182
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
183
private LatLng latLng;
184
185
@Override
186
public void onAnimationUpdate(ValueAnimator animation) {
187
latLng = (LatLng) animation.getAnimatedValue();
188
taxi.current = latLng;
189
if (isLongestDrive) {
190
updateTaxisSource();;
191
}
192
}
193
});
194
195
if (isLongestDrive) {
196
valueAnimator.addListener(new AnimatorListenerAdapter() {
197
@Override
198
public void onAnimationEnd(Animator animation) {
199
super.onAnimationEnd(animation);
200
updateDestinations();
201
animateTaxis();
202
}
203
});
204
}
205
206
valueAnimator.addListener(new AnimatorListenerAdapter() {
207
@Override
208
public void onAnimationStart(Animator animation) {
209
super.onAnimationStart(animation);
210
taxi.feature.properties().addProperty("bearing", Taxi.getBearing(taxi.current, taxi.next));
211
}
212
});
213
214
int offset = random.nextInt(2) == 0 ? 0 : random.nextInt(1000) + 250;
215
valueAnimator.setStartDelay(offset);
216
valueAnimator.setDuration(taxi.duration - offset);
217
valueAnimator.setInterpolator(new LinearInterpolator());
218
valueAnimator.start();
219
220
animators.add(valueAnimator);
221
}
222
}
223
224
private void updateTaxisSource() {
225
for (Taxi taxi : taxis) {
226
taxi.updateFeature();
227
}
228
taxiSource.setGeoJson(taxiMarkerFeatures());
229
}
230
231
private void updateDestinations(){
232
for (Taxi taxi : taxis) {
233
taxi.setNext(getRandomLatLng());
234
}
235
}
236
237
///////////////////////////////////////////////////////////////////////////
238
//
239
///////////////////////////////////////////////////////////////////////////
240
241
private LatLng getRandomLatLng() {
242
LatLngBounds bounds = nextbillionMap.getProjection().getVisibleRegion().latLngBounds;
243
Random generator = new Random();
244
double randomLat = bounds.getLatSouth() + generator.nextDouble()
245
* (bounds.getLatNorth() - bounds.getLatSouth());
246
double randomLon = bounds.getLonWest() + generator.nextDouble()
247
* (bounds.getLonEast() - bounds.getLonWest());
248
return new LatLng(randomLat, randomLon);
249
}
250
251
private long getDuration() {
252
return random.nextInt(DURATION_RANDOM_MAX) + DURATION_BASE;
253
}
254
255
private Taxi getLongestDrive() {
256
Taxi longestDrive = null;
257
for (Taxi taxi : taxis) {
258
if (longestDrive == null) {
259
longestDrive = taxi;
260
} else if (longestDrive.duration < taxi.duration) {
261
longestDrive = taxi;
262
}
263
}
264
return longestDrive;
265
}
266
267
///////////////////////////////////////////////////////////////////////////
268
//
269
///////////////////////////////////////////////////////////////////////////
270
271
private static class Taxi {
272
private Feature feature;
273
private LatLng next;
274
private LatLng current;
275
private long duration;
276
277
Taxi(Feature feature, LatLng next, long duration) {
278
this.feature = feature;
279
Point point = ((Point) feature.geometry());
280
this.current = new LatLng(point.latitude(), point.longitude());
281
this.duration = duration;
282
this.next = next;
283
}
284
285
void setNext(LatLng next) {
286
this.next = next;
287
}
288
289
void updateFeature() {
290
feature = Feature.fromGeometry(Point.fromLngLat(
291
current.getLongitude(),
292
current.getLatitude())
293
);
294
feature.properties().addProperty("bearing", getBearing(current, next));
295
}
296
297
private static float getBearing(LatLng from, LatLng to) {
298
return (float) TurfMeasurement.bearing(
299
Point.fromLngLat(from.getLongitude(), from.getLatitude()),
300
Point.fromLngLat(to.getLongitude(), to.getLatitude())
301
);
302
}
303
}
304
305
private static class LatLngEvaluator implements TypeEvaluator<LatLng> {
306
307
private LatLng latLng = new LatLng();
308
309
@Override
310
public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
311
latLng.setLatitude(startValue.getLatitude()
312
+ ((endValue.getLatitude() - startValue.getLatitude()) * fraction));
313
latLng.setLongitude(startValue.getLongitude()
314
+ ((endValue.getLongitude() - startValue.getLongitude()) * fraction));
315
return latLng;
316
}
317
}
318
319
}

The example code is an Android activity that demonstrates how to animate symbol markers on a map using the Nextbillion Maps SDK. Here's a summary of the code:

Initializing MapView:

  • The MapView is initialized in the onCreate method using the mapView.onCreate(savedInstanceState) method.

Adding Symbol Layer Source:

  • The generateTaxis method adds a symbol layer source to the map.
  • It defines a custom image called "taxi" using a bitmap resource.
  • It creates a GeoJsonSource object named "taxi-source" and adds it to the map's style.
  • A SymbolLayer named "taxi-layer" is also created and added to the style.
  • The SymbolLayer properties are set to display the "taxi" icon image and allow overlap.

Animating Symbol Layer:

  • The animateTaxis method animates the symbol markers.
  • It uses ValueAnimator to animate the markers' positions from their current location to a new location.
  • A ValueAnimator listener updates the taxi's current position and calls updateTaxisSource to update the source on the map.
  • The longest drive is identified, and when its animation ends, it updates the destinations of all taxis and restarts the animation.
  • Each animator is given a random start delay and duration based on the taxi's duration.

Updating Symbol Layer Source:

  • The updateTaxisSource method updates the GeoJsonSource on the map with the updated taxi marker features.
  • The code uses Nextbillion Maps SDK to work with maps, symbols, and animations in an Android application. It generates random taxi markers on the map, animates their movement, and updates the marker positions dynamically.

Additional notes:

  • The code includes lifecycle methods (onStart, onResume, onPause, onStop, onSaveInstanceState, onDestroy, onLowMemory) to manage the lifecycle of the MapView.

© 2024 NextBillion.ai all rights reserved.