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"3xmlns:app="http://schemas.android.com/apk/res-auto"4xmlns:tools="http://schemas.android.com/tools"5android:layout_width="match_parent"6android:layout_height="match_parent"7tools:context=".MainActivity">89<ai.nextbillion.maps.core.MapView10android:id="@+id/map_view"11android:layout_width="match_parent"12android:layout_height="match_parent"13app:nbmap_uiAttribution="false"14app:nbmap_cameraTargetLat="53.550813508267716"15app:nbmap_cameraTargetLng="9.992248999933745"16app:nbmap_cameraZoom="15" />1718<ImageView19android:id="@+id/iv_back"20android:layout_width="40dp"21android:layout_height="40dp"22android:layout_marginLeft="16dp"23app:layout_constraintTop_toTopOf="parent"24app:layout_constraintLeft_toLeftOf="parent"25android:layout_marginTop="16dp"26android:background="@drawable/circle_white_bg"27android:src="@drawable/icon_back"28app:tint="@color/color_back_icon"/>2930</androidx.constraintlayout.widget.ConstraintLayout>
AnimateSymbolActivity view source
1package ai.nextbillion;23import 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;1112import com.google.gson.JsonObject;1314import java.util.ArrayList;15import java.util.List;16import java.util.Random;1718import 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;3233import 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;3839public class AnimateSymbolActivity extends AppCompatActivity implements OnMapReadyCallback {40private static final String TAXI = "taxi";41private static final String TAXI_LAYER = "taxi-layer";42private static final String TAXI_SOURCE = "taxi-source";43private static final String PROPERTY_BEARING = "bearing";44private static final int DURATION_RANDOM_MAX = 1500;45private static final int DURATION_BASE = 3000;46private final Random random = new Random();4748private MapView mapView;49private NextbillionMap nextbillionMap;50private Style style;51private List<Taxi> taxis = new ArrayList<>();52private GeoJsonSource taxiSource;53private List<Animator> animators = new ArrayList<>();54private ImageView ivBack;5556@Override57protected void onCreate(Bundle savedInstanceState) {58super.onCreate(savedInstanceState);59setContentView(R.layout.activity_animate_markers);60ivBack = findViewById(R.id.iv_back);61mapView = findViewById(R.id.map_view);62mapView.onCreate(savedInstanceState);63mapView.getMapAsync(this);64ivBack.setOnClickListener(v -> finish());65}6667@Override68public void onMapReady(@NonNull NextbillionMap nextbillionMap) {69this.nextbillionMap = nextbillionMap;70nextbillionMap.getStyle(new Style.OnStyleLoaded() {71@Override72public void onStyleLoaded(@NonNull Style style) {73AnimateSymbolActivity.this.style = style;74generateTaxis();75animateTaxis();76}77});78}7980///////////////////////////////////////////////////////////////////////////81// Lifecycle82///////////////////////////////////////////////////////////////////////////8384@Override85protected void onStart() {86super.onStart();87mapView.onStart();88}8990@Override91protected void onResume() {92super.onResume();93mapView.onResume();94}9596@Override97protected void onPause() {98super.onPause();99mapView.onPause();100}101102@Override103protected void onStop() {104super.onStop();105mapView.onStop();106}107108@Override109protected void onSaveInstanceState(@NonNull Bundle outState) {110super.onSaveInstanceState(outState);111mapView.onSaveInstanceState(outState);112}113114@Override115protected void onDestroy() {116super.onDestroy();117for (Animator animator : animators) {118if (animator != null) {119animator.removeAllListeners();120animator.cancel();121}122}123mapView.onDestroy();124}125126@Override127public void onLowMemory() {128super.onLowMemory();129mapView.onLowMemory();130}131132///////////////////////////////////////////////////////////////////////////133//134///////////////////////////////////////////////////////////////////////////135136private void generateTaxis(){137style.addImage(TAXI,138((BitmapDrawable) getResources().getDrawable(R.mipmap.beat_taxi)).getBitmap());139140for (int i = 0; i < 10; i++) {141LatLng latLng = getRandomLatLng();142LatLng destination = getRandomLatLng();143JsonObject properties = new JsonObject();144145properties.addProperty(PROPERTY_BEARING, Taxi.getBearing(latLng, destination));146Feature feature = Feature.fromGeometry(147Point.fromLngLat(148latLng.getLongitude(),149latLng.getLatitude()), properties);150151Taxi taxi = new Taxi(feature, destination, getDuration());152taxis.add(taxi);153}154155taxiSource = new GeoJsonSource(TAXI_SOURCE, taxiMarkerFeatures());156style.addSource(taxiSource);157158SymbolLayer symbolLayer = new SymbolLayer(TAXI_LAYER, TAXI_SOURCE);159style.addLayer(symbolLayer);160symbolLayer.withProperties(161iconImage(TAXI),162iconAllowOverlap(true),163iconRotate(get(PROPERTY_BEARING)),164iconIgnorePlacement(true)165);166}167168private FeatureCollection taxiMarkerFeatures() {169List<Feature> features = new ArrayList<>();170for (Taxi taxi : taxis) {171features.add(taxi.feature);172}173return FeatureCollection.fromFeatures(features);174}175176private void animateTaxis(){177final Taxi longestDrive = getLongestDrive();178final Random random = new Random();179for (final Taxi taxi : taxis) {180final boolean isLongestDrive = longestDrive.equals(taxi);181ValueAnimator valueAnimator = ValueAnimator.ofObject(new LatLngEvaluator(), taxi.current, taxi.next);182valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {183private LatLng latLng;184185@Override186public void onAnimationUpdate(ValueAnimator animation) {187latLng = (LatLng) animation.getAnimatedValue();188taxi.current = latLng;189if (isLongestDrive) {190updateTaxisSource();;191}192}193});194195if (isLongestDrive) {196valueAnimator.addListener(new AnimatorListenerAdapter() {197@Override198public void onAnimationEnd(Animator animation) {199super.onAnimationEnd(animation);200updateDestinations();201animateTaxis();202}203});204}205206valueAnimator.addListener(new AnimatorListenerAdapter() {207@Override208public void onAnimationStart(Animator animation) {209super.onAnimationStart(animation);210taxi.feature.properties().addProperty("bearing", Taxi.getBearing(taxi.current, taxi.next));211}212});213214int offset = random.nextInt(2) == 0 ? 0 : random.nextInt(1000) + 250;215valueAnimator.setStartDelay(offset);216valueAnimator.setDuration(taxi.duration - offset);217valueAnimator.setInterpolator(new LinearInterpolator());218valueAnimator.start();219220animators.add(valueAnimator);221}222}223224private void updateTaxisSource() {225for (Taxi taxi : taxis) {226taxi.updateFeature();227}228taxiSource.setGeoJson(taxiMarkerFeatures());229}230231private void updateDestinations(){232for (Taxi taxi : taxis) {233taxi.setNext(getRandomLatLng());234}235}236237///////////////////////////////////////////////////////////////////////////238//239///////////////////////////////////////////////////////////////////////////240241private LatLng getRandomLatLng() {242LatLngBounds bounds = nextbillionMap.getProjection().getVisibleRegion().latLngBounds;243Random generator = new Random();244double randomLat = bounds.getLatSouth() + generator.nextDouble()245* (bounds.getLatNorth() - bounds.getLatSouth());246double randomLon = bounds.getLonWest() + generator.nextDouble()247* (bounds.getLonEast() - bounds.getLonWest());248return new LatLng(randomLat, randomLon);249}250251private long getDuration() {252return random.nextInt(DURATION_RANDOM_MAX) + DURATION_BASE;253}254255private Taxi getLongestDrive() {256Taxi longestDrive = null;257for (Taxi taxi : taxis) {258if (longestDrive == null) {259longestDrive = taxi;260} else if (longestDrive.duration < taxi.duration) {261longestDrive = taxi;262}263}264return longestDrive;265}266267///////////////////////////////////////////////////////////////////////////268//269///////////////////////////////////////////////////////////////////////////270271private static class Taxi {272private Feature feature;273private LatLng next;274private LatLng current;275private long duration;276277Taxi(Feature feature, LatLng next, long duration) {278this.feature = feature;279Point point = ((Point) feature.geometry());280this.current = new LatLng(point.latitude(), point.longitude());281this.duration = duration;282this.next = next;283}284285void setNext(LatLng next) {286this.next = next;287}288289void updateFeature() {290feature = Feature.fromGeometry(Point.fromLngLat(291current.getLongitude(),292current.getLatitude())293);294feature.properties().addProperty("bearing", getBearing(current, next));295}296297private static float getBearing(LatLng from, LatLng to) {298return (float) TurfMeasurement.bearing(299Point.fromLngLat(from.getLongitude(), from.getLatitude()),300Point.fromLngLat(to.getLongitude(), to.getLatitude())301);302}303}304305private static class LatLngEvaluator implements TypeEvaluator<LatLng> {306307private LatLng latLng = new LatLng();308309@Override310public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {311latLng.setLatitude(startValue.getLatitude()312+ ((endValue.getLatitude() - startValue.getLatitude()) * fraction));313latLng.setLongitude(startValue.getLongitude()314+ ((endValue.getLongitude() - startValue.getLongitude()) * fraction));315return latLng;316}317}318319}
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.