メインコンテンツまでスキップ
For the complete documentation index, see llms.txt.

Custom screens

This guide shows how to create a custom Android Auto screen by extending the Screen class and returning a template from onGetTemplate(). Use Mapbox components to add map rendering, camera behavior, and map controls without implementing them yourself.

This guide combines three modes in one screen:

  • Free Drive: Follow the user's location without a route.
  • Route Preview: Display a route on the map and let the user start navigation.
  • Active Guidance: Show turn-by-turn instructions and a stop button.

It requires Car API Level 7 or higher because route preview uses MapWithContentTemplate. To keep the example small, it previews one route at a time and leaves search and route fetching to your app.

Create a screen​

The screen uses the MapboxCarMap and MapboxNavigation already configured by your app. Map observers provide the camera, route line, guidance markers, and navigation information. Register them while the screen is resumed and unregister them when it pauses.

CustomNavigationScreen.kt
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.annotations.RequiresCarApi
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.mapbox.maps.extension.androidauto.MapboxCarMap
import com.mapbox.maps.extension.androidauto.MapboxCarMapObserver
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.ui.androidauto.action.MapboxMapActionStrip
import com.mapbox.navigation.ui.androidauto.navigation.CarActiveGuidanceMarkers
import com.mapbox.navigation.ui.androidauto.navigation.CarCameraMode
import com.mapbox.navigation.ui.androidauto.navigation.CarNavigationCamera
import com.mapbox.navigation.ui.androidauto.navigation.CarNavigationInfoProvider
import com.mapbox.navigation.ui.androidauto.navigation.CarNavigationTemplates
import com.mapbox.navigation.ui.androidauto.preview.CarRouteLineRenderer
import com.mapbox.navigation.ui.androidauto.preview.CarRouteLineRendererOptions
import com.mapbox.navigation.ui.androidauto.routes.CarRoutesProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow

@RequiresCarApi(7)
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class CustomNavigationScreen(
carContext: CarContext,
private val mapboxCarMap: MapboxCarMap,
private val mapboxNavigation: MapboxNavigation,
) : Screen(carContext) {
private enum class Mode { FREE_DRIVE, PREVIEW, ACTIVE_GUIDANCE }

private var mode = Mode.FREE_DRIVE
private val routes = MutableStateFlow<List<NavigationRoute>>(emptyList())
private val routesProvider = object : CarRoutesProvider {
override val navigationRoutes: Flow<List<NavigationRoute>> = routes
}
private val camera = CarNavigationCamera(
initialCarCameraMode = CarCameraMode.FOLLOWING,
alternativeCarCameraMode = CarCameraMode.OVERVIEW,
carRoutesProvider = routesProvider,
)
private val mapActionStrip = MapboxMapActionStrip(this, camera)
private val navigationInfo = CarNavigationInfoProvider().invalidateOnChange(this)
private val mapObservers: List<MapboxCarMapObserver> = listOf(
camera,
CarRouteLineRenderer(
options = CarRouteLineRendererOptions.Builder().build(),
carRoutesProvider = routesProvider,
onRouteClick = {},
),
CarActiveGuidanceMarkers(routesProvider),
navigationInfo,
)

init {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
mapObservers.forEach(mapboxCarMap::registerObserver)
mapboxCarMap.setGestureHandler(camera.gestureHandler)
}

override fun onPause(owner: LifecycleOwner) {
mapboxCarMap.setGestureHandler(null)
mapObservers.forEach(mapboxCarMap::unregisterObserver)
}
})
}

// Call on the main thread after your app fetches a route, before guidance starts.
fun showPreview(route: NavigationRoute) {
routes.value = listOf(route)
mode = Mode.PREVIEW
camera.updateCameraMode(CarCameraMode.OVERVIEW)
invalidate()
}

private fun startGuidance() {
mapboxNavigation.setNavigationRoutes(routes.value)
mode = Mode.ACTIVE_GUIDANCE
camera.updateCameraMode(CarCameraMode.FOLLOWING)
invalidate()
}

private fun returnToFreeDrive() {
mapboxNavigation.setNavigationRoutes(emptyList())
routes.value = emptyList()
mode = Mode.FREE_DRIVE
camera.updateCameraMode(CarCameraMode.FOLLOWING)
invalidate()
}

override fun onGetTemplate(): Template = when (mode) {
Mode.FREE_DRIVE -> CarNavigationTemplates.freeDrive(
actions("Follow") { camera.updateCameraMode(CarCameraMode.FOLLOWING) },
mapActionStrip.build(),
)
Mode.PREVIEW -> previewTemplate()
Mode.ACTIVE_GUIDANCE -> CarNavigationTemplates.activeGuidance(
navigationInfo.carNavigationInfo.value,
actions("Stop", ::returnToFreeDrive),
mapActionStrip.build(),
)
}

private fun previewTemplate(): Template {
val route = routes.value.first().directionsRoute
val row = Row.Builder()
.setTitle("Start navigation")
.addText("${(route.duration() / 60).toInt()} min")
.setOnClickListener { startGuidance() }
.build()
val content = ListTemplate.Builder()
.setHeader(Header.Builder().setTitle("Route preview").build())
.setSingleList(ItemList.Builder().addItem(row).build())
.build()

return MapWithContentTemplate.Builder()
.setContentTemplate(content)
.setActionStrip(actions("Cancel", ::returnToFreeDrive))
.setMapController(
MapController.Builder().setMapActionStrip(mapActionStrip.build()).build()
)
.build()
}

private fun actions(title: String, onClick: () -> Unit): ActionStrip =
ActionStrip.Builder()
.addAction(Action.Builder().setTitle(title).setOnClickListener { onClick() }.build())
.build()
}

showPreview() changes only the routes displayed on the map. Selecting Start navigation passes the route to MapboxNavigation and switches to active guidance. CarNavigationInfoProvider supplies guidance information and invalidates the screen when it changes. Cancel and Stop clear the route and return to free drive without stopping location updates.

Register the screen​

In your session, register a MapboxScreenFactory after calling prepareScreens(). Replace NAVIGATION on car systems that support Car API Level 7 or higher, and leave the default screens in place on older car systems. Pass the same map and navigation instances used by your session:

import com.mapbox.maps.extension.androidauto.MapboxCarMap
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.ui.androidauto.MapboxCarContext
import com.mapbox.navigation.ui.androidauto.screenmanager.MapboxScreen
import com.mapbox.navigation.ui.androidauto.screenmanager.MapboxScreenFactory
import com.mapbox.navigation.ui.androidauto.screenmanager.prepareScreens

// Properties inside your Session class.
private val mapboxCarMap = MapboxCarMap()
private var customNavigationScreen: CustomNavigationScreen? = null

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
private val mapboxCarContext = MapboxCarContext(lifecycle, mapboxCarMap)
.prepareScreens()
.apply {
mapboxScreenManager[MapboxScreen.NAVIGATION] = MapboxScreenFactory { carContext ->
check(carContext.supportsUnifiedNavigation())
CustomNavigationScreen(
carContext,
mapboxCarMap,
checkNotNull(MapboxNavigationApp.current()),
).also { customNavigationScreen = it }
}
}

Keep your session's existing map setup, navigation lifecycle management, and location permission checks. Initialize MapboxNavigationApp, attach the session, and start a trip session before using guidance. This screen does not initialize the map or start a trip session. See Get Started for session setup.

After location permission is granted, select NAVIGATION when carContext.supportsUnifiedNavigation() is true, or FREE_DRIVE otherwise, as shown in the Navigation guide.

When your app receives a route, call customNavigationScreen?.showPreview(route) on the main thread. This requires the screen to have been created first. Search, route requests, and delivery of their results are app responsibilities.

For brevity, this example omits alternative routes, arrival handling, voice guidance controls, and synchronization with navigation started elsewhere in your app. In a production app, also keep the displayed routes synchronized with reroutes and handle Android Auto's template-type restrictions when returning from other screens.

この{Type}は役に立ちましたか?