In this page

Custom Location Pulsing Symbol

This example shows how to Custom Location Pulsing Style

  1. Location Permissions Handling

  2. Tracking current location automatically when Map ready

  3. Custom location pulsing style

    1. pulseColor

    2. pulseSingleDuration

    3. Enable/Disable pulsing

    4. pulseMaxRadius

    5. pulseAlpha

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

activity_customized_location_pulsing_circle.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
8    <ai.nextbillion.maps.core.MapView
9        android:id="@+id/mapView"
10        android:layout_width="match_parent"
11        android:layout_height="match_parent"
12        android:layout_marginBottom="0dp"
13        app:layout_constraintBottom_toTopOf="@+id/linearLayout"
14        app:layout_constraintLeft_toLeftOf="parent"
15        app:layout_constraintRight_toRightOf="parent"
16        app:layout_constraintTop_toTopOf="parent"
17        app:nbmap_uiAttribution="false" />
18
19    <LinearLayout
20        android:id="@+id/linearLayout"
21        style="?android:attr/buttonBarStyle"
22        android:layout_width="0dp"
23        android:layout_height="wrap_content"
24        android:background="@color/palette_mint_100"
25        android:orientation="horizontal"
26        android:weightSum="4"
27        app:layout_constraintBottom_toBottomOf="parent"
28        app:layout_constraintLeft_toLeftOf="parent"
29        app:layout_constraintRight_toRightOf="parent"
30        tools:layout_constraintBottom_creator="1"
31        tools:layout_constraintLeft_creator="1"
32        tools:layout_constraintRight_creator="1">
33
34        <TextView
35            android:id="@+id/tv_frequency"
36            android:layout_width="0dp"
37            android:layout_height="match_parent"
38            android:layout_weight=".75"
39            android:gravity="center"
40            android:text="Duration:"
41            android:textColor="@color/white"
42            android:textSize="18sp"
43            android:textStyle="bold" />
44
45        <Button
46            android:id="@+id/button_location_circle_duration"
47            style="?android:attr/buttonBarButtonStyle"
48            android:layout_width="0dp"
49            android:layout_height="wrap_content"
50            android:layout_weight="1.25"
51            android:gravity="center"
52            tools:text="400ms"
53            android:textColor="@android:color/white" />
54
55        <TextView
56            android:id="@+id/tv_color"
57            android:layout_width="0dp"
58            android:layout_height="match_parent"
59            android:layout_weight=".85"
60            android:gravity="center"
61            android:text="Color:"
62            android:textColor="@color/white"
63            android:textSize="18sp"
64            android:textStyle="bold" />
65
66        <Button
67            android:id="@+id/button_location_circle_color"
68            style="?android:attr/buttonBarButtonStyle"
69            android:layout_width="0dp"
70            android:layout_height="wrap_content"
71            android:layout_weight="1.15"
72            android:gravity="center"
73            android:text="None"
74            android:textColor="@android:color/white" />
75
76    </LinearLayout>
77
78</androidx.constraintlayout.widget.ConstraintLayout>

CustomizedLocationPulsingCircleActivity view source

1package ai.nextbillion;
2
3import android.annotation.SuppressLint;
4import android.graphics.Color;
5import android.location.Location;
6import android.os.Bundle;
7import android.view.Menu;
8import android.view.MenuItem;
9import android.view.animation.DecelerateInterpolator;
10import android.view.animation.Interpolator;
11import android.widget.ArrayAdapter;
12import android.widget.Button;
13import android.widget.Toast;
14
15import java.util.ArrayList;
16import java.util.List;
17
18import ai.nextbillion.maps.camera.CameraUpdateFactory;
19import ai.nextbillion.maps.core.MapView;
20import ai.nextbillion.maps.core.NextbillionMap;
21import ai.nextbillion.maps.core.OnMapReadyCallback;
22import ai.nextbillion.maps.core.Style;
23import ai.nextbillion.maps.location.LocationComponent;
24import ai.nextbillion.maps.location.LocationComponentActivationOptions;
25import ai.nextbillion.maps.location.LocationComponentOptions;
26import ai.nextbillion.maps.location.engine.LocationEngineRequest;
27import ai.nextbillion.maps.location.modes.CameraMode;
28import ai.nextbillion.maps.location.permissions.PermissionsListener;
29import ai.nextbillion.maps.location.permissions.PermissionsManager;
30import androidx.annotation.NonNull;
31import androidx.appcompat.app.AppCompatActivity;
32import androidx.appcompat.widget.ListPopupWindow;
33
34
35/**
36 * This activity shows how to customize the LocationComponent's pulsing circle.
37 */
38public class CustomizedLocationPulsingCircleActivity extends AppCompatActivity implements OnMapReadyCallback {
39
40    //region
41
42    // Adjust these variables to customize the example's pulsing circle UI
43    private static final float DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS = 2300;
44    private static final float SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS = 800;
45    private static final float THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS = 8000;
46    private static final float DEFAULT_LOCATION_CIRCLE_PULSE_RADIUS = 35;
47    private static final float DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA = .55f;
48    private static final Interpolator DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE
49            = new DecelerateInterpolator();
50    private static final boolean DEFAULT_LOCATION_CIRCLE_PULSE_FADE_MODE = true;
51    //endregion
52
53    //region
54    private static int LOCATION_CIRCLE_PULSE_COLOR;
55    private static float LOCATION_CIRCLE_PULSE_DURATION = DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS;
56    private static final String SAVED_STATE_LOCATION = "saved_state_location";
57    private static final String SAVED_STATE_LOCATION_CIRCLE_PULSE_COLOR = "saved_state_color";
58    private static final String SAVED_STATE_LOCATION_CIRCLE_PULSE_DURATION = "saved_state_duration";
59    private static final String LAYER_BELOW_ID = "waterway-label";
60
61    private Location lastLocation;
62    private MapView mapView;
63    private Button pulsingCircleDurationButton;
64    private Button pulsingCircleColorButton;
65    private PermissionsManager permissionsManager;
66    private LocationComponent locationComponent;
67    private NextbillionMap nextbillionMap;
68    private float currentPulseDuration;
69    //endregion
70
71    @Override
72    protected void onCreate(Bundle savedInstanceState) {
73        super.onCreate(savedInstanceState);
74        setContentView(R.layout.activity_customized_location_pulsing_circle);
75
76        LOCATION_CIRCLE_PULSE_COLOR = Color.BLUE;
77
78        mapView = findViewById(R.id.mapView);
79
80        if (savedInstanceState != null) {
81            lastLocation = savedInstanceState.getParcelable(SAVED_STATE_LOCATION);
82            LOCATION_CIRCLE_PULSE_COLOR = savedInstanceState.getInt(SAVED_STATE_LOCATION_CIRCLE_PULSE_COLOR);
83            LOCATION_CIRCLE_PULSE_DURATION = savedInstanceState.getFloat(SAVED_STATE_LOCATION_CIRCLE_PULSE_DURATION);
84        }
85
86        pulsingCircleDurationButton = findViewById(R.id.button_location_circle_duration);
87        pulsingCircleDurationButton.setText(String.format("%sms",
88                String.valueOf(LOCATION_CIRCLE_PULSE_DURATION)));
89        pulsingCircleDurationButton.setOnClickListener(v -> {
90            if (locationComponent == null) {
91                return;
92            }
93            showDurationListDialog();
94        });
95
96        pulsingCircleColorButton = findViewById(R.id.button_location_circle_color);
97        pulsingCircleColorButton.setOnClickListener(v -> {
98            if (locationComponent == null) {
99                return;
100            }
101            showColorListDialog();
102        });
103
104        mapView.onCreate(savedInstanceState);
105
106        checkPermissions();
107    }
108
109    @SuppressLint("MissingPermission")
110    @Override
111    public void onMapReady(@NonNull NextbillionMap nextbillionMap) {
112        this.nextbillionMap = nextbillionMap;
113        nextbillionMap.animateCamera(CameraUpdateFactory.zoomBy(13));
114
115        nextbillionMap.setStyle(StyleConstants.LIGHT, style -> {
116            locationComponent = nextbillionMap.getLocationComponent();
117
118            LocationComponentOptions locationComponentOptions =
119                    buildLocationComponentOptions(
120                            LOCATION_CIRCLE_PULSE_COLOR,
121                            LOCATION_CIRCLE_PULSE_DURATION)
122                            .pulseEnabled(true)
123                            .build();
124
125            LocationComponentActivationOptions locationComponentActivationOptions =
126                    buildLocationComponentActivationOptions(style,locationComponentOptions);
127
128            locationComponent.activateLocationComponent(locationComponentActivationOptions);
129            locationComponent.setLocationComponentEnabled(true);
130            locationComponent.setCameraMode(CameraMode.TRACKING);
131            locationComponent.forceLocationUpdate(lastLocation);
132        });
133    }
134
135    private LocationComponentOptions.Builder buildLocationComponentOptions(int pulsingCircleColor,
136                                                                           float pulsingCircleDuration
137    ) {
138        currentPulseDuration = pulsingCircleDuration;
139        return LocationComponentOptions.builder(this)
140                .layerBelow(LAYER_BELOW_ID)
141                .pulseFadeEnabled(DEFAULT_LOCATION_CIRCLE_PULSE_FADE_MODE)
142                .pulseInterpolator(DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE)
143                .pulseColor(pulsingCircleColor)
144                .pulseAlpha(DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA)
145                .pulseSingleDuration(pulsingCircleDuration)
146                .pulseMaxRadius(DEFAULT_LOCATION_CIRCLE_PULSE_RADIUS);
147    }
148
149    @SuppressLint("MissingPermission")
150    private void setNewLocationComponentOptions(float newPulsingDuration,
151                                                int newPulsingColor) {
152        nextbillionMap.getStyle(style -> locationComponent.applyStyle(
153                buildLocationComponentOptions(
154                        newPulsingColor,
155                        newPulsingDuration)
156                        .pulseEnabled(true)
157                        .build()));
158    }
159
160    private LocationComponentActivationOptions buildLocationComponentActivationOptions(
161            @NonNull Style style,
162            @NonNull LocationComponentOptions locationComponentOptions) {
163        return LocationComponentActivationOptions
164                .builder(this, style)
165                .locationComponentOptions(locationComponentOptions)
166                .useDefaultLocationEngine(true)
167                .locationEngineRequest(new LocationEngineRequest.Builder(750)
168                        .setFastestInterval(750)
169                        .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
170                        .build())
171                .build();
172    }
173
174    @Override
175    public boolean onCreateOptionsMenu(Menu menu) {
176        getMenuInflater().inflate(R.menu.menu_pulsing_location_mode, menu);
177        return true;
178    }
179
180    @SuppressLint("MissingPermission")
181    @Override
182    public boolean onOptionsItemSelected(MenuItem item) {
183        if (locationComponent == null) {
184            return super.onOptionsItemSelected(item);
185        }
186
187        int id = item.getItemId();
188        if (id == R.id.action_component_disable) {
189            locationComponent.setLocationComponentEnabled(false);
190            return true;
191        } else if (id == R.id.action_component_enabled) {
192            locationComponent.setLocationComponentEnabled(true);
193            return true;
194        } else if (id == R.id.action_stop_pulsing) {
195            locationComponent.applyStyle(LocationComponentOptions.builder(
196                            CustomizedLocationPulsingCircleActivity.this)
197                    .pulseEnabled(false)
198                    .build());
199            return true;
200        } else if (id == R.id.action_start_pulsing) {
201            locationComponent.applyStyle(buildLocationComponentOptions(
202                    LOCATION_CIRCLE_PULSE_COLOR,
203                    LOCATION_CIRCLE_PULSE_DURATION)
204                    .pulseEnabled(true)
205                    .build());
206            return true;
207        }
208        return super.onOptionsItemSelected(item);
209    }
210
211    private void checkPermissions() {
212        if (PermissionsManager.areLocationPermissionsGranted(this)) {
213            mapView.getMapAsync(this);
214        } else {
215            permissionsManager = new PermissionsManager(new PermissionsListener() {
216                @Override
217                public void onExplanationNeeded(List<String> permissionsToExplain) {
218                    Toast.makeText(CustomizedLocationPulsingCircleActivity.this, "You need to accept location permissions.",
219                            Toast.LENGTH_SHORT).show();
220                }
221
222                @Override
223                public void onPermissionResult(boolean granted) {
224                    if (granted) {
225                        mapView.getMapAsync(CustomizedLocationPulsingCircleActivity.this);
226                    } else {
227                        finish();
228                    }
229                }
230            });
231            permissionsManager.requestLocationPermissions(this);
232        }
233    }
234
235    private void showDurationListDialog() {
236        List<String> modes = new ArrayList<>();
237        modes.add(String.format("%sms", String.valueOf(DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS)));
238        modes.add(String.format("%sms", String.valueOf(SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS)));
239        modes.add(String.format("%sms", String.valueOf(THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS)));
240        ArrayAdapter<String> profileAdapter = new ArrayAdapter<>(this,
241                android.R.layout.simple_list_item_1, modes);
242        ListPopupWindow listPopup = new ListPopupWindow(this);
243        listPopup.setAdapter(profileAdapter);
244        listPopup.setAnchorView(pulsingCircleDurationButton);
245        listPopup.setOnItemClickListener((parent, itemView, position, id) -> {
246            String selectedMode = modes.get(position);
247            pulsingCircleDurationButton.setText(selectedMode);
248            if (selectedMode.contentEquals(String.format("%sms",
249                    String.valueOf(DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS)))) {
250                LOCATION_CIRCLE_PULSE_DURATION = DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS;
251                setNewLocationComponentOptions(DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS, LOCATION_CIRCLE_PULSE_COLOR);
252            } else if (selectedMode.contentEquals(String.format("%sms",
253                    String.valueOf(SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS)))) {
254                LOCATION_CIRCLE_PULSE_DURATION = SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS;
255                setNewLocationComponentOptions(SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS, LOCATION_CIRCLE_PULSE_COLOR);
256            } else if (selectedMode.contentEquals(String.format("%sms",
257                    String.valueOf(THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS)))) {
258                LOCATION_CIRCLE_PULSE_DURATION = THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS;
259                setNewLocationComponentOptions(THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS, LOCATION_CIRCLE_PULSE_COLOR);
260            }
261            listPopup.dismiss();
262        });
263        listPopup.show();
264    }
265
266    private void showColorListDialog() {
267        List<String> trackingTypes = new ArrayList<>();
268        trackingTypes.add("Blue");
269        trackingTypes.add("Red");
270        trackingTypes.add("Green");
271        trackingTypes.add("Gray");
272        ArrayAdapter<String> profileAdapter = new ArrayAdapter<>(this,
273                android.R.layout.simple_list_item_1, trackingTypes);
274        ListPopupWindow listPopup = new ListPopupWindow(this);
275        listPopup.setAdapter(profileAdapter);
276        listPopup.setAnchorView(pulsingCircleColorButton);
277        listPopup.setOnItemClickListener((parent, itemView, position, id) -> {
278            String selectedTrackingType = trackingTypes.get(position);
279            pulsingCircleColorButton.setText(selectedTrackingType);
280            if (selectedTrackingType.contentEquals("Blue")) {
281                LOCATION_CIRCLE_PULSE_COLOR = Color.BLUE;
282                setNewLocationComponentOptions(currentPulseDuration, Color.BLUE);
283            } else if (selectedTrackingType.contentEquals("Red")) {
284                LOCATION_CIRCLE_PULSE_COLOR = Color.RED;
285                setNewLocationComponentOptions(currentPulseDuration, Color.RED);
286            } else if (selectedTrackingType.contentEquals("Green")) {
287                LOCATION_CIRCLE_PULSE_COLOR = Color.GREEN;
288                setNewLocationComponentOptions(currentPulseDuration, Color.GREEN);
289            } else if (selectedTrackingType.contentEquals("Gray")) {
290                LOCATION_CIRCLE_PULSE_COLOR = Color.parseColor("#4a4a4a");
291                setNewLocationComponentOptions(currentPulseDuration, Color.parseColor("#4a4a4a"));
292            }
293            listPopup.dismiss();
294        });
295        listPopup.show();
296    }
297
298
299    @Override
300    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
301        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
302        permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
303    }
304
305    @Override
306    protected void onStart() {
307        super.onStart();
308        mapView.onStart();
309    }
310
311    @Override
312    protected void onResume() {
313        super.onResume();
314        mapView.onResume();
315    }
316
317    @Override
318    protected void onPause() {
319        super.onPause();
320        mapView.onPause();
321    }
322
323    @Override
324    protected void onStop() {
325        super.onStop();
326        mapView.onStop();
327    }
328
329    @SuppressLint("MissingPermission")
330    @Override
331    protected void onSaveInstanceState(Bundle outState) {
332        super.onSaveInstanceState(outState);
333        mapView.onSaveInstanceState(outState);
334        if (locationComponent != null) {
335            outState.putParcelable(SAVED_STATE_LOCATION, locationComponent.getLastKnownLocation());
336            outState.putInt(SAVED_STATE_LOCATION_CIRCLE_PULSE_COLOR, LOCATION_CIRCLE_PULSE_COLOR);
337            outState.putFloat(SAVED_STATE_LOCATION_CIRCLE_PULSE_DURATION, LOCATION_CIRCLE_PULSE_DURATION);
338        }
339    }
340
341    @Override
342    protected void onDestroy() {
343        super.onDestroy();
344        mapView.onDestroy();
345    }
346
347    @Override
348    public void onLowMemory() {
349        super.onLowMemory();
350        mapView.onLowMemory();
351    }
352}

The given code is an Android activity that demonstrates how to customize the pulsing circle of the location component on a map. The activity allows the user to change the duration and color of the pulsing circle.

initMapView:

  1. The onCreate method initializes the mapView by finding the view from the layout file and calling onCreate on it.

  2. The onMapReady method is implemented to handle the map when it is ready for use.

  3. The mapView is initialized by calling getMapAsync and passing the activity itself as the callback.

handle Location Permissions:

  1. The checkPermissions method checks if location permissions are granted. If permissions are granted, it calls getMapAsync to initialize the map.

  2. If permissions are not granted, it creates a PermissionsManager and requests location permissions.

  3. The PermissionsListener interface is implemented to handle the result of the permission request.

track the current location automatically when the map is ready:

  1. The onMapReady method is called when the map is ready.

  2. It sets the map style using the setStyle method, passing the desired style and a callback for when the style is loaded.

  3. The locationComponent is obtained from the nextbillionMap object, and location component options are built.

  4. The location component is activated and enabled, and the camera mode is set to tracking.

  5. The last known location is forced to update if available.

customize the location pulsing style:

  1. The buildLocationComponentOptions method is used to build the options for the location component.

  2. It takes the pulsing circle color and duration as parameters and sets the desired properties for the location component options.

  3. The setNewLocationComponentOptions method is called to update the location component options with new pulsing duration and color.

  4. The following properties are adjusted to customize the example's pulsing circle UI:

    1. DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS: The default duration in milliseconds for the pulsing circle animation.

    2. SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS: The duration in milliseconds for the second pulsing circle animation.

    3. THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS: The duration in milliseconds for the third pulsing circle animation.

    4. DEFAULT_LOCATION_CIRCLE_PULSE_RADIUS: The default radius of the pulsing circle.

    5. DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA: The default alpha value of the pulsing circle.

    6. DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE: The default interpolator for the pulsing circle animation.

    7. DEFAULT_LOCATION_CIRCLE_PULSE_FADE_MODE: The default fade mode for the pulsing circle animation.

The color, duration and other properties of the pulsing circle can be changed by interacting with buttons and dialog menus in the activity. The onOptionsItemSelected method handles the button clicks and updates the location component accordingly.

DIDN'T FIND WHAT YOU LOOKING FOR?