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

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
1package ai.nextbillion;
2
3import android.animation.Animator;
4import android.animation.AnimatorListenerAdapter;
5import android.animation.TypeEvaluator;
6import android.animation.ValueAnimator;
7import android.graphics.drawable.BitmapDrawable;
8import android.os.Bundle;
9import android.view.animation.LinearInterpolator;
10import android.widget.ImageView;
11
12import com.google.gson.JsonObject;
13
14import java.util.ArrayList;
15import java.util.List;
16import java.util.Random;
17
18import ai.nextbillion.kits.geojson.Feature;
19import ai.nextbillion.kits.geojson.FeatureCollection;
20import ai.nextbillion.kits.geojson.Point;
21import ai.nextbillion.kits.turf.TurfMeasurement;
22import ai.nextbillion.maps.core.MapView;
23import ai.nextbillion.maps.core.NextbillionMap;
24import ai.nextbillion.maps.core.OnMapReadyCallback;
25import ai.nextbillion.maps.core.Style;
26import ai.nextbillion.maps.geometry.LatLng;
27import ai.nextbillion.maps.geometry.LatLngBounds;
28import ai.nextbillion.maps.style.layers.SymbolLayer;
29import ai.nextbillion.maps.style.sources.GeoJsonSource;
30import androidx.annotation.NonNull;
31import androidx.appcompat.app.AppCompatActivity;
32
33import static ai.nextbillion.maps.style.expressions.Expression.get;
34import static ai.nextbillion.maps.style.layers.PropertyFactory.iconAllowOverlap;
35import static ai.nextbillion.maps.style.layers.PropertyFactory.iconIgnorePlacement;
36import static ai.nextbillion.maps.style.layers.PropertyFactory.iconImage;
37import static ai.nextbillion.maps.style.layers.PropertyFactory.iconRotate;
38
39public 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.