Android - Create custom colors in Compose with Material 3
Asked Answered
B

2

11

I have been exploring Compose + Material 3 for the first time and I am having a really hard time trying to implement a custom color.

What I mean by this is doing the following based on how things could be done before Compose:

I have my custom attribute in attrs.xml

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <declare-styleable name="CustomStyle">
        <attr name="myCustomColor"
            format="reference|color"/>
    </declare-styleable>
</resources>

And that custom attribute can be used in my light and dark styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
        <item name="myCustomColor">@color/white</item> <!-- @color/black in the dark style config -->
    </style>
</resources>

And then I can use it anywhere I want, both in code or in layouts:

<com.google.android.material.imageview.ShapeableImageView
    android:layout_width="24dp"
    android:layout_height="24dp"
    android:background="?myCustomColor"

This is very simple and practical because it resolves the light and dark color automatically and all I need is to use the custom color reference.

But in Compose with Material 3 I can't find anywhere explained how something like this can be done.

In Material 2 it was possible to something like this:

val Colors.myExtraColor: Color
    get() = if (isLight) Color.Red else Color.Green

But in Material 3 this is no longer possible:

Unlike the M2 Colors class, the M3 ColorScheme class doesn’t include an isLight parameter. In general you should try and model whatever needs this information at the theme level.

https://developer.android.com/jetpack/compose/designsystems/material2-material3#islight

I tried looking for a solution here in SO but so far found nothing that would work for this.

Is there a simple way of achieving this like it is possible with the non-Compose version as I exemplified above?

Beene answered 1/9, 2023 at 22:40 Comment(5)
CompositionLocalProvider is what are you looking for, this answer can help you to achieve that. The example was written with M2, but it is independent of it, it will work the same in M3 and can even be used for other things, like dp, Shapes, etc.Grantgranta
@ThalesIsidoro But that suggestion means I will no longer be able to use default references such as MaterialTheme.colorScheme.background, right? Or it allows both to be used without issue? I already have a lightColorScheme/darkColorScheme configured, but your suggestion appears to be incompatible with this, as in it's either one or the other.Beene
CompositionLocalProvider is not a replacement, you can still use the others color's reference. A solid example of that is NowInAndroid whit the same thing (customs colors).Grantgranta
@ThalesIsidoro Thank you, but as per the answer example you provided, the Materialtheme object used can only accept 1 color palette. If I have both the Material palette and my custom palette it seems like there is no way to use both. See Theme.kt here for example of what I currently have developer.android.com/codelabs/jetpack-compose-theming#3Beene
@ThalesIsidoro to continue the above, if I already have my own lightColorScheme and darkColorScheme to pass into my MaterialTheme color scheme, I cannot understand how I can also include the CustomColorsPalette you've suggested since it appears that I can only use one OR the other, but not both at the same time. If I am misunderstanding this, could you please post an answer to this question with an example of achieving it so I can understand it better?Beene
G
33

CompositionLocalProvider is the way for that.

Colors.kt:

import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color

val LightPrimary = Color(color = 0xFF6750A4)
val LightOnPrimary = Color(color = 0xFFFFFFFF)
val LightPrimaryContainer = Color(color = 0xFFEADDFF)
val LightOnPrimaryContainer = Color(color = 0xFF21005D)
val LightInversePrimary = Color(color = 0xFFD0BCFF)
val LightSecondary = Color(color = 0xFF625B71)
val LightOnSecondary = Color(color = 0xFFFFFFFF)
val LightSecondaryContainer = Color(color = 0xFFE8DEF8)
val LightOnSecondaryContainer = Color(color = 0xFF1D192B)
val LightTertiary = Color(color = 0xFF7D5260)
val LightOnTertiary = Color(color = 0xFFFFFFFF)
val LightTertiaryContainer = Color(color = 0xFFFFD8E4)
val LightOnTertiaryContainer = Color(color = 0xFF31111D)
val LightBackground = Color(color = 0xFFFFFBFE)
val LightOnBackground = Color(color = 0xFF1C1B1F)
val LightSurface = Color(color = 0xFFFFFBFE)
val LightOnSurface = Color(color = 0xFF1C1B1F)
val LightSurfaceVariant = Color(color = 0xFFE7E0EC)
val LightOnSurfaceVariant = Color(color = 0xFF49454F)
val LightInverseSurface = Color(color = 0xFF313033)
val LightInverseOnSurface = Color(color = 0xFFF4EFF4)
val LightSurfaceTint = Color(color = 0xFF6750A4)
val LightError = Color(color = 0xFFB3261E)
val LightOnError = Color(color = 0xFFFFFFFF)
val LightErrorContainer = Color(color = 0xFFF9DEDC)
val LightOnErrorContainer = Color(color = 0xFF410E0B)
val LightOutline = Color(color = 0xFF79747E)
val LightOutlineVariant = Color(color = 0xFFCAC4D0)
val LightScrim = Color(color = 0xFF4B484E)

val DarkPrimary = Color(color = 0xFFD0BCFF)
val DarkOnPrimary = Color(color = 0xFF381E72)
val DarkPrimaryContainer = Color(color = 0xFF4F378B)
val DarkOnPrimaryContainer = Color(color = 0xFFEADDFF)
val DarkInversePrimary = Color(color = 0xFF6750A4)
val DarkSecondary = Color(color = 0xFFCCC2DC)
val DarkOnSecondary = Color(color = 0xFF332D41)
val DarkSecondaryContainer = Color(color = 0xFF4A4458)
val DarkOnSecondaryContainer = Color(color = 0xFFE8DEF8)
val DarkTertiary = Color(color = 0xFFEFB8C8)
val DarkOnTertiary = Color(color = 0xFF492532)
val DarkTertiaryContainer = Color(color = 0xFF633B48)
val DarkOnTertiaryContainer = Color(color = 0xFFFFD8E4)
val DarkBackground = Color(color = 0xFF1C1B1F)
val DarkOnBackground = Color(color = 0xFFE6E1E5)
val DarkSurface = Color(color = 0xFF1C1B1F)
val DarkOnSurface = Color(color = 0xFFE6E1E5)
val DarkSurfaceVariant = Color(color = 0xFF49454F)
val DarkOnSurfaceVariant = Color(color = 0xFFCAC4D0)
val DarkInverseSurface = Color(color = 0xFFE6E1E5)
val DarkInverseOnSurface = Color(color = 0xFF313033)
val DarkSurfaceTint = Color(color = 0xFFD0BCFF)
val DarkError = Color(color = 0xFFF2B8B5)
val DarkOnError = Color(color = 0xFF601410)
val DarkErrorContainer = Color(color = 0xFF8C1D18)
val DarkOnErrorContainer = Color(color = 0xFFF9DEDC)
val DarkOutline = Color(color = 0xFF938F99)
val DarkOutlineVariant = Color(color = 0xFF49454F)
val DarkScrim = Color(color = 0xFFB4B0BB)

val LightColorScheme = lightColorScheme(
    primary = LightPrimary,
    onPrimary = LightOnPrimary,
    primaryContainer = LightPrimaryContainer,
    onPrimaryContainer = LightOnPrimaryContainer,
    inversePrimary = LightInversePrimary,
    secondary = LightSecondary,
    onSecondary = LightOnSecondary,
    secondaryContainer = LightSecondaryContainer,
    onSecondaryContainer = LightOnSecondaryContainer,
    tertiary = LightTertiary,
    onTertiary = LightOnTertiary,
    tertiaryContainer = LightTertiaryContainer,
    onTertiaryContainer = LightOnTertiaryContainer,
    background = LightBackground,
    onBackground = LightOnBackground,
    surface = LightSurface,
    onSurface = LightOnSurface,
    surfaceVariant = LightSurfaceVariant,
    onSurfaceVariant = LightOnSurfaceVariant,
    surfaceTint = LightSurfaceTint,
    inverseSurface = LightInverseSurface,
    inverseOnSurface = LightInverseOnSurface,
    error = LightError,
    onError = LightOnError,
    errorContainer = LightErrorContainer,
    onErrorContainer = LightOnErrorContainer,
    outline = LightOutline,
    outlineVariant = LightOutlineVariant,
    scrim = LightScrim
)

val DarkColorScheme = darkColorScheme(
    primary = DarkPrimary,
    onPrimary = DarkOnPrimary,
    primaryContainer = DarkPrimaryContainer,
    onPrimaryContainer = DarkOnPrimaryContainer,
    inversePrimary = DarkInversePrimary,
    secondary = DarkSecondary,
    onSecondary = DarkOnSecondary,
    secondaryContainer = DarkSecondaryContainer,
    onSecondaryContainer = DarkOnSecondaryContainer,
    tertiary = DarkTertiary,
    onTertiary = DarkOnTertiary,
    tertiaryContainer = DarkTertiaryContainer,
    onTertiaryContainer = DarkOnTertiaryContainer,
    background = DarkBackground,
    onBackground = DarkOnBackground,
    surface = DarkSurface,
    onSurface = DarkOnSurface,
    surfaceVariant = DarkSurfaceVariant,
    onSurfaceVariant = DarkOnSurfaceVariant,
    surfaceTint = DarkSurfaceTint,
    inverseSurface = DarkInverseSurface,
    inverseOnSurface = DarkInverseOnSurface,
    error = DarkError,
    onError = DarkOnError,
    errorContainer = DarkErrorContainer,
    onErrorContainer = DarkOnErrorContainer,
    outline = DarkOutline,
    outlineVariant = DarkOutlineVariant,
    scrim = DarkScrim
)

Theme.kt:

import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext

@Composable
fun AppTheme(
    useDarkTheme: Boolean = isSystemInDarkTheme(),
    useDynamicColors: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            if (useDarkTheme) dynamicDarkColorScheme(context = LocalContext.current)
            else dynamicLightColorScheme(context = LocalContext.current)
        }

        useDarkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        content = content
    )
}

Above we have a basic code for defining theme colors in Material 3.

To add custom colors or anything else using CompositionLocalProvider, we first need to create a data class that will contain the values/types. Let's assume we want 3 new color types, which can be different depending on the light or dark theme.

To do this, let's add a new file to the project:

CustomColorsPalette.kt

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color

@Immutable
data class CustomColorsPalette(
    val extraColor1: Color = Color.Unspecified,
    val extraColor2: Color = Color.Unspecified,
    val extraColor3: Color = Color.Unspecified
)

val LightExtraColor1 = Color(color = 0xFF29B6F6)
val LightExtraColor2 = Color(color = 0xFF26A69A)
val LightExtraColor3 = Color(color = 0xFFEF5350)

val DarkExtraColor1 = Color(color = 0xFF0277BD)
val DarkExtraColor2 = Color(color = 0xFF00695C)
val DarkExtraColor3 = Color(color = 0xFFC62828)

val LightCustomColorsPalette = CustomColorsPalette(
    extraColor1 = LightExtraColor1,
    extraColor2 = LightExtraColor2,
    extraColor3 = LightExtraColor3
)

val DarkCustomColorsPalette = CustomColorsPalette(
    extraColor1 = DarkExtraColor1,
    extraColor2 = DarkExtraColor2,
    extraColor3 = DarkExtraColor3
)

val LocalCustomColorsPalette = staticCompositionLocalOf { CustomColorsPalette() }
  • The CustomColorsPalette data class has 3 colors.
  • 6 colors were added, where 3 are light and 3 are dark.
  • Two val types of CustomColorsPalette were created, one with light colors and another with dark colors.
  • A staticCompositionLocalOf is created based on docs from CompositionLocalProvider.

After that, we can go back to our Theme.kt file to add the logic and finish configuring the CompositionLocalProvider.

Theme.kt:

import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext

@Composable
fun AppTheme(
    useDarkTheme: Boolean = isSystemInDarkTheme(),
    useDynamicColors: Boolean = true,
    content: @Composable () -> Unit
) {
    // "normal" palette, nothing change here
    val colorScheme = when {
        useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            if (useDarkTheme) dynamicDarkColorScheme(context = LocalContext.current)
            else dynamicLightColorScheme(context = LocalContext.current)
        }

        useDarkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    // logic for which custom palette to use
    val customColorsPalette =
        if (useDarkTheme) DarkCustomColorsPalette
        else LightCustomColorsPalette

    // here is the important point, where you will expose custom objects
    CompositionLocalProvider(
        LocalCustomColorsPalette provides customColorsPalette // our custom palette
    ) {
        MaterialTheme(
            colorScheme = colorScheme, // the MaterialTheme still uses the "normal" palette
            content = content
        )
    }
}

And finally we can use the colors as follows:

MainActivity.kt

AppTheme {
    Scaffold { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues = innerPadding),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "Default Material 3 color on background")

            Card {
                Text(text = "Default Material 3 color for elevation")
            }

            Text(
                text = "One of customs colors",
                color = LocalCustomColorsPalette.current.extraColor1
            )

            Text(
                text = "Other custom color",
                color = LocalCustomColorsPalette.current.extraColor2
            )
        }
    }
}

light theme sample dark theme sample

The colors we added can be used through the LocalCustomColorsPalette.current, as seen in the example above. It's exactly the same as other Compose objects, such as the LocalTextStyle.current, LocalDensity.current, etc.

There is the possibility of applying a trick to modify the call to these custom objects to be similar to the patterns that are inside the MaterialTheme object, for that just add the following code on CustomColorsPalette.kt:

// ...
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
// ...

val MaterialTheme.customColorsPalette: CustomColorsPalette
    @Composable
    @ReadOnlyComposable
    get() = LocalCustomColorsPalette.current

Now it will be possible to call the colors like this:

Text(
    text = "Default color scheme remains available",
    color = MaterialTheme.colorScheme.onBackground
)

Text(
    text = "One of customs colors",
    color = MaterialTheme.customColorsPalette.extraColor1
)

Text(
    text = "Other custom color",
    color = MaterialTheme.customColorsPalette.extraColor2
)
Grantgranta answered 5/9, 2023 at 0:28 Comment(4)
Amazing explanation, I was definitely lost and did not understand the custom palette works as an extra layer on top of the material one. I applied your solution to my case and it worked exactly as I wanted, thank you so much for this!Beene
How to access this in a Style.kt file where i am adding all my textStyles like val headerStyle = TextStyle ( color = ? )Hallvard
@SiddarthG I haven't tested it here, but it might work: val headerStyle @Composable get() = TextStyle(color = MaterialTheme.customColorsPalette.extraColor1). To create customizations I prefer to create a composable component, such as: @Composable fun HeaderText(modifier: Modifier = Modifier, text: String) = Text(modifier = modifier, text = text, style = TextStyle(color = MaterialTheme.customColorsPalette.extraColor1))Grantgranta
Does data class has to be @Immutable? What if i want to grant user ability to change this custom palette , can't i just change the values in CustomColorsPalette.kt directly at runtime?Ocarina
I
1

In Material 2 it was possible to something like this:

val Colors.myExtraColor: Color
    get() = if (isLight) Color.Red else Color.Green  

You can still do something similar in Material 3, 3 things to note:

  1. Colors is now ColorScheme
  2. isLight was removed
  3. isSystemInDarkTheme() is still there

With that said, your code for myExtraColor in Material 3 will become:

val ColorScheme.myExtraColor: Color
    @Composable
    get() = if(isSystemInDarkTheme()) Color.Green else Color.Red

Notice the @Composable added to the get()

In your code you can use it like other Material 3 colors:

@Composable
fun Testing() {
    Text(
        text = "A text with my custom extra color", 
        color = colorScheme.myExtraColor
    )
}
Ingrowth answered 18/2 at 7:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.