Surface content colour slightly different to expected - android

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

Related

Unexpected Text colour alpha in Jetpack Compose Material Theme

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()
}
}

How does Text("Hello") display font color when I use Jetpack Compose with default settings?

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
}
)

Android Compose How to Fix "ComposableModifierFactory" and "UnnecessaryComposedModifier" Lint Warnings?

How can I create reusable modifiers without android compose lint rules throwing a fit?
I don't want to have to copy/paste the same modifiers for every screen within my app, I would rather just create an extension function I can call like this,
Box(modifier = Modifier.defaultFillScreen())
But that extension function, shown below, keeps throwing lint errors.
#Composable
fun Modifier.defaultFillScreen() = this.then(Modifier
.fillMaxWidth()
.navigationBarsWithImePadding()
.verticalScroll(rememberScrollState())
.padding(dimensionResource(id = R.dimen.standard_padding)))
Gives me the following lint error:
ComposableModifierFactory: Modifier factory functions should not be
marked as #Composable, and should use composed instead
When I make that change I then get a new lint error:
fun Modifier.defaultFillScreen() = composed { this.then(Modifier
.fillMaxWidth()
.navigationBarsWithImePadding()
.verticalScroll(rememberScrollState())
.padding(dimensionResource(id = R.dimen.standard_padding))) }
UnnecessaryComposedModifier: Unnecessary use of Modifier.composed
How can I create a reusable modifier without compose complaining about it? Writing the same 5 lines of modifier code for every screen is not an acceptable answer.
dependencies:
'androidx.activity:activity-compose:1.3.1',
'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07',
'androidx.compose.material:material:1.0.5',
'androidx.navigation:navigation-compose:2.4.0-alpha06',
'androidx.compose.ui:ui:1.0.5',
'androidx.compose.ui:ui-tooling:1.0.5'
Android studio:
Android Studio Arctic Fox | 2020.3.1 Build
#AI-203.7717.56.2031.7583922, built on July 26, 2021
I don't see the same warning with compose 1.2.0-alpha and Android Studio Bubblebee, it used to appear when i use Modifier.composed without state.
Purpose of Modifier.composed is having stateful modifiers which you use with remember, LaunchedEffect. When you don't have a state associated with your Modifier you should you Modifier.then instead
fun Modifier.composedBackground(width: Dp, height: Dp, index: Int) = composed(
// pass inspector information for debug
inspectorInfo = debugInspectorInfo {
// name should match the name of the modifier
name = "myModifier"
// add name and value of each argument
properties["width"] = width
properties["height"] = height
properties["index"] = index
},
// pass your modifier implementation that resolved per modified element
factory = {
val density = LocalDensity.current
val color: Color = remember(index) {
Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
}
// 🔥 Without remember this color is created every time item using this modifier composed
// val color: Color = Color(
// red = Random.nextInt(256),
// green = Random.nextInt(256),
// blue = Random.nextInt(256),
// alpha = 255
// )
// add your modifier implementation here
Modifier.drawBehind {
val widthInPx = with(density) { width.toPx() }
val heightInPx = with(density) { height.toPx() }
drawRect(color = color, topLeft = Offset.Zero, size = Size(widthInPx, heightInPx))
}
}
)
This is just a sample composed example. If you change from remember you will see that at each recomposition random color will change.
And without composed it will give error #Composable invocations can only happen from the context of a #Composable function if you use remember like the snippet below.
fun Modifier.nonComposedBackground(width: Dp, height: Dp, index: Int) = this.then(
// add your modifier implementation here
Modifier.drawBehind {
val color: Color = remember(index) {
Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
}
val widthInPx = width.toPx()
val heightInPx = height.toPx()
drawRect(color = color, topLeft = Offset.Zero, size = Size(widthInPx, heightInPx))
}
)

Change TopAppBar background color from themes.xml

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.

Default Style Jetpack Compose

Does somebody know how to change default style to button?
Style in xml:
<item name="materialButtonStyle">#style/ButtonStyle</item>
And I want to convert it to Jetpack Compose.
In default compose sample(Android Studio Canary) You can see ui.theme folder and it's a analog of values folder but without Strings and Dimens. So how I can add Strings and Dimens to this compose folder?
As described in the nglauber answer you can customize the shape, typography and color in your theme, or in the Button parameters.
Also you can override these values and build a default button style.
Something like:
#Composable
fun DefaultButtonStyle(content: #Composable () -> Unit) {
MaterialTheme(
//override the shape
shapes = MaterialTheme.shapes.copy(small = CutCornerShape(12.dp)),
//Override the typography.button using the merge method
typography = MaterialTheme.typography.copy(
button = MaterialTheme.typography.button.merge(TextStyle(fontSize = 20.sp))),
//override the colors define in the material theme
colors = MaterialTheme.colors.copy(
primary = Color.Yellow,
onPrimary = Color.Blue)
) {
content()
}
}
Then just use it with:
DefaultButtonStyle() {
Button(onClick = { /*....*/ }) {
Text(text = "BUTTON")
}
}
If you look into the Button source, you'll notice that it uses a couple of default values that you can customize (via params or via custom style).
shape: Uses MaterialTheme.shapes.small (you can customized this field in your style);
val shapes = Shapes(
small = CutCornerShape(4.dp), // << here
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
colors: which is an instance of ButtonColors that provides backgroundColor, contentColor, disabledBackgroundColor and disabledContentColor. Look into the Button.buttonColors function to see how to customize the colors for your button.
In terms of text, the Button component gets the text style from MaterialTheme.typography.button, so you can override this field in your style to customize your button's text.
val typography = Typography(
...
button = defaultTypography.button.copy(
fontFamily = yourFontFamily,
color = Color.Yellow
)
)
For text and dimensions you can continue using XML files (res/values) and refer to them using stringResource(id) and dimensionResource(id) functions respectively.

Categories

Resources