Create a Custom Theme

Last edit: 2024.05.29

Knowledge of the theming will help you to understand this guide. If you are not yet familiar with it, feel free to take a look at the theming-and-customization.

To provide your own theme to the system, you need to create your style resources which define the theme attributes used by the system. Additional to TomTom Digital Cockpit stock theme, if you want an alternative font theme for example, you can also provide your own theme to the system. To do that, you need to create your styles and IviThemeRegistrySources which providing IviThemeComponents that referring to these style resources.

This tutorial shows how to create a custom color theme to the system step by step. For other theme categories mentioned in Theme categories, we follow the same steps to implement them.

The complete source for this example can be found in the following directory in the examples source: examples/theming/

Create a custom theme component and its flavors

For the custom color theme, we also want to provide light and dark flavors. Let's start from creating a CustomThemeCategoryStylingFlavor first.

In src/main/kotlin/com/example/ivi/example/theming/CustomThemeCategoryFlavors.kt

1@OptIn(IviExperimental::class)
2internal interface CustomThemeCategoryStylingFlavor : IviThemeCategoryStylingFlavor {
3 val componentId: String
4
5 val styleFlavorName: String
6
7 override val label: StringResolver
8 get() = StaticStringResolver(styleFlavorName)
9}

CustomThemeCategoryStylingFlavor is an IviThemeCategoryStylingFlavor and is used to define enum classes to represent theme flavors for theme categories. The color flavors are defined like this:

1enum class CustomColorThemeCategoryStylingFlavor(
2 override val componentId: String,
3 override val styleFlavorName: String,
4) : CustomThemeCategoryStylingFlavor {
5
6 /**
7 * The custom Light color flavor.
8 */
9 CUSTOM_LIGHT("customlight", "Custom Light"),
10
11 /**
12 * The custom Dark color flavor.
13 */
14 CUSTOM_DARK("customdark", "Custom Dark"),
15}

Then we start to declare the custom color theme components with flavors in src/main/kotlin/com/example/ivi/example/theming/CustomThemeComponents.kt:

1@OptIn(IviExperimental::class)
2val customColorThemeComponents:
3 Set<IviThemeComponent.WithStylingFlavorType<CustomColorThemeCategoryStylingFlavor>> =
4 colorThemeCategory.createCustomThemeComponents()

customColorThemeComponents is a Set of IviThemeComponent, which has the style flavor CustomColorThemeCategoryStylingFlavor. Two IviThemeComponents are created for colorThemeCategory, one for CustomColorThemeCategoryStylingFlavor.CUSTOM_LIGHT and the other for CustomColorThemeCategoryStylingFlavor.CUSTOM_DARK. To reduce boilerplate code for different IviThemeCategorys, we implement an extension method: createCustomThemeComponents in src/main/kotlin/com/example/ivi/example/theming/IviThemeCategoryExtensions.kt:

1/**
2 * Creates [IviThemeComponent]s for each [CustomThemeCategoryStylingFlavor] option that belongs to
3 * the receiver [IviThemeCategory].
4 */
5@OptIn(IviExperimental::class)
6internal inline fun <reified SF> IviThemeCategory.createCustomThemeComponents():
7 Set<IviThemeComponent.WithStylingFlavorType<SF>>
8 where SF : Enum<SF>, SF : CustomThemeCategoryStylingFlavor =
9 createComponents {
10 formatComponentId(id, "custom.${it.componentId}")
11 }
12
13internal fun formatComponentId(categoryId: String, componentName: String): String =
14 listOf(
15 "com.example.ivi.example.themecomponent",
16 categoryId,
17 componentName,
18 ).joinToString(".") { it.lowercase() }

Create a theme registry source

IviThemeRegistrySources provide IviThemeComponents that form an IviTheme. To provide custom themes, we need to define them so the system can collect theme components from these sources. Let's start to create a core theme registry source first. Core theme components define the styles which are general for each IVI domain. For example, the custom color theme component registered in this source defines the colors that can be used by each IVI domain.

In src/main/kotlin/com/example/ivi/example/theming/CustomCoreComponentsThemeRegistrySource.kt:

1@OptIn(IviExperimental::class)
2internal class CustomCoreComponentsThemeRegistrySource(
3 registrySourceContext: IviThemeRegistrySourceContext,
4) : IviThemeRegistrySource.Components(registrySourceContext) {
5
6 override suspend fun IviThemeCategorySetConfigurator.applyConfig() {
7 // `applyConfig` will be invoked by the system to collect theme components. This is the only
8 // method we need to override. The details will be explained later.
9 }
10}

Add a theme registry source to the system

Once an IviThemeRegistrySource is defined, we need to add it to the system. Each IviThemeRegistrySource is built by an IviThemeRegistrySourceBuilder. So we need to define one for CustomCoreComponentsThemeRegistrySource.

In src/main/kotlin/com/example/ivi/example/theming/CustomCoreComponentsThemeRegistrySourceBuilder.kt:

1OptIn(IviExperimental::class)
2class CustomCoreComponentsThemeRegistrySourceBuilder : IviThemeRegistrySourceBuilder {
3
4 override fun build(
5 registrySourceContext: IviThemeRegistrySourceContext,
6 ): IviThemeRegistrySource =
7 CustomCoreComponentsThemeRegistrySource(registrySourceContext)
8}

IviThemeRegistrySourceBuilder is referred by the Gradle API: IviThemeRegistrySourceConfig. It can be defined in the top-level Gradle file, for example themeregistrysources.gradle.kts.

1// In this tutorial, the theming module is implemented by `examples_theming`.
2private val themingModule = ExampleModuleReference("examples_theming")
3
4// To allow the Gradle's extra extensions to work, the properties in this file need to use the
5// `by extra` delegation to assign the value.
6val customCoreComponentsThemeRegistrySource by extra {
7 IviThemeRegistrySourceConfig(
8 registrySourceBuilderName = "CustomCoreComponentsThemeRegistrySourceBuilder",
9 implementationModule = themingModule,
10 )
11}

The properties defined in this file can be accessed in the build.gradle.kts by applying this file in it and by using Gradle's extra extension. For example,

1
2apply(from = rootProject.file("themeregistrysources.gradle.kts"))
3
4val customCoreComponentsThemeRegistrySource: IviThemeRegistrySourceConfig by project.extra
5
6iviInstances {
7 create(IviInstanceIdentifier.default) {
8 theming {
9 addRegistrySources(
10 customCoreComponentsThemeRegistrySource,
11 )
12 }
13 }
14}

Once customCoreComponentsThemeRegistrySource is added, the system knows how to get the theme registry source and collect theme components.

Add custom theme components to a theme category

Back to CustomCoreComponentsThemeRegistrySource, it provides custom theme components. Each theme component has its own corresponding style resource. First, let's define the custom color styles for light and dark mode. In src/main/res/values/themes_colors.xml, we define styles as below:

1<resources>
2 <!-- We define two styles here: CustomThemeColorDark and CustomThemeColorLight. They inherit
3 TomTom Digital Cockpit's stock color styles and only override tt_surface_color with new
4 color values. -->
5 <style name="CustomThemeColorDark" parent="TtiviThemeColorDark">
6 <item name="tt_surface_color">#006400</item>
7 </style>
8
9 <style name="CustomThemeColorLight" parent="TtiviThemeColorLight">
10 <item name="tt_surface_color">#D3D3D3</item>
11 </style>
12</resources>

Then in the override function applyConfig, we need to configure the color theme category with custom theme components. The word configure used here implies the actions to register custom theme components to a category, and apply style resource to the corresponding theme component.

1@OptIn(IviExperimental::class)
2internal class CustomCoreComponentsThemeRegistrySource(
3 registrySourceContext: IviThemeRegistrySourceContext,
4) : IviThemeRegistrySource.Components(registrySourceContext) {
5
6 override suspend fun IviThemeCategorySetConfigurator.applyConfig() {
7 configureColorThemeComponents()
8 }
9
10 private fun IviThemeCategorySetConfigurator.configureColorThemeComponents() {
11 // Configures the color category.
12 configure(colorThemeCategory) {
13 // Registers `customColorThemeComponents`.
14 registerComponents(customColorThemeComponents)
15 // To style the primary stylable resource in the color category.
16 toStyle(primaryStyleableResource) {
17 // For each custom color theme component(DARK and LIGHT in this example).
18 onComponents(customColorThemeComponents) {
19 when (component.stylingFlavor) {
20 // When Dark flavor
21 CustomColorThemeCategoryStylingFlavor.CUSTOM_DARK ->
22 // Applies `R.style.CustomThemeColorDark`.
23 applyStyle(R.style.CustomThemeColorDark)
24 // When Light flavor
25 CustomColorThemeCategoryStylingFlavor.CUSTOM_LIGHT ->
26 // Applies `R.style.CustomThemeColorLight`.
27 applyStyle(R.style.CustomThemeColorLight)
28 }
29 }
30 }
31 }
32 }
33}

Now, for colorThemeCategory, we have registered customColorThemeComponents and each theme component has its own style resource. But this is not enough. TomTom Digital Cockpit consists of various domains, such as media, communications, hvac etc.. The CustomCoreComponentsThemeRegistrySource is only for the core theme components. Core theme components define the styles which are general for each IVI domain. For specific domains, they also need their own theme registry sources which providing their own specific theme components. So we continue the similar process for communications domain. Other domains can follow the same implementation process. Like CustomCoreComponentsThemeRegistrySource, we need a CustomCommunicationsThemeRegistrySource for communications domain. In src/main/res/values/themes_colors_communications.xml, we define two color styles that are identical to TomTom Digital Cockpit's stock styles. You can change them to experiment the theming customization.

1<resources>
2 <style name="CustomThemeCommunicationsColorDark" parent="TtiviThemeCommunicationsColorDark">
3 <!-- The current style is identical to the stock one. For more customizations, re-assign
4 values to attributes defined in the parent style.-->
5 </style>
6
7 <style name="CustomThemeCommunicationsColorLight" parent="TtiviThemeCommunicationsColorLight">
8 <!-- The current style is identical to the stock one. For more customizations, re-assign
9 values to attributes defined in the parent style.-->
10 </style>
11</resources>

Just like CustomCoreComponentsThemeRegistrySource, we also need to define a CustomCommunicationsThemeRegistrySource. In this class, we need to extend customColorThemeComponents . The word extend, comparing configure used in CustomCoreComponentsThemeRegistrySource, means we need to extend customColorThemeComponents for the domain-specific stylable resource with the corresponding style resources. Note that customColorThemeComponents are already registered in CustomCoreComponentsThemeRegistrySource.

In src/main/kotlin/com/example/ivi/example/theming/CustomCommunicationsThemeRegistrySource.kt:

1@OptIn(IviExperimental::class)
2class CustomCommunicationsThemeRegistrySource(
3 registrySourceContext: IviThemeRegistrySourceContext,
4) : IviThemeRegistrySource.Components(registrySourceContext) {
5
6 override suspend fun IviThemeCategorySetConfigurator.applyConfig() {
7 extendColorThemeComponents()
8 }
9
10 private fun IviThemeCategorySetConfigurator.extendColorThemeComponents() {
11 // Configures the color category.
12 configure(colorThemeCategory) {
13 // To style Communications color stylable `TtiviCommunicationsThemeCategoryColors`.
14 toStyle(RAttr.styleable.TtiviCommunicationsThemeCategoryColors) {
15 // For each custom color theme component(DARK and LIGHT in this example).
16 onComponents(customColorThemeComponents) {
17 when (component.stylingFlavor) {
18 // When Dark flavor
19 CustomColorThemeCategoryStylingFlavor.CUSTOM_DARK ->
20 // Applies `R.style.CustomThemeCommunicationsColorDark`.
21 applyStyle(R.style.CustomThemeCommunicationsColorDark)
22 // When Light flavor
23 CustomColorThemeCategoryStylingFlavor.CUSTOM_LIGHT ->
24 // Applies `R.style.CustomThemeCommunicationsColorLight`.
25 applyStyle(R.style.CustomThemeCommunicationsColorLight)
26 }
27 }
28 }
29 }
30 }
31}

For CustomCommunicationsThemeRegistrySource, we also need a builder for it and add it to the system:

In src/main/kotlin/com/example/ivi/example/theming/CustomCommunicationsThemeRegistrySourceBuilder.kt:

1@OptIn(IviExperimental::class)
2class CustomCommunicationsThemeRegistrySourceBuilder : IviThemeRegistrySourceBuilder {
3
4 override fun build(
5 registrySourceContext: IviThemeRegistrySourceContext,
6 ): IviThemeRegistrySource =
7 CustomCommunicationsThemeRegistrySource(registrySourceContext)
8}

Then in themeregistrysources.gradle.kts:

1val customCommunicationsThemeRegistrySource by extra {
2 IviThemeRegistrySourceConfig(
3 registrySourceBuilderName = "CustomCommunicationsThemeRegistrySourceBuilder",
4 implementationModule = themingModule,
5 )
6}

And in build.gradle.kts:

1val customCommunicationsThemeRegistrySource: IviThemeRegistrySourceConfig by project.extra
2
3iviInstances {
4 create(IviInstanceIdentifier.default) {
5 theming {
6 addRegistrySources(
7 customCommunicationsThemeRegistrySource,
8 )
9 }
10 }
11}

For other domains, we follow the same implementation process as described above.

Select default theme components

Once you define your custom theme components, you may want to make them the default ones adopted by the system. To do so, we need an IviThemeComponentSelector. The theme components retrieved from the selector have higher priorities than others not defined in the selector. To simplify the implementation, TomTom Digital Cockpit provides an API toComponentSelector. It converts a Set of IviThemeComponents to an IviThemeComponentSelector. So first we need to declare the Set includes the default theme components.

In src/main/kotlin/com/example/ivi/example/theming/DefaultThemeComponentSelector.kt:

1@OptIn(IviExperimental::class)
2private val defaultThemeComponents: Set<IviThemeComponent> =
3 // The theme components defined below are selected as the default one per category.
4 setOf(
5 customColorThemeComponents[CustomColorThemeCategoryStylingFlavor.CUSTOM_DARK],
6 )

Then we can transform the Set into a selector.

@OptIn(IviExperimental::class)
internal val defaultThemeComponentsSelector = defaultThemeComponents.toComponentSelector()

The following steps are the same as IviThemeRegistrySource. First, we also need a builder for the selector.

In src/main/kotlin/com/example/ivi/example/theming/DefaultThemeComponentsSelectorThemeRegistrySourceBuilder.kt:

1@OptIn(IviExperimental::class)
2class DefaultThemeComponentsSelectorThemeRegistrySourceBuilder : IviThemeRegistrySourceBuilder {
3
4 override fun build(
5 registrySourceContext: IviThemeRegistrySourceContext,
6 ): IviThemeRegistrySource =
7 object : IviThemeRegistrySource.Selectors(registrySourceContext) {
8 override fun IviThemeComponentSelectorConfigurator.registerSelectors() {
9 register(
10 defaultThemeComponentsSelector,
11 // `defaultThemeComponentsSelector` is put before the settings selector.
12 // When the theme components are selected by clients, the default theme
13 // components are overridden.
14 IviThemeComponentSelectorPosition.Before(settingsBasedComponentSelectorId),
15 )
16 }
17 }
18}

Then in themeregistrysources.gradle.kts:

1val defaultThemeComponentsSelectorThemeRegistrySource by extra {
2 IviThemeRegistrySourceConfig(
3 registrySourceBuilderName = "DefaultThemeComponentsSelectorThemeRegistrySourceBuilder",
4 implementationModule = themingModule,
5 )
6}

And in build.gradle.kts:

1val defaultThemeComponentsSelectorThemeRegistrySource: IviThemeRegistrySourceConfig by project.extra
2
3iviInstances {
4 create(IviInstanceIdentifier.default) {
5 theming {
6 addRegistrySources(
7 defaultThemeComponentsSelectorThemeRegistrySource,
8 )
9 }
10 }
11}

See the custom color theme

So far we go through each step to provide a custom color theme to the system. Let's check how the custom light mode and custom dark mode looks in the system.