'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 |