Customization

Nextbillion.AI Navigation SDK provides a wide range of customization options for developers to tailor the navigation experience to their specific needs. One way to achieve this is by using the default drop-in navigation activity that is launched using NavigationLauncher and NavLauncherConfig. However, for more advanced use cases, developers can also build their own custom navigation view by embedding the NavigationView into their own Activity or Fragment. In this article, we will be discussing how to create a customized navigation experience by building a custom navigation view.

NavigationView is the key component of the navigation, we can either add it to the layout XML file or add it to ViewGroup programmatically

1
<ai.nextbillion.navigation.ui.NavigationView
2
android:id="@+id/navigation_view"
3
android:layout_width="match_parent"
4
android:layout_height="match_parent"/>

The code above is an example of how to add the NavigationView component to an XML layout file. The NavigationView class is a custom view class provided by Nextbillion.AI Navigation SDK, it is responsible for displaying the navigation map and all related information, such as the route, user location, and instructions. The NavigationView can be added to any layout file by using the <ai.nextbillion.navigation.ui.NavigationView> tag and setting the width and height to match the parent container. It is important to note that the id of the NavigationView is set to @+id/navigation_view this will be used to reference the view in the Java code.

Lifecycle Binding

In order to ensure proper initialization and resource management, as well as handling user interactions, the NavigationView component must be bound to the activity's lifecycle using a series of callbacks. This is achieved by binding the NavigationView's lifecycle callbacks to the corresponding callbacks in the activity. This allows the NavigationView to properly handle the activity's lifecycle transitions and respond accordingly. Following are the callbacks included:

1onCreate
2onStart
3onResume
4onPause
5onStop
6onDestroy
7onLowMemory
8onBackPressed
9onSaveInstanceState
10onRestoreInstanceState
1
@Override
2
protected void onCreate(@Nullable Bundle savedInstanceState) {
3
...
4
navigationView = findViewById(R.id.navigationView);
5
navigationView.onCreate(savedInstanceState);
6
...
7
}
8
9
@Override
10
public void onStart() {
11
super.onStart();
12
navigationView.onStart();
13
}
14
15
@Override
16
public void onResume() {
17
super.onResume();
18
navigationView.onResume();
19
}
20
21
@Override
22
public void onLowMemory() {
23
super.onLowMemory();
24
navigationView.onLowMemory();
25
}
26
27
@Override
28
public void onBackPressed() {
29
// If the navigation view didn't need to do anything, call super
30
if (!navigationView.onBackPressed()) {
31
super.onBackPressed();
32
}
33
}
34
35
@Override
36
protected void onSaveInstanceState(Bundle outState) {
37
navigationView.onSaveInstanceState(outState);
38
super.onSaveInstanceState(outState);
39
}
40
41
@Override
42
protected void onRestoreInstanceState(Bundle savedInstanceState) {
43
super.onRestoreInstanceState(savedInstanceState);
44
navigationView.onRestoreInstanceState(savedInstanceState);
45
}
46
47
48
@Override
49
public void onPause() {
50
super.onPause();
51
navigationView.onPause();
52
}
53
54
@Override
55
public void onStop() {
56
super.onStop();
57
navigationView.onStop();
58
}
59
60
@Override
61
protected void onDestroy() {
62
super.onDestroy();
63
navigationView.onDestroy();
64
}

By calling these methods on the NavigationView in the corresponding activity lifecycle methods, the developer can ensure that the NavigationView is properly set up and taken down, and that it responds to user interactions in a way that is consistent with the activity's state.

Initialization

The NavigationView component must be fully initialized before starting navigation. To ensure this, the initialize method can be used with an OnNavigationReadyCallback to perform any necessary actions, such as route fetching or starting a navigation, once the NavigationView is ready. The onNavigationReady method within the callback will be triggered once the NavigationView is fully initialized and ready to be used, and it will also indicate if a navigation is already running or not. Before we start navigation, we need to make sure that the NavigationView is completely initialized, it's recommended to perform startNavigation in an OnNavigationReadyCallback,

1
navigationView.initialize(new OnNavigationReadyCallback() {
2
@Override
3
public void onNavigationReady(boolean isRunning) {
4
//Perform route fetching or start a navigation here
5
}
6
});
7

Start a Navigation

To start a navigation session in the NavigationView, we need to provide it with a NavViewConfig that contains a DirectionsRoute. The following example shows how to create a NavViewConfig and start a navigation:

1
NavViewConfig.Builder config =
2
NavViewConfig.builder().route(directionsRoute);
3
navigationView.startNavigation(config.build());

The NavViewConfig builder also allows developers to customize various components of the navigation experience and register listeners for events related to the navigation engine. Developers can use the NavViewConfig to customize the appearance and behavior of the NavigationView, such as the route, simulate navigation, wayname chip, location layer render mode, show speedometer, and more. Additionally, developers can register listeners for events such as route updates, navigation status, progress changes, milestone events, speech announcements, and more.

for more details, please refer to Android Navigation SDK Configurations

Set Theme

The Navigation SDK allows developers to customize the theme of the NavigationView by providing two default styles, NavigationViewDark and NavigationViewLight. These styles can be extended and modified by creating custom styles, such as CustomNavigationViewLight and CustomNavigationViewDark, and then applying them to the NavigationView in the layout XML file using the navigationDarkTheme and navigationLightTheme attributes. This allows developers to tailor the look and feel of the NavigationView to match their app's branding and design.

1
<ai.nextbillion.navigation.ui.NavigationView
2
android:id='@+id/navigationView'
3
android:layout_width='match_parent'
4
android:layout_height='match_parent'
5
app:navigationDarkTheme='@style/NavigationViewDark'
6
app:navigationLightTheme='@style/NavigationViewLight'
7
/>

The Navigation SDK has provided two default styles

  1. NavigationViewDark

  2. NavigationViewLight

to customize them, we can declare our own styles by extending and modifying the default styles.

1
<style name='CustomNavigationViewLight' parent='NavigationViewLight'>
2
...
3
</style>
4
5
<style name='CustomNavigationViewDark' parent='NavigationViewDark'>
6
...
7
</style>

and update the layout file:

1
<ai.nextbillion.navigation.ui.NavigationView
2
android:id='@+id/navigationView'
3
android:layout_width='match_parent'
4
android:layout_height='match_parent'
5
app:navigationDarkTheme='@style/CustomNavigationViewDark'
6
app:navigationLightTheme='@style/CustomNavigationViewLight'
7
/>

Custom Navigation Notification

The Nextbillion Navigation SDK displays an ongoing notification during navigation. The default notification style is a bright background with black text for light themes and a dark background with white text for dark themes. However, developers can customize the notification style by implementing a custom NavigationNotification and registering it in the NavEngineConfig. The custom NavEngineConfig should then be passed to the SDK through the NavViewConfig when starting navigation.

1
public interface NavigationNotification {
2
3
/**
4
* A Notification to start NavigationService as a foreground service
5
*/
6
Notification getNotification();
7
8
/**
9
* An integer id that will be used to start this notification
10
*/
11
int getNotificationId();
12
13
/**
14
* This method will be called every time the NavProgress is updated
15
*/
16
void updateNotification(NavProgress routeProgress);
17
18
/**
19
* Will be triggered when {@link NextbillionNav#stopNavigation()} is called.
20
*/
21
void onNavigationStopped(Context context);
22
}

Step 1

Implements NavigationNotification

1
public class CustomNavigationNotification implements NavigationNotification {
2
3
public CustomNavigationNotification(Context applicationContext) {
4
5
}
6
7
@Override
8
public Notification getNotification() {
9
return null;
10
}
11
12
@Override
13
public int getNotificationId() {
14
return 0;
15
}
16
17
@Override
18
public void updateNotification(NavProgress routeProgress) {
19
20
}
21
22
@Override
23
public void onNavigationStopped(Context context) {
24
25
}
26
27
}

Step 2

Build notification view

1
public class CustomNavigationNotification implements NavigationNotification {
2
3
private static final int CUSTOM_NOTIFICATION_ID = 11112;
4
private static final String STOP_NAVIGATION_ACTION = "stop_navigation_action";
5
6
private final Notification customNotification;
7
private final NotificationCompat.Builder customNotificationBuilder;
8
private final NotificationManager notificationManager;
9
10
public CustomNavigationNotification(Context applicationContext) {
11
notificationManager = (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE);
12
13
customNotificationBuilder = new NotificationCompat.Builder(applicationContext, NAVIGATION_NOTIFICATION_CHANNEL)
14
.setSmallIcon(R.drawable.ic_navigation)
15
.setContentTitle("Customised Notification")
16
.setContentText("Ongoing Navigation!");
17
18
customNotification = customNotificationBuilder.build();
19
}
20
21
@Override
22
public Notification getNotification() {
23
return customNotification;
24
}
25
26
@Override
27
public int getNotificationId() {
28
return CUSTOM_NOTIFICATION_ID;
29
}
30
31
@Override
32
public void updateNotification(NavProgress routeProgress) {
33
34
}
35
36
@Override
37
public void onNavigationStopped(Context context) {
38
notificationManager.cancel(CUSTOM_NOTIFICATION_ID);
39
}
40
}

To display the notification, we need to finish step 3 as well.

Step 3

Notification update

1
public class CustomNavigationNotification implements NavigationNotification {
2
3
...
4
5
private int numberOfUpdates;
6
7
...
8
9
@Override
10
public void updateNotification(NavProgress routeProgress) {
11
// Update the builder with a new number of updates
12
customNotificationBuilder.setContentText("Number of updates: " + numberOfUpdates++);
13
14
notificationManager.notify(CUSTOM_NOTIFICATION_ID, customNotificationBuilder.build());
15
}
16
17
...
18
}

The updateNotification method will be called every time the NavProgress is updated, in this method, we can perform UI updates.

Step 4 (Optional)

A button to stop the navigation The key to declaring a stop button is to create a pending intent to broadcast the event

  1. create a pending intent and set it as the content intent of the notification builder.

  2. a method to register broadcast receiver

  3. a method to unregister broadcast receiver

  4. call register method in initialize phase, for example. the constructor of the CustomNavigationNotification

  5. call unregister method when the navigation is stopped.

1
public class CustomNavigationNotification implements NavigationNotification {
2
3
...
4
private static final String STOP_NAVIGATION_ACTION = "stop_navigation_action";
5
6
private BroadcastReceiver stopNavigationReceiver;
7
8
...
9
10
public CustomNavigationNotification(Context applicationContext) {
11
...
12
customNotificationBuilder = new NotificationCompat.Builder(applicationContext, NAVIGATION_NOTIFICATION_CHANNEL)
13
...
14
.setContentIntent(createPendingStopIntent(applicationContext));
15
16
register(stopNavigationReceiver, applicationContext);
17
...
18
}
19
20
...
21
22
@Override
23
public void onNavigationStopped(Context context) {
24
context.unregisterReceiver(stopNavigationReceiver);
25
...
26
}
27
28
public void register(BroadcastReceiver stopNavigationReceiver, Context applicationContext) {
29
this.stopNavigationReceiver = stopNavigationReceiver;
30
applicationContext.registerReceiver(stopNavigationReceiver, new IntentFilter(STOP_NAVIGATION_ACTION));
31
}
32
33
private PendingIntent createPendingStopIntent(Context context) {
34
Intent stopNavigationIntent = new Intent(STOP_NAVIGATION_ACTION);
35
return PendingIntent.getBroadcast(context, 0, stopNavigationIntent, 0);
36
}
37
}

Full example

1
public class CustomNavigationNotification implements NavigationNotification {
2
3
4
private static final int CUSTOM_NOTIFICATION_ID = 11112;
5
private static final String STOP_NAVIGATION_ACTION = "stop_navigation_action";
6
7
8
private final Notification customNotification;
9
private final NotificationCompat.Builder customNotificationBuilder;
10
private final NotificationManager notificationManager;
11
private BroadcastReceiver stopNavigationReceiver;
12
private int numberOfUpdates;
13
14
15
public CustomNavigationNotification(Context applicationContext) {
16
notificationManager = (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE);
17
18
19
customNotificationBuilder = new NotificationCompat.Builder(applicationContext, NAVIGATION_NOTIFICATION_CHANNEL)
20
.setSmallIcon(R.drawable.ic_navigation)
21
.setContentTitle("Custom Navigation Notification")
22
.setContentText("Display your own content here!")
23
.setContentIntent(createPendingStopIntent(applicationContext));
24
25
customNotification = customNotificationBuilder.build();
26
register(stopNavigationReceiver, applicationContext);
27
}
28
29
30
@Override
31
public Notification getNotification() {
32
return customNotification;
33
}
34
35
36
@Override
37
public int getNotificationId() {
38
return CUSTOM_NOTIFICATION_ID;
39
}
40
41
42
@Override
43
public void updateNotification(NavProgress routeProgress) {
44
// Update the builder with a new number of updates
45
customNotificationBuilder.setContentText("Number of updates: " + numberOfUpdates++);
46
47
48
notificationManager.notify(CUSTOM_NOTIFICATION_ID, customNotificationBuilder.build());
49
}
50
51
52
@Override
53
public void onNavigationStopped(Context context) {
54
context.unregisterReceiver(stopNavigationReceiver);
55
notificationManager.cancel(CUSTOM_NOTIFICATION_ID);
56
}
57
58
59
public void register(BroadcastReceiver stopNavigationReceiver, Context applicationContext) {
60
this.stopNavigationReceiver = stopNavigationReceiver;
61
applicationContext.registerReceiver(stopNavigationReceiver, new IntentFilter(STOP_NAVIGATION_ACTION));
62
}
63
64
private PendingIntent createPendingStopIntent(Context context) {
65
Intent stopNavigationIntent = new Intent(STOP_NAVIGATION_ACTION);
66
return PendingIntent.getBroadcast(context, 0, stopNavigationIntent, 0);
67
}
68
}

The CustomNavigationNotification class is an implementation of the NavigationNotification interface, it allows developers to customize the style of the ongoing notification displayed during navigation. It creates a NotificationCompat.Builder with a custom small icon, title, and text, and assigns it to a Notification object. The updateNotification method updates the notification's text to display the number of updates. The getNotificationId method returns a unique integer identifier for the notification, and onNavigationStopped method cancels the notification and unregisters the BroadcastReceiver when navigation is stopped. The register method is used to register the stopNavigationReceiver to listen for the STOP_NAVIGATION_ACTION intent. A PendingIntent is created with the STOP_NAVIGATION_ACTION intent, which is set as the content intent of the notification.

Custom Speechplayer

Overview

NavViewConfig provides a field that allows developers to customize a speech player. In this article, we are going to cover how to implement a customized speed player with the following steps:

  1. init a player

  2. implement announcement playing method

  3. request audio focus when playing voice instructions

Interface

SpeechPlayer is an interface that provides a way for the SDK to play voice guidance to the user during navigation. The interface has several methods that can be implemented to control the behavior of the speech player.

  • The play() method is used to play a given speech announcement, multiple speech announcements will be queued in a first-in-first-out (FIFO) fashion.

  • The isMuted() method is used to determine whether the speech player is currently muted or not.

  • The setMuted() method is used to cancel the currently playing announcement immediately and clear any queued announcements if any.

  • The onOffRoute() method is used to stop and release the media if needed, or play voice guidance to notify users about the off-route event.

  • The onDestroy() method is used to stop and release the media if needed.

  • The isSpeaking() method is used to determine whether the speech player is currently playing an announcement or not.

  • The stop() method is used to stop the speech player if it is playing.

1
public interface SpeechPlayer {
2
3
/**
4
* Will play the given string speechAnnouncement. multiple speechAnnouncement will be queued in FIFO fashion.
5
*/
6
void play(SpeechAnnouncement speechAnnouncement);
7
8
/**
9
* determine whether the speechPlayer is muted or not
10
*/
11
boolean isMuted();
12
13
/**
14
* cancel currently playing announcement immediately,
15
* and clear queued announcements if any.
16
*/
17
void setMuted(boolean isMuted);
18
19
/**
20
* Stop the current announcement if playing.
21
* or play voice guidance to notify users about the offroute event.
22
*/
23
void onOffRoute();
24
25
/**
26
* Used to stop and release the media (if needed).
27
*/
28
void onDestroy();
29
30
/**
31
* determine whether the speechPlayer is currently playing an announcement or not
32
*/
33
boolean isSpeaking();
34
35
/**
36
* stop the SpeechPlayer if playing.
37
*/
38
void stop();
39
}

Overall, it allows the developer to customize the audio guidance for the user by providing their own implementation of this interface.

Init a player

In Android we can use different players to play guidance, for example - TextToSpeech or MediaPlayer.

In this example, we are going to use TextToSpeech.

1
import android.speech.tts.TextToSpeech;
2
3
class AndroidSpeechPlayer implements SpeechPlayer {
4
private final TextToSpeech textToSpeech;
5
private boolean speechHasInit = false;
6
private boolean languageSupported = false;
7
private boolean isMuted;
8
9
AndroidSpeechPlayer(Context context, final String language) {
10
textToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
11
@Override
12
public void onInit(int status) {
13
boolean ableToInitialize = status == TextToSpeech.SUCCESS && language != null;
14
if (!ableToInitialize) {
15
return;
16
}
17
speechHasInit = true;
18
setLanguage(new Locale(language));
19
}
20
});
21
}
22
23
24
private void setLanguage(Locale language) {
25
boolean isLanguageAvailable = textToSpeech.isLanguageAvailable(language) == TextToSpeech.LANG_AVAILABLE;
26
if (!isLanguageAvailable) {
27
Log.w("The specified language is not supported by TTS");
28
return;
29
}
30
languageSupported = true;
31
textToSpeech.setLanguage(language);
32
}
33
...
34
}
35

The AndroidSpeechPlayer class is an implementation of the SpeechPlayer interface. It uses the TextToSpeech class to play speech announcements. The class takes in a Context and a language string in its constructor and initializes the TextToSpeech object with the specified language. The class also has methods to check whether the speech player is muted, set the language, play speech announcements, stop the speech player, check if the player is speaking and other methods that follow the SpeechPlayer interface.

Implement play announcement

1
@Override
2
public void play(SpeechAnnouncement speechAnnouncement) {
3
boolean isValidAnnouncement = speechAnnouncement != null
4
&& !TextUtils.isEmpty(speechAnnouncement.announcement());
5
boolean canPlay = isValidAnnouncement && languageSupported && !isMuted;
6
if (!canPlay) {
7
return;
8
}
9
10
11
HashMap<String, String> params = new HashMap<>(1);
12
params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, DEFAULT_UTTERANCE_ID);
13
textToSpeech.speak(speechAnnouncement.announcement(), TextToSpeech.QUEUE_ADD, params);
14
}
15
16
17
@Override
18
public boolean isMuted() {
19
return isMuted;
20
}
21
22
23
@Override
24
public void setMuted(boolean isMuted) {
25
this.isMuted = isMuted;
26
if (isMuted) {
27
muteTts();
28
}
29
}
30
31
@Override
32
public void onOffRoute() {
33
stop();
34
}
35
36
37
@Override
38
public void onDestroy() {
39
if (textToSpeech != null) {
40
textToSpeech.stop();
41
textToSpeech.shutdown();
42
}
43
}
44
45
46
@Override
47
public boolean isSpeaking() {
48
if (textToSpeech != null) {
49
return textToSpeech.isSpeaking();
50
}
51
return false;
52
}
53
54
55
@Override
56
public void stop() {
57
if (textToSpeech != null && textToSpeech.isSpeaking()) {
58
textToSpeech.stop();
59
}
60
}

The above code demonstrates an example implementation of the SpeechPlayer interface, including methods for playing announcements, muting the speech player, handling off-route events, cleaning up resources, and checking the speaking status of the speech player.

Handle Audio Focus

Two or more Android apps can play audio to the same output stream simultaneously, and the system mixes everything together. While this is technically impressive, it can be very aggravating to a user. To avoid every music app playing at the same time, Android introduces the idea of audio focus. Only one app can hold audio focus at a time.

More details at https://developer.android.com/guide/topics/media-apps/audio-focus

Voice guidance is important to drivers, it's necessary to obtain the audio focus when we play voice guidance. Add the code below to our customised speech player:

1
2
3
private AudioManager audioManager;
4
private AudioFocusRequest audioFocusRequest;
5
6
...
7
8
private void initAudioManager(Context context) {
9
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
10
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
11
audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK).build();
12
}
13
}
14
15
private void requestFocus() {
16
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
17
audioManager.requestAudioFocus(audioFocusRequest);
18
} else {
19
audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC,
20
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
21
}
22
}
23
24
private void abandonFocus() {
25
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
26
audioManager.abandonAudioFocusRequest(audioFocusRequest);
27
} else {
28
audioManager.abandonAudioFocus(null);
29
}
30
}

And then we can modify the constructor:

1
AndroidSpeechPlayer(Context context, final String language) {
2
initAudioManager(context);
3
textToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
4
@Override
5
public void onInit(int status) {
6
boolean ableToInitialize = status == TextToSpeech.SUCCESS && language != null;
7
if (!ableToInitialize) {
8
Timber.e("There was an error initializing native TTS");
9
return;
10
}
11
setLanguage(new Locale(language));
12
speechHasInit = true;
13
textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
14
@Override
15
public void onStart(String s) {
16
requestFocus();
17
}
18
19
20
@Override
21
public void onDone(String s) {
22
abandonFocus();
23
}
24
25
26
@Override
27
public void onError(String s) {
28
29
30
}
31
});
32
}
33
});
34
}

The above code creates an instance of the Android built-in Text-To-Speech (TTS) engine and uses the AudioManager to request and abandon audio focus from the Android system.

To handle audio focus properly, we need to create an UtteranceProgressListener to hook audio focus handling into the TTS,

© 2024 NextBillion.ai all rights reserved.