During cetrain times I want to use my own color resource ColorGreen to change the background colour of my custom SmallTopAppBar, and during other times I don't want to use it so that the default black and white colours are used instead depending on the device's current theme. What is the best way to avoid a possible null pointer exception? Should an 'if null' statement be used in the custom toolbar code? Should null be used in the activity declaration where the underscores are?
Color.kt
val ColorGreen = Color(0,110,20,255)
Custom toolbar code
package com.mycompany.myapp.ui.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
#Composable
fun MySmallTopAppBar(
backgroundColor: Color,
title: String,
titleColor: Color
) {
SmallTopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = backgroundColor),
title = {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Start,
maxLines = 1,
color = titleColor
)
}
)
}
within MainActivity.kt
...
setContent {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Scaffold(
topBar = { MySmallTopAppBar(ColorGreen, getGreetingMessage(), __) }
) {
}
}
}
}
...
In such cases you need to inspect component source code to understand how it works.
In case of Text, default color value is Color.Unspecified - if this value is used, color will be taken from the style or from LocalContentColor (of style value is unspecified too). So if you want to follow default behaviour, you need to pass this value it too:
val backgroundColor: Color?
// ...
MySmallTopAppBar(backgroundColor, getGreetingMessage(), if (backgroundColor == null) Color.Unspecified else Color.SomeColor)
In case if smallTopAppBarColors default value is internal, plus since it may change in the future(as material 3 is in alpha so far), the most correct way would be something like this:
colors = if (backgroundColor != null)
TopAppBarDefaults.smallTopAppBarColors(containerColor = backgroundColor)
else
TopAppBarDefaults.smallTopAppBarColors(),
Related
I discovered today that MaterialTheme applies an alpha to Text's colour. As you can see from the example attached, when I change the background colour, the text's colour appears to be different because it has a transparency value. I can force set a colour (Text(color = MaterialTheme.colors.onBackground, ....)) and this works correctly but I don't want to have to do this for every single Text.
Why does MaterialTheme do this? How do I override this behaviour?
Compose and Material Compose Material version: 1.2.1
#Preview
#Composable
private fun Preview_Playground() {
MaterialTheme {
Box(Modifier.background(Color.Green)) {
Text("Test", fontWeight = FontWeight.ExtraBold, modifier = Modifier.alpha(1f))
}
}
}
With M2 (androidx.compose.material) the color of the Text is defined by the color parameter or applying a TextStyle.
The default value is Color.Unspecified.
If color = Color.Unspecified and style has no color set, this will be LocalContentColor mixed with LocalContentAlpha.current.
In the Text.kt you can find:
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
With M3 (androidx.compose.material3) it doesn't happen since LocalContentColor.current is not mixed:
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current
}
}
If you have to use M2, you can define a custom composable for your Text, or you can change the LocalContentAlpha in the theme for the whole application (not only the Text):
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes
){
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
content()
}
}
My Text colour is not the same as that defined in the theme despite it being inside Surface; it appears almost the same but not quite.
This simplistic sample layout is bare bones:
MyTheme {
Surface {
Column(Modifier.padding(12.dp)) {
Text(
text = "This is a line of text, uses surface",
)
Text(
text = "This is a line of text, forced white",
color = Color.White,
modifier = Modifier.padding(top = 4.dp)
)
}
}
}
My theme is simple:
#Composable
fun MyTheme(
content: #Composable () -> Unit,
) {
MaterialTheme(
colors = DarkColours,
...,
content = content
)
}
internal val DarkColours = darkColors(
surface = Color(0xFF043143),
onSurface = Color.White,
... // None of the other colours are close to off-white
)
Even if I explicitly specify the colours to use in the surface, the top Text is still off-white (no change):
Surface(
color = Color(0xFF043143),
contentColor = Color.White
) { ... }
Elevation is 0.0dp and uses DefaultElevationOverlay so shouldn't have any effect on contentColor
LocalContentColor.current reports Color(1.0, 1.0, 1.0) when printed by the first Text.
Figured it out while typing this question up so figured I'd share it.
LocalContentAlpha was set to 0.87 inside the contents of MaterialTheme. This is because of its line
LocalContentAlpha provides ContentAlpha.high,
which ultimately resolves to this due to the fact that I'm using a material Color scheme with isLight = false (thanks to darkColors).
private object LowContrastContentAlpha {
const val high: Float = 0.87f
const val medium: Float = 0.60f
const val disabled: Float = 0.38f
}
A workaround is to re-set alpha back to 1.0:
MaterialTheme(
colors = DarkColours,
...
) {
CompositionLocalProvider(
// Undo our "dark" colours triggering Material theme
// to use a low contrast alpha (87%).
LocalContentAlpha provides 1.0f,
content = content
)
}
(or just use lightColors / set isLight = true if you can afford to do this)
This kind of blows my mind, that Material would just change the alpha of literally all of my app's content - I can't find any doc on why it does this either in the material spec!
It happens only with M2 and it doesn't depends on the Surface, but on the Text itself.
The color in the Text is defined by the color parameter or applying a TextStyle. The default value is Color.Unspecified.
If color = Color.Unspecified and style has no color set, this will be LocalContentColor mixed with LocalContentAlpha.current.
In the Text.kt you can find:
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
As you described in your answer in the MaterialTheme is defined:
fun MaterialTheme( /* .. */ ) {
//...
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high, //0.87f
//...
)
In M3 (androidx.compose.material3) it doesn't happen since LocalContentColor.current is not mixed:
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current
}
}
I create a project with the wizard Empty Compose Activity of Android Studio. I use Text("Hello") to display a text.
I know the font size of the text is 16.sp by looking source code, but how can I know which color is displayed of the text?
BTW, I look at the source code , the color of font is specified as Color.Unspecified, I don't know what color will be displayed for Color.Unspecified.
Source Code
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
...
)
class TextStyle
#OptIn(ExperimentalTextApi::class)
internal constructor(
...
) {
...
#OptIn(ExperimentalTextApi::class)
constructor(
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
...
}
val Unspecified = Color(0f, 0f, 0f, 0f, ColorSpaces.Unspecified)
If we look at the source code of Text composable, we can find,
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
Here color is the argument passed to the Text composable, which defaults to Color.Unspecified if no color is passed.
We see a method takeOrElse is used.
takeOrElse definition
inline fun Color.takeOrElse(block: () -> Color): Color = if (isSpecified) this else block()
Color.isSpecified definition
inline val Color.isSpecified: Boolean get() = value != Color.Unspecified.value
In other words, we can read this code as set textColor as the color if it is not equal to Color.Unspecified, else use the value in the given lambda.
Since we already know the color is Color.Unspecified if no color is specified (from above point 1), the value in the lambda will be used.
Value in lambda
The lambda checks if there is a color in the style TextStyle provided (either by user or by default value LocalTextStyle.current). If there is one, it will be used. Else the value of the below will be used.
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
LocalContentColor, LocalContentAlpha and LocalTextStyle are provided using CompositionLocalProvider.
The color in the Text is defined by the color parameter or applying a TextStyle. The default value is Color.Unspecified.
If color = Color.Unspecified and style has no color set, this will be LocalContentColor which provides a default Color.Black color if not specified.
As the other answers explain which color is interpreted when color is Color.Unspecified (which is LocalContentColor.current), I will answer why there is a Color.Unspecified instead of Color.Transparent
Because Color is an inline class, this represents an unset value without having to box the Color object. It will be treated as Transparent when drawn. A Color object can be compared with Color.Unspecified for equality check, or the property isUnspecified of the object can be used to check for the unset value (or Color#isSpecified for any color that isn't Color.Unspecified).
This approach is used with Offset.Unspecified, Size.Unspecified and other classes that have an equivalent 'Unspecified' field for equality checks.
I don't know what color will be displayed for Color.Unspecified.
If you need to get the color dynamically at runtime, for example if you dynamically set colors and need them to change based on their current color you can use
Text("Hello World",
onTextLayout = {
it.layoutInput.style.color
}
)
Which will return
And if you assign a Color
Text("Hello World",
color = Color.Red,
onTextLayout = {
it.layoutInput.style.color
}
)
Hey I am new in jetpack compose. I tried to set window background color black and white according to theme. When I created custom theme and set background color my text color will be black.
theme.kt
package com.vivek.sportsresult.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.systemuicontroller.rememberSystemUiController
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
background = Color.Black
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
background = Color.White
)
#Composable
fun SportsResultTheme(
darkTheme: Boolean = isDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: #Composable () -> Unit
) {
val systemUiController = rememberSystemUiController()
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> {
DarkColorScheme
}
else -> {
LightColorScheme
}
}
if (darkTheme) {
systemUiController.setSystemBarsColor(
color = Color.Black
)
} else {
systemUiController.setSystemBarsColor(
color = Color.White
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
#Composable
fun isDarkTheme() = isSystemInDarkTheme()
#Composable
fun getBackgroundColor() = if (isDarkTheme()) {
DarkColorScheme.background
} else {
LightColorScheme.background
}
MainActivity.kt
package com.vivek.sportsresult
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.vivek.sportsresult.ui.theme.SportsResultTheme
import com.vivek.sportsresult.ui.theme.getBackgroundColor
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SportsResultTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = getBackgroundColor()
) {
Log.e("TAG", "onCreate: ")
Greeting("Android")
}
}
}
}
}
#Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
SportsResultTheme {
Greeting("Android")
}
}
When I tried to use color = getBackgroundColor() it not changing text color but if use color = MaterialTheme.colorScheme.background it working correct on dark and white theme. I don't understand why? Can someone guide me on this?
Actual Output
Expected Output
When you use MaterialTheme.colorScheme.background it works correctly because in that case the Surface composable is able to determine the correct content color (that would be the contentColor parameter) based on the background color which you set (which is the color parameter).
However, when you use getBackgroundColor() in dark mode you get back Color.Black and since you are running your code on a device with Android 12 your theme is a dynamic color theme which was created here
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
and apparently does not contain any color that is exactly Color.Black, i.e. that is exactly Color(0xFF000000), and thus the Surface composable is unable to determine the correct content color.
When a given color is not found in the theme then the contentColor is set to LocalContentColor.current and it just happens that in your case this results in a black text on a black background.
You have 2 options:
you can use MaterialTheme.colorScheme.background and let it determine the contentColor automatically based on your theme.
you can set both color and contentColor yourself like this
Surface(
modifier = Modifier.fillMaxSize(),
color = getBackgroundColor(),
contentColor = /* get and set some content color */,
)
This behavior is due to how Surface contentColor parameter default value is implemented by calling contentColorFor(color). See the Surface composable implementation and contentColorFor implementation (in Android Studio you Ctrl/Cmd+click on them, or open the context menu > Go To > Implementation).
In Jetpack Compose, TopAppBar shows default background color irrespective of what we added to themes.xml.
So how can we change TopAppBar background color from themes.xml so it's applied globally to the App?
TopAppBar(
title = { Text("Activity") },
navigationIcon = {
IconButton(onClick = { onBackPressed() }) {
Icon(Icons.Filled.ArrowBack, contentDescription = null)
}
}
)
themes.xml
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight">
<!-- Primary brand color. -->
<item name="colorPrimary">#android:color/black</item>
<item name="colorPrimaryVariant">#android:color/holo_orange_dark</item>
<item name="colorOnPrimary">#android:color/white</item>
</style>
Note : we can change it through backgroundColor attribute of TopAppBar but here I want to achieve it globally.
The accepted answer explains what to do adequately. There is one thing you might need to keep in mind, though.
TopAppBar uses MaterialTheme.colors.primarySurface as the background color, and it's defined as the following.
[androidx/compose/material/Colors.kt]
val Colors.primarySurface: Color get() = if (isLight) primary else surface
In other words, if the device is in the light mode, it uses primary, and in the dark mode, it uses surface.
Suppose I have a theme with everything as default values.
private val DarkColorPalette = darkColors()
private val LightColorPalette = lightColors()
#Composable
fun ComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: #Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
)
lightColors and darkColors have the following values by default.
fun lightColors(
primary: Color = Color(0xFF6200EE),
...
)
fun darkColors(
surface: Color = Color(0xFF121212),
...
)
When the device is in the light mode, primary (0xFF6200EE) will be TopAppBar's background color.
But when the device is in the dark mode, surface (0xFF121212) is not TopAppBar's background color. It's slightly lighter; 0xFF282828 to be exact.
The reason is TopAppBar has a built-in elevation, which is 4.dp by default.
[androidx/compose/material/AppBar.kt]
#Composable
fun TopAppBar(
...
elevation: Dp = AppBarDefaults.TopAppBarElevation // 4.dp
) {
This most likely won't cause a problem, but it might matter if you want to apply the exactly same background color to somewhere else, such as setting the same color for the status area's background.
val systemUiController = rememberSystemUiController()
systemUiController.setSystemBarsColor(color = Color(0xFF282828))
Note that for this particular case, it would be easier if you just go full screen and add padding at the top.
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
MyAppTheme {
Surface(
modifier = Modifier.systemBarsPadding().fillMaxSize(), // <--
) {
...
}
}
}
For the TopAppBar and the other composables the basis of theming is the MaterialTheme composable and not the AppCompat/MaterialComponents XML themes.
The TopAppBar uses the backgroundColor attribute.
The default value is defined by MaterialTheme.colors.primarySurface.
You can change these colors globally defining your theme adding your Colors and passing them to a MaterialTheme:
private val LightColors = lightColors(
primary = Yellow500,
//...
)
Otherwise you can simply use :
TopAppBar(
title = { Text("Activity") },
backgroundColor = /*...*/,
/* ... */
)
If you want to use the AppCompat XML themes in Jetpack Compose you can use the AppCompat Compose Theme Adapter provided by the accompanist library.
When you create a new project in studio, you get a file named Theme.kt, in which there are color palettes named lightColors and darkColors. You should modify the parameters of those values to achieve the result globally in your app.