Using Material Color Utilities for Dynamic Theming

Hello everyone :partying_face:

I am pleased to present the direct successor to this guide

Recently, I discovered the Material Color Utilities library, which completely revolutionized the way I managed themes in my applications, allowing an extremely high level of customization for both the application itself and the end user.

ezgif-7994d1c6285b1b33

Material Color Utilities in brief

MCU is a library developed by Google that allows you to automatically generate a complete color system for a theme starting from a single base color.

It creates a series of tones and uses them to generate different color roles (for example primary, onPrimary, surface, background, etc.) following the Material Design 3 guidelines.

The library is able to create both a light and a dark color scheme, automatically adapting the colors to maintain good contrast and visual consistency.

In addition to this, it is also capable of doing more, but I wrote “in brief” :grin:, so if you need more information, please refer directly to the official website

https://m3.material.io/


:bookmark: Index

1. generateColorScheme

2. themedComponents

3. getComponentType

4. applyColorScheme & applyColorSchemeAndFont

5. applyColorSchemeToSystemUI (optional)

6. Practical example

7. AIA


generateColorScheme


I’ll assume you already know how a WebViewer + HTML file + JavaScript file works, and to avoid making the guide too heavy, I’ll refer you to the explanation provided in this other guide.
Setup the WebView - JSON Tree Viewer in WebViewer (Without Extensions)
Offline viewer - JSON Tree Viewer in WebViewer (Without Extensions)

This procedure uses the JavaScript version of the Material Color Utilities library, called through WebViewer.EvaluateJS(generateColorScheme(parameters)).

The following parameters are passed:

Parameter Value
colorInput #RRGGBB - R,G,B - int (native Kodular colors)
variant TonalSpot (default) - Content - Expressive - Fidelity - Monochrome - Neutral - Vibrant - Rainbow - FruitSalad
contrastLevel default 0.0 (default range -1..1)

The procedure returns an object containing two main scheme:

  • light
  • dark

Inside each scheme are the various roles associated with their respective colors (int).

{
	"light": {
		"primaryPaletteKeyColor": -5414314,
		"secondaryPaletteKeyColor": -7180439,
		"tertiaryPaletteKeyColor": -7703484,
		"neutralPaletteKeyColor": -8227726,
		"neutralVariantPaletteKeyColor": -8031376,
		"background": -1802,
		...
        },
     "dark": {
		"primaryPaletteKeyColor": -5414314,
		"secondaryPaletteKeyColor": -7180439,
		"tertiaryPaletteKeyColor": -7703484,
		"neutralPaletteKeyColor": -8227726,
		"neutralVariantPaletteKeyColor": -8031376,
		"background": -15068912,
		...
     }
}

So let’s pay close attention that we will now have shades and color contrasts that are compatible with each other, divided by theme and role, which will simply correspond to the “position” of that component, for example, we might have a layout with the surface role, and a label on it will mainly have a textColor with the onSurface role.


themedComponents

  • As a first layer we have a text key that exists solely as a container, meaning it will only serve to group the components together and keep everything more organized.

what you would put in RecyclerList.SetProperties but with roles instead of the value

leaving the dictionary empty will apply the default roles from componentRoleDefaults

componentRoleDefaults?

image
This is probably the only truly smart thing I’ve done.
Initially, for each component I created a dictionary with all the properties, it wasn’t even a big problem because it was almost all copy-paste, until I realized I had a mile-long, unprofessional dictionary.
So I created a json with the main component types and their defaultProperties


Afterwards, the theme application procedure will use getComponentType to figure out which component it is cycling through, and in case there are no properties present in themedComponents, it uses the corresponding one in componentRoleDefaults

In my componentRoleDefaults I decided that card views are outlined by default
image


but nothing stops you from customizing it however you see fit!

componentRoleDefaults.json
{
	"Button": {
		"BackgroundColor": "primary",
		"TextColor": "onPrimary"
	},
	"CheckBox": {
		"CheckboxColor": "primary",
		"TextColor": "onSurface"
	},
	"MakeroidCircularProgress": {
		"Color": "primary"
	},
	"ProgressBar": {
		"BackgroundColor": "surfaceVariant",
		"Color": "primary"
	},
	"DatePicker": {
		"BackgroundColor": "surfaceContainerHigh",
		"TextColor": "onSurface"
	},
	"Label": {
		"TextColor": "onSurface"
	},
	"MakeroidLinearProgressbar": {
		"IndeterminateColor": "primary",
		"ProgressColor": "primary"
	},
	"ListPicker": {
		"BackgroundColor": "surfaceContainer",
		"ItemBackgroundColor": "surfaceContainer",
		"ItemTextColor": "onSurface",
		"TextColor": "onSurface",
		"StatusBarColor": "surfaceContainer",
		"TitleBarColor": "surfaceContainer"
	},
	"Radiobutton": {
		"RadioButtonColor": "primary",
		"TextColor": "onSurface"
	},
	"Ratingbar": {
		"BackgroundColor": "surfaceVariant",
		"StarColor": "primary"
	},
	"Slider": {
		"ColorLeft": "primary",
		"ColorRight": "surfaceVariant",
		"ThumbColor": "primary"
	},
	"Spinner": {
		"ItemBackgroundColor": "surfaceContainer",
		"ItemTextColor": "onSurface",
		"PromptItemColor": "onSurfaceVariant",
		"SpinnerColor": "outline"
	},
	"KodularStateProgressBar": {
		"BackgroundColor": "surfaceVariant",
		"CurrentStateDescriptionColor": "primary",
		"ForegroundColor": "primary",
		"StateDescriptionColor": "onSurfaceVariant",
		"StateNumberBackgroundColor": "onSurfaceVariant",
		"StateNumberForegroundColor": "onPrimary"
	},
	"SwitchToggle": {
		"TextColor": "onSurface",
		"ThumbColor": "primary",
		"TrackColor": "primary"
	},
	"TextBox": {
		"BackgroundColor": "surfaceVariant",
		"CursorColor": "primary",
		"HighlightColor": "primaryContainer",
		"HintColor": "onSurfaceVariant",
		"TextColor": "onSurface"
	},
	"TimePicker": {
		"BackgroundColor": "surfaceContainerHigh",
		"TextColor": "onSurface"
	},
	"MakeroidCardView": {
		"BackgroundColor": "surface",
		"StrokeColor": "outlineVariant",
		"TouchColor": "surfaceContainerHighest"
	},
	"HorizontalArrangement": {
		"BackgroundColor": "surfaceContainer "
	},
	"VerticalArrangement": {
		"BackgroundColor": "surfaceContainer "
	},
	"KodularBottomNavigation": {
		"BackgroundColor": "surfaceContainer",
		"SelectedColor": "primary",
		"UnselectedColor": "onSurfaceVariant"
	},
	"MakeroidViewPager": {
		"TabsActiveTextColor": "primary",
		"TabsBackgroundColor": "surface",
		"TabsIndicatorColor": "primary",
		"TabsTextColor": "onSurfaceVariant"
	}
}

getComponentType

This is a helper function whose sole purpose is to identify a component by returning its class (Label, Button, etc.).


applyColorScheme & applyColorSchemeAndFont

These two procedures are responsible for actually applying the theme to the components.
Both cycle through the categories and components of themedComponents and, using RecyclerList’s SetProperties method (replaceable with the same-named one in DynamicComponents), apply the properties, but let’s go into more depth shortly.
As explained earlier regarding the themedComponents variable, an empty dictionary will use the default values
applyColorSchemeAndFont additionally applies the font to all components in fontTargets (covers practically all the main native components)

If you want to use applyColorSchemeAndFont and have a different font for some components, for example Material Icons, you can specify it in themedComponents.

blocks(62)

Why are KodularStateProgressBar and a few others missing?

Unlike most components that use the FontTypefaceImport property, some have special properties, the StateProgressBar, for example, has StateNumberTypefaceImport and StateDescriptionTypefaceImport.
If needed, it could still be implemented by adding an exception in the procedure.

Is it possible to use only native components? (without RL/DC)

Sure, it’s less convenient but not impossible.
Just handle everything through anyComponents, making use of the getComponentType function

If the getComponentType = "X"
 use AnyX.BackgroundColor
else if the getComponentType = "Y"
 use AnyY.BackgroundColor

applyColorSchemeToSystemUI

This function is an extra, meaning it can be removed and integrated into the other two procedures. It simply applies the roles to components that do not allow the use of .SetProperties (which are not AndroidViewComponents StatusBar, Snackbar, Notifier and others).


Practical example

Example 1 - Predefined themes

If we wanted to use predefined themes, it would be enough to simply save the output of generateColorScheme as text and store it somewhere, then call it back during initialization.


Since it is a JSON text, it must be converted using Web.JSONTextDecode.

Example 2 - Random theme on each initialization



AIA

Essential AIA


MCU_Theme_Manager_Essential.aia (3.0 MB)

Enhanced AIA


MCU_Theme_Manager.aia (3.0 MB)

Hey, in this project there’s one of my extensions!
Take a look here [F/OS] - RealtimeBlurBackground

In this project, in the roles section, you can view all the available roles and see how they change based on the selected color.