Theming from code
Since Nutrient Android SDK 12.1, you can theme the SDK’s screens from code with a Compose UiColorScheme instead of XML theme attributes. One call to setColorScheme() on PdfActivity or PdfUiFragment colors the toolbars, the outline view, document info, and the tab bar. It also colors the share menu, the settings sheet, AI Assistant, the thumbnail bar, and the thumbnail grid.
XML theme or color scheme
Both approaches theme most of the same screens. Pick the one that matches how your app defines its colors:
- XML theme attributes stay the default. Use them when your app already has an XML theme. They also style anything a color scheme doesn’t cover, such as annotation hints, the password view, and the signature layout. See appearance styling for every attribute.
setColorScheme()suits apps that define their colors in code — for example, in a ComposeMaterialTheme. It also enables you to change the colors at runtime without recreating the activity.
A scheme set in code wins over the XML theme for the screens it covers. Call setColorScheme(null) to return to the XML theme.
The SDK stores the scheme on the hosting activity. Two PdfUiFragment instances in one activity share it, and the last call wins. PdfUiFragment also accepts the call before it’s attached and applies the scheme once it is.
Choosing a starting point
A UiColorScheme has a Material 3 ColorScheme base, material, and a color class per screen. Every color you leave unset takes its Material role from material, so you only set the colors that should differ. Start from one of these:
| Function | Starts from | Composable |
|---|---|---|
UiColorScheme.fromMaterial(colors) | The roles of a Material 3 ColorScheme | No |
UiColorScheme.fromXmlTheme(context) | The colors your XML theme attributes give today | No |
UiColorScheme.default() | The ambient MaterialTheme.colorScheme | Yes |
fromMaterial() and fromXmlTheme(context) are plain functions, so you can call them in onCreate():
class MyPdfActivity : PdfActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
// Every screen takes its colors from your app's Material 3 roles. setColorScheme(UiColorScheme.fromMaterial(lightColorScheme(primary = Color(0xFF1E5AA8)))) }}Use fromXmlTheme(context) when you only want to change some screens. A scheme copied from it keeps the XML colors of every screen it doesn’t change. fromXmlTheme() derives its Material base from the outline view’s background and text. The theme’s colorAccent becomes primary, the main toolbar background becomes surfaceContainer, and the outline’s delete color becomes error.
Starting from fromMaterial() or default() instead moves every screen to Material colors, including the ones you don’t set.
default() returns a resolved scheme, so every color in it is already set. Replacing material on its result doesn’t change the screens’ colors. To start from other Material colors, pass them to fromMaterial() instead.
What each part colors
Use copy() to replace the parts you want to change. Each part is a color class whose properties all default to unset:
UiColorScheme property | Class | Screen |
|---|---|---|
mainToolbar | MainToolbarColors | Main toolbar, its overflow menu, and the document title |
contextualToolbar | ContextualToolbarColors | Annotation creation toolbar and its submenus |
outline | OutlineColors | Outline view with its bookmark, annotation, and attachment lists |
documentInfoColorScheme | DocumentInfoColorScheme | Document info |
tabBar | TabBarColors | Document tab bar |
actionMenu | ActionMenuColors | Share menu |
settingsColorScheme | SettingsColorScheme | Settings sheet and the custom stamp creator’s switches |
aiAssistantColorScheme | AiAssistantColorScheme | AI Assistant |
thumbnailBar | ThumbnailBarColors | Floating, pinned, and scrollable thumbnail bars |
thumbnailGrid | ThumbnailGridColors | Thumbnail grid (document overview) |
An unset color resolves to a Material role of material. These are the main ones:
| Part | Background | Content | Selection |
|---|---|---|---|
mainToolbar | surfaceContainer | onBackground for text and title; icons follow the text | Selected icons follow the icon color |
contextualToolbar | surfaceContainer | onSurfaceVariant icons | primary |
outline | background | onBackground text, onSurfaceVariant unselected tabs | primary |
documentInfoColorScheme | background | onBackground titles, onSurfaceVariant values | primary group titles, primaryContainer button |
tabBar | background | onSurfaceVariant titles | secondaryContainer tab, onSecondaryContainer title |
actionMenu | background | onBackground labels | primary icon background, onPrimary icon |
settingsColorScheme | background | onBackground titles and labels, outlineVariant dividers | primary, with secondary for unselected options |
thumbnailBar | surfaceContainer (the scrollable bar stays transparent) | outline thumbnail borders | primary current page |
thumbnailGrid | surfaceDim | primary current page and selection check |
Some colors follow another color of the same part instead of a role. For example, an unset MainToolbarColors.iconColor follows textColor, so setting textColor also changes the icon color. The title doesn’t follow textColor, so set titleTextColor as well:
setColorScheme( UiColorScheme.fromXmlTheme(this).copy( mainToolbar = MainToolbarColors( backgroundColor = Color(0xFF1E2A44), textColor = Color.White, titleTextColor = Color.White, popup = ToolbarPopupColors(backgroundColor = Color(0xFF1E2A44)), ), // A scheme copied from `fromXmlTheme()` keeps the XML annotation creation toolbar. // Set it too when the two toolbars should match. contextualToolbar = ContextualToolbarColors( backgroundColor = Color(0xFF1E2A44), iconColor = Color.White, ), ),)The mainToolbar icon colors also color the SDK’s built-in toolbar items and the overflow button. A scheme from fromXmlTheme() takes them from pspdf__iconsColor and pspdf__iconsColorActivated in pspdf__actionBarIconsStyle, so the toolbar keeps its XML look until you change them. See appearance styling for the icon style.
The SDK fills in unset colors only when it resolves the scheme. Inside composition, read colors through NutrientTheme.colorScheme, which is already resolved. Outside composition, call resolve() on the scheme first.
A component’s own colors still win over the scheme for that component. For example, the colors parameter of NutrientMainToolbar wins for that toolbar. Once you call PdfThumbnailBar.setBackgroundColor(), it wins for the floating and pinned bar. That method takes a color, not null, so you can’t hand the background back to the scheme afterward.
Setting one accent color
withAccent(accent, onAccent) returns a copy of a scheme with one accent on the selection colors of every screen. accent becomes primary, primaryContainer, secondaryContainer, and inversePrimary in material, with onAccent as the content color on them. Surfaces and most text stay as they are, so it works well on top of fromXmlTheme(). AI Assistant is the exception: Its text, toolbar text, and icons follow primary, so they take the accent too:
setColorScheme(UiColorScheme.fromXmlTheme(this).withAccent(Color(0xFF1E5AA8), Color.White))// `withAccent()` takes packed ARGB colors from Java.setColorScheme(UiColorScheme.fromXmlTheme(this).withAccent(0xFF1E5AA8, Color.WHITE));A toolbar keeps its selected icon color when the accent wouldn’t be readable on its background. Replace material before you call withAccent(), not after. withAccent() fixes AI Assistant’s reply bubble and button colors to the values they have when you call it, so a material you set afterward doesn’t reach them.
Light and dark mode
A scheme set with setColorScheme(UiColorScheme) applies in both light and dark mode. To follow the theme, pass a pair: either two Material 3 ColorSchemes, which the SDK wraps with fromMaterial(), or two UiColorSchemes:
// Two Material 3 color schemes.setColorScheme( lightColorScheme(primary = brandColor), darkColorScheme(primary = brandColorDark),)
// Dynamic color, on Android 12 and later.if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setColorScheme(dynamicLightColorScheme(this), dynamicDarkColorScheme(this))}
// Two `UiColorScheme`s, when you also set parts of the scheme.setColorScheme( UiColorScheme.fromMaterial(lightColors).withAccent(brandColor, Color.White), UiColorScheme.fromMaterial(darkColors).withAccent(brandColorDark, Color.Black),)The SDK shows the light scheme when the activity’s theme declares itself light with isLightTheme. It shows the dark scheme when the theme declares itself dark. Every AppCompat and Material Components theme declares it. Any other theme follows the configuration’s night mode. It switches on its own, so you don’t need to set the scheme again after a configuration change. In practice:
- A DayNight theme switches the scheme with the device’s dark mode.
ThemeMode.NIGHTshows the dark scheme whatever the device’s mode, because it applies a dark theme.- The SDK’s default theme is light only. If you keep it, the light scheme shows even on a dark device. To show the dark scheme, pass a DayNight theme to
PdfActivityConfiguration.Builder#theme(int)or useThemeMode.NIGHT.
To build the pair from two XML themes, wrap the activity in a ContextThemeWrapper for each:
setColorScheme( UiColorScheme.fromXmlTheme(ContextThemeWrapper(this, R.style.MyTheme)), UiColorScheme.fromXmlTheme(ContextThemeWrapper(this, R.style.MyTheme_Dark)),)setColorScheme( UiColorScheme.fromXmlTheme(new ContextThemeWrapper(this, R.style.MyTheme)), UiColorScheme.fromXmlTheme(new ContextThemeWrapper(this, R.style.MyTheme_Dark)));A ContextThemeWrapper reads resources in the activity’s current night mode. If a theme’s colors come from -night resources, call applyOverrideConfiguration() on its wrapper with a configuration that sets uiMode. Use UI_MODE_NIGHT_YES for the dark theme and UI_MODE_NIGHT_NO for the light theme.
Compose content
NutrientTheme provides the SDK’s colors to Compose content, such as a host-rendered NutrientMainToolbar or NutrientTabBar. It picks its scheme from the first of these that is set:
- The
colorSchemepassed to it, or to aNutrientThemearound it. It applies to that content only. The SDK’s own sheets and dialogs keep the activity’s scheme. - The scheme set with
setColorScheme(), the light or dark one of a pair. - The activity’s XML theme attributes, or those of a theme overlay around the composition, such as
android:themeor aContextThemeWrapper.
So NutrientTheme without a colorScheme matches the SDK screens around it. Read the colors with NutrientTheme.colorScheme and the icons with NutrientTheme.iconScheme:
NutrientTheme { Icon( painter = painterResource(R.drawable.ic_share), contentDescription = null, tint = NutrientTheme.colorScheme.mainToolbar.iconColor, )}NutrientTheme also installs the scheme’s material base as the MaterialTheme of its content. Stock Material 3 components inside it, such as switches, ripples, and text fields, therefore follow the SDK’s colors. Typography, shapes, and the text style stay those of the enclosing composition.
If you wrap SDK composables in your own MaterialTheme and want its colors to apply, pass them in:
MaterialTheme(colorScheme = appColors) { NutrientTheme(colorScheme = UiColorScheme.fromMaterial(MaterialTheme.colorScheme)) { NutrientMainToolbar(coordinator = coordinator) }}Inside composition, the composable UiColorScheme.fromXmlTheme() returns the XML theme’s scheme for the current context, so you can copy it and change one part.
PdfActivity and PdfUiFragment update every NutrientTheme in the activity after Activity.setTheme() or a configuration change, without recreating the activity. Colors read from NutrientTheme.colorScheme update on their own. If your code remembers a value read from the XML theme, key it on LocalNutrientThemeKey as well, because LocalContext and LocalConfiguration don’t change on setTheme():
val context = LocalContext.currentval accent = remember(context, LocalNutrientThemeKey.current) { val value = TypedValue() context.theme.resolveAttribute(androidx.appcompat.R.attr.colorAccent, value, true) Color(value.data)}Using color schemes from Java
setColorScheme() and the scheme functions that don’t take Compose colors work from Java:
UiColorScheme.fromXmlTheme(context), including with aContextThemeWrapperfor a light and dark pair.withAccent(int accent, int onAccent), which takes packed alpha, red, green, and blue (ARGB) colors.setColorScheme(light, dark)andsetColorScheme(null).UiColorScheme.fromMaterial(colors)andsetColorScheme(lightColors, darkColors), which take Material 3ColorSchemes.resolve(), and the getters of every part, such asgetMainToolbar().
Compose Material 3 compiles its dynamic color functions into DynamicTonalPaletteKt, so Java can call them:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setColorScheme( DynamicTonalPaletteKt.dynamicLightColorScheme(this), DynamicTonalPaletteKt.dynamicDarkColorScheme(this));}lightColorScheme() and darkColorScheme() take Compose Color values, which Java can’t pass. Neither can the constructors of the color classes, and UiColorScheme.copy() has no Java overloads. UiColorScheme.default() and NutrientTheme are composable. Write a small Kotlin function that returns the scheme, and call it from Java. Use this to build a ColorScheme from your own colors or to set individual colors.
Migrating from the deprecated theming APIs
Nutrient Android SDK 12.1 deprecates the earlier theming APIs. Switch to the replacements below. Most of them keep working, with one exception. The NutrientTheme overload that takes a controller is hidden, not only deprecated. Code compiled against 12.0 still runs, but Kotlin source that passes controller no longer compiles, so remove the argument when you update:
| Deprecated | Replacement |
|---|---|
PdfActivity.setMainToolbarColorScheme(scheme) | setColorScheme(scheme) |
NutrientTheme(controller = …, colorScheme = scheme) | NutrientTheme(colorScheme = scheme). Source that passes controller no longer compiles |
NutrientThemeController, rememberNutrientThemeController() | Nothing. Remove calls to invalidate(), and key remembered XML values on LocalNutrientThemeKey |
UiTheme.colors/UiTheme.icons | NutrientTheme.colorScheme/NutrientTheme.iconScheme |
SdkTheme(colors, icons), SdkTheme.default() | NutrientTheme(colorScheme = colors, iconScheme = icons) { … } |
getUiColors() | UiColorScheme.fromXmlTheme() |
getComposeUiColors() | UiColorScheme.default() |
NutrientFloatingToolbarSearchTheme { … } | NutrientTheme(colorScheme = floatingToolbarSearchColorScheme()) { … } |
A scheme set with setMainToolbarColorScheme() still wins over setColorScheme() on the main toolbar. When you switch, stop calling it, or call it with null, or the main toolbar keeps its old colors.
setColorScheme() colors more screens than setMainToolbarColorScheme() did. If your scheme was copied from UiColorScheme.fromXmlTheme(), the other screens keep their colors. If it was copied from UiColorScheme.default(), they move to Material 3 colors, so set the parts you want to keep. See the 12.1 migration guide for the other changes to how schemes resolve.
Examples
These Catalog examples build a scheme from UiColorScheme.fromXmlTheme() and pass it to NutrientTheme around host-rendered toolbars:
- Custom Toolbars: PdfActivity, Custom Toolbars: PdfFragment, Custom Toolbars: PdfFragment (View Interop), and Custom Toolbars: DocumentView (
CustomToolbarsExamples.kt) - Tabbed Documents (Compose) (
DocumentTabsComposeExample.kt) - Annotation Toolbar: User-Editable Tools (
AnnotationToolbarUserEditableExample.kt)