'How to match color attributes between theme and icon in Jetpack Compose?

I have a vector drawable which has two paths with different attributes referencing to different theme colors.

And these attributes' values are being changed by different theme, how to achieve the same in Jetpack Compose?

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="82dp"
    android:height="96dp"
    android:viewportWidth="82"
    android:viewportHeight="96">
  <path
      android:pathData="M0.2887,4.6197C0.2887,2.2278 2.2278,0.2887 4.6197,0.2887H77.3803C79.7722,0.2887 81.7113,2.2278 81.7113,4.6197V91.2394C81.7113,93.6314 79.7722,95.5704 77.3803,95.5704H4.6197C2.2278,95.5704 0.2887,93.6314 0.2887,91.2394V4.6197Z"
      android:fillColor="?attr/colorPrimary" />
  <path
      android:pathData="M4.043,4.0422h73.9155v73.9155h-73.9155z"
      android:fillColor="?attr/colorSecondary"/>
</vector>

styles.xml with different themes, as an example

<style name="RedTheme" parent="GlobalTheme">
    <item name="colorPrimary">@color/red</item>
    <item name="colorSecondary">@color/redDark</item>
    ...
</style>

<style name="GreenTheme" parent="GlobalTheme">
    <item name="colorPrimary">@color/green</item>
    <item name="colorSecondary">@color/greenDark</item>
    ...
</style>

Depending which theme is currently used, vector drawable or icon can have different colors



Solution 1:[1]

First define the color:

import androidx.compose.ui.graphics.Color
val white = Color(0xFFFFFFFF)
val black = Color(0xFF000000)
...

Define attributes:

class GlobalThemeData(colorPrimary: Color ...){
    var colorPrimary by mutableStateOf(colorPrimary)
    ...
}

Define theme :

val LightColorPalette = GlobalThemeData(colorPrimary = white)

val DarkColorPalette = GlobalThemeData(colorPrimary = black)

...

object GlobalTheme {
    val colors: GlobalThemeData
        @Composable
        get() = LightColorPalette

    enum class Theme {
        Light, Dark ...
    }
}

Define compositionLocal and GlobalTheme

private val LocalGlobalColors = compositionLocalOf {
    LightColorPalette
}

@Composable
fun GlobalTheme(theme: GlobalTheme.Theme = GlobalTheme.Theme.Light,
                content: @Composable () -> Unit) {
    val targetColors =
        when(theme){
            GlobalTheme.Theme.Dark -> DarkColorPalette
            GlobalTheme.Theme.Light -> LightColorPalette
            GlobalTheme.Theme.Red -> RedColorPalette
            GlobalTheme.Theme.Green -> GreenColorPalette
        }
    CompositionLocalProvider(LocalGlobalColors provides targetColors) {
        MaterialTheme(
            shapes = Shapes,
            typography = Typography,
            content = content
        )
    }
}

Examples:

val white = Color(0xFFFFFFFF)
val black = Color(0xFF000000)
val red = Color(0xFFC51614)
val green = Color(0xFF67BF63)

val LightColorPalette = GlobalThemeData(colorPrimary = white)

val DarkColorPalette = GlobalThemeData(colorPrimary = black)

val RedColorPalette = GlobalThemeData(colorPrimary = red)

val GreenColorPalette = GlobalThemeData(colorPrimary = green)

object GlobalTheme {
    val colors: GlobalThemeData
        @Composable
        get() = LocalGlobalColors.current

    enum class Theme {
        Light, Dark,Red,Green
    }
}

class GlobalThemeData(colorPrimary: Color){
    var colorPrimary by mutableStateOf(colorPrimary)
}

private val LocalGlobalColors = compositionLocalOf {
    LightColorPalette
}

@Composable
fun GlobalTheme(theme: GlobalTheme.Theme = GlobalTheme.Theme.Light,
                content: @Composable () -> Unit) {
    val targetColors =
        when(theme){
            GlobalTheme.Theme.Dark -> DarkColorPalette
            GlobalTheme.Theme.Light -> LightColorPalette
            GlobalTheme.Theme.Red -> RedColorPalette
            GlobalTheme.Theme.Green -> GreenColorPalette
        }
    CompositionLocalProvider(LocalGlobalColors provides targetColors) {
        MaterialTheme(
            shapes = Shapes,
            typography = Typography,
            content = content
        )
    }
}

setContent {
    var theme by remember{mutableStateOf(GlobalTheme.Theme.Dark)}
    GlobalTheme(theme) {
        Column() {
            Icon(Icons.Default.Lock,
                modifier = Modifier.size(48.dp),
                contentDescription = "",
                tint = GlobalTheme.colors.colorPrimary)
            Button(onClick = {
                theme = when(theme){
                    GlobalTheme.Theme.Dark -> GlobalTheme.Theme.Red
                    GlobalTheme.Theme.Light -> GlobalTheme.Theme.Dark
                    GlobalTheme.Theme.Red -> GlobalTheme.Theme.Green
                    GlobalTheme.Theme.Green -> GlobalTheme.Theme.Light
                }
            }) {
                Text(text = "Change",color = GlobalTheme.colors.colorPrimary)
            }
        }

    }
}

Solution 2:[2]

TL;DR

Applying a theme to a drawable within compose can be a bit tricky as Xml drawables can not access color definitions in Jetpack Compose. Therefore you should define your colors and themes in Xml and make Jetpack Compose aware of those values.

Use AndroidView within Compose for loading an xml drawable:

AndroidView(factory = { context ->
  val contextThemeWrapper = ContextThemeWrapper(context, R.style.RedTheme)
  val drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.your_drawable, contextThemeWrapper.theme)

  ImageView(context).apply { setImageDrawable(drawable) }
})

This way the drawable will respect the theme style you are passing to ContextThemeWrapper.

Define a Theme in Xml

Along with your defined themes RedTheme and GreenTheme you also need to declare those attributes: You also need to define the style attributes:

<declare-styleable name="ThemeStyle">
  <attr name="colorPrimary" format="color" />
  <attr name="colorSecondary" format="color" />
</declare-styleable>

Map Theme to Compose

Define an AppTheme class:

object AppTheme {
  val colors: AppColors
    @Composable
    @ReadOnlyComposable
    get() = LocalColors.current

  val style: Int
    @Composable
    @ReadOnlyComposable
    get() = LocalStyleRes.current
}

internal val LocalStyleRes: ProvidableCompositionLocal<Int> = staticCompositionLocalOf { R.style.RedTheme }

@Composable
fun AppTheme(
  @StyleRes styleRes: Int,
  context: Context,
  content: @Composable () -> Unit,
) {

  val themeReader = ThemeReader(context, styleRes)
  val colors = AppColors(
    primary = Color(themeReader.colorPrimary),
    secondary = Color(themeReader.colorSecondary),
  )

  CompositionLocalProvider(
    LocalColors provides colors,
    LocalStyleRes provides styleRes
  ) {
    content()
  }
}

And a AppColor class:

data class AppColors(
  val primary: Color,
  val secondary: Color,
)

internal val LocalColors: ProvidableCompositionLocal<AppColors> = staticCompositionLocalOf {
  AppColors(
    primary = Color.White,
    secondary = Color.White
  )
}

Read Theme from Xml

Create a ThemeReader class that can read attributes for a style theme.

class ThemeReader(
  context: Context,
  @StyleRes styleRes: Int
) {
  private val attributes: IntArray = intArrayOf(R.attr.primary, R.attr.success, R.attr.error)
  private val typedArray: TypedArray = context.obtainStyledAttributes(styleRes, attributes)

  val colorPrimary: Int = typedArray.getColor(R.styleable.ThemeStyle_colorPrimary, -1)
  val colorSecondary: Int = typedArray.getColor(R.styleable.ThemeStyle_colorSecondary, -1)
}

Loading the Xml Drawable

To ensure that your loaded xml drawable is aware of your theme colors you should not load your drawable with Compose like:

Image(
  painter = painterResource(iconTypeResId),
  contentDescription = null
)

rather than loading it the classic way as an ImageView and wrapping it into Compose AndroidView. Lets create a composable for this:

@Composable
fun XmlDrawable(
  @DrawableRes drawableResId: Int,
) {
  @StyleRes val styleRes: Int = AppTheme.style
  AndroidView(factory = { context ->
    val contextThemeWrapper = ContextThemeWrapper(context, styleRes)
    val drawable = ResourcesCompat.getDrawable(context.resources, drawableResId, contextThemeWrapper.theme)

    ImageView(context).apply { setImageDrawable(drawable) }
  })
}

Now we can apply what we have done.

class YourActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
      // instantiate our custom theme
      AppTheme(
        styleRes = R.style.RedTheme, // set the theme you wanna use
        context = requireContext()
      ) {
        XmlDrawable(drawableResId = R.drawable.your_drawable) // load the xml drawable
        // you can also access any AppColor within the composition tree like: AppTheme.colors.primary
      }
    }
  }
}

Reference

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Yshh
Solution 2