In this page

Animate Symbol Layer

This example shows how to Animate Symbol Layers

  1. Add Custom Symbol Layers with Properties

  2. Animate Symbol layers using Value Animator

  3. 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:

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

Adding Symbol Layer Source:

  1. The generateTaxis method adds a symbol layer source to the map.

  2. It defines a custom image called "taxi" using a bitmap resource.

  3. It creates a GeoJsonSource object named "taxi-source" and adds it to the map's style.

  4. A SymbolLayer named "taxi-layer" is also created and added to the style.

  5. The SymbolLayer properties are set to display the "taxi" icon image and allow overlap.

Animating Symbol Layer:

  1. The animateTaxis method animates the symbol markers.

  2. It uses ValueAnimator to animate the markers' positions from their current location to a new location.

  3. A ValueAnimator listener updates the taxi's current position and calls updateTaxisSource to update the source on the map.

  4. The longest drive is identified, and when its animation ends, it updates the destinations of all taxis and restarts the animation.

  5. Each animator is given a random start delay and duration based on the taxi's duration.

Updating Symbol Layer Source:

  1. The updateTaxisSource method updates the GeoJsonSource on the map with the updated taxi marker features.

  2. 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:

  1. The code includes lifecycle methods (onStart, onResume, onPause, onStop, onSaveInstanceState, onDestroy, onLowMemory) to manage the lifecycle of the MapView.
DIDN'T FIND WHAT YOU LOOKING FOR?