Is it possible to adjust the tonalElevation (but not the shadowElevation) of Material Design 3 components?
It looks as though it's only possible to adjust both. Below is the implementation of a Floating Action Button in Material Design 3. The same problem exists with other components.
#Composable
fun FloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = FabPrimaryTokens.ContainerShape,
containerColor: Color = FabPrimaryTokens.ContainerColor.toColor(),
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: #Composable () -> Unit,
) {
Surface(
onClick = onClick,
modifier = modifier,
shape = shape,
color = containerColor,
contentColor = contentColor,
tonalElevation = elevation.tonalElevation(interactionSource = interactionSource).value,
shadowElevation = elevation.shadowElevation(interactionSource = interactionSource).value,
interactionSource = interactionSource,
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
// Adding the text style from [ExtendedFloatingActionButton] to all FAB variations. In
// the majority of cases this will have no impact, because icons are expected, but if a
// developer decides to put some short text to emulate an icon, (like "?") then it will
// have the correct styling.
ProvideTextStyle(
MaterialTheme.typography.fromToken(ExtendedFabPrimaryTokens.LabelTextFont),
) {
Box(
modifier = Modifier
.defaultMinSize(
minWidth = FabPrimaryTokens.ContainerWidth,
minHeight = FabPrimaryTokens.ContainerHeight,
),
contentAlignment = Alignment.Center,
) { content() }
}
}
}
}
There's an extension function on a ColorScheme class, that converts an Elevation into a Color: surfaceColorAtElevation().
TextField(
...
colors = TextFieldDefaults.textFieldColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
)
)
Also, there are six different Elevation levels in Material 3 (https://m3.material.io/styles/elevation/tokens):
Level 0: 0dp
Level 1: 1dp
Level 2: 3dp
Level 3: 6dp
Level 4: 8dp
Level 5: 12dp
Related
I am using the Card composable and I want it to be colored white.
But when I add some elevation to it, it changes color to more like the primaryContainer color.
I have seen documentation where they have somehting called as elevationOverlay. But couldn't find an example which says how to use it.
Here is my code:
Card(
modifier = Modifier.padding(top = 16.dp),
colors = CardDefaults.cardColors(containerColor = White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
}
I do know I can use Elevated card instead of card, but there is same problem with elevated card as well.
Also, this is a special case so I am applying the color manually
To resolve the issue of changing card color when modifying the card elevation in Jetpack Compose with Material Design 3, you can use the background modifier and pass it a Color object to set the desired color. Additionally, you can use the elevationOverlay parameter to set the overlay color.
Here's an updated example of your code:
Card(
modifier = Modifier.padding(top = 16. dp)
.background(color = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2. dp),
elevationOverlay = Color.White
) {}
After trying multiple ways found out that there is no way to override this except for to look at the Card.kt file from SDK and create something similar but disable the tonalColor(Thanks for hint #ianhanniballake that it is using tonalelevation)
Following code should do the work, until overriding is officially supported:
#Composable
fun CardWithoutTonalElevation(
modifier: Modifier = Modifier,
shape: Shape = CardDefaults.shape,
colors: Color = White,
border: BorderStroke? = null,
elevation: Dp = 0.dp,
content: #Composable ColumnScope.() -> Unit = {}
) {
Surface(
modifier = modifier,
shape = shape,
color = colors,
tonalElevation = 0.dp,
shadowElevation = elevation,
border = border,
) {
Column(content = content)
}
}
Using Button, I want to make its diminensions such that its border hugs its width, with a minimum width and height of 40dp. In the sample below, I like the looks of the BigNumber preview. It does not have any outside horizontal padding. The Default preview does have padding outside the border. How do I fix this without setting an absolute width? Consider this sample:
#Composable
fun BasketQuantityStepper(
quantityControlsViewState: QuantityControlsViewState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.basketQuantityStepperBackground)),
border = BorderStroke(dimensionResource(id = R.dimen.buttonBorderWidth), colorResource(id = R.color.basketQuantityStepperBorderColor)),
modifier = modifier
.heightIn(min = 40.dp)
.widthIn(min = 40.dp),
) {
Text(
text = "${quantityControlsViewState.currentQuantity}",
)
}
}
#Preview
#Composable
private fun PreviewDefault() {
BasketQuantityStepper(quantityControlsViewState = QuantityControlsViewState(
currentQuantity = 1,
minOrderQuantity = 1,
maxOrderQuantity = 10,
stepQuantity = 1
), onClick = {})
}
#Preview
#Composable
private fun PreviewBigNumber() {
BasketQuantityStepper(quantityControlsViewState = QuantityControlsViewState(
currentQuantity = 100,
minOrderQuantity = 1,
maxOrderQuantity = 1000,
stepQuantity = 1
), onClick = {})
}
Minimum dimension of Composables' touch area is 48.dp by default for accessibility.
You can remove it by wrapping your button with
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
}
but it's not advised to have Composable's smaller than accebility size. Icons, CheckBox, even Slider uses 48.dp by default.
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
Button(
onClick = {},
border = BorderStroke(2.dp, Color.LightGray),
modifier = Modifier
.border(2.dp, Color.Green)
.heightIn(min = 40.dp)
.widthIn(min = 40.dp),
) {
Text(
text = "$counter",
)
}
}
https://developer.android.com/jetpack/compose/accessibility
Remove Default padding around checkboxes in Jetpack Compose new update
I am trying to remove padding from TextButton but it wont work.
TextButton(
onClick = {},
modifier = Modifier.padding(0.dp)
) {
Text(
" ${getString(R.string.terms_and_conditions)}",
color = MaterialTheme.colors.primary,
fontFamily = FontFamily(Font(R.font.poppins_regular)),
fontSize = 10.sp,
)
}
I have tried setting the height and size in Modifier property as well but the padding is still present
Wrap the TextButton with CompositionLocalProvider to override the value of LocalMinimumTouchTargetEnforcement. This will only remove the extra margin but will not modify the defaultMinSize which is hardcoded.
CompositionLocalProvider(
LocalMinimumTouchTargetEnforcement provides false,
) {
TextButton(
onClick = {},
contentPadding = PaddingValues(),
) {
Text(
"Button",
color = MaterialTheme.colors.primary,
fontSize = 10.sp,
)
}
}
You cannot reduce padding with the padding modifier: it always adds an extra padding on top of the existing padding. See this reply for more details about the order of modifiers.
You can reduce TextButton padding with contentPadding argument, by specifying PaddingValues(0.dp), but this will not fully remove the padding.
If you need fully remove the padding, you can use the clickable modifier instead:
Text(
"getString(R.string.terms_and_conditions",
color = MaterialTheme.colors.primary,
fontFamily = FontFamily(Font(R.font.neris_semi_bold)),
fontSize = 10.sp,
modifier = Modifier
.clickable {
// onClick()
}
)
If you want to change the color of the ripple, as is done in TextButton, you can do it as follows:
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colors.primary),
) {
}
You can achieve it changing the contentPadding and applying a fixed size:
TextButton(
onClick = {},
contentPadding = PaddingValues(0.dp),
modifier = Modifier.height(20.dp).width(40.dp)
) {
Text(
"Button",
color = MaterialTheme.colors.primary,
fontSize = 10.sp,
)
}
If you come across unwanted margins. When you use onclick in any Button function, it sets propagateMinConstraints = true inside the view's surface - this will apply to unwanted margins. Example how solve this problem:
#Composable
fun ButtonWithoutOuterPadding(
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = RectangleShape,
elevation: Dp = 0.dp,
color: Color = Color.Transparent,
border: BorderStroke? = null,
contentAlignment: Alignment = Alignment.Center,
content: #Composable () -> Unit
) {
Box(
modifier
.shadow(elevation, shape, clip = false)
.then(if (border != null) Modifier.border(border, shape) else Modifier)
.background(
color = color,
shape = shape
)
.clip(shape)
.then(
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = LocalIndication.current,
enabled = true,
onClickLabel = null,
role = null,
onClick = onClick
)
),
contentAlignment = contentAlignment,
propagateMinConstraints = false
) {
content()
}
}
There's Surface composable in Jetpack Compose which represents a material surface. A surface allows you to setup things like background color or border but it seems that the same might be done using modifiers. When should I use the Surface composable and what the benefits it gives me?
Surface composable makes the code easier as well as explicitly indicates that the code uses a material surface. Let's see an example:
Surface(
color = MaterialTheme.colors.primarySurface,
border = BorderStroke(1.dp, MaterialTheme.colors.secondary),
shape = RoundedCornerShape(8.dp),
elevation = 8.dp
) {
Text(
text = "example",
modifier = Modifier.padding(8.dp)
)
}
and the result:
The same result can be achieved without Surface:
val shape = RoundedCornerShape(8.dp)
val shadowElevationPx = with(LocalDensity.current) { 2.dp.toPx() }
val backgroundColor = MaterialTheme.colors.primarySurface
Text(
text = "example",
color = contentColorFor(backgroundColor),
modifier = Modifier
.graphicsLayer(shape = shape, shadowElevation = shadowElevationPx)
.background(backgroundColor, shape)
.border(1.dp, MaterialTheme.colors.secondary, shape)
.padding(8.dp)
)
but it has a few drawbacks:
The modifiers chain is pretty big and it isn't obvious that it implements a material surface
I have to declare a variable for the shape and pass it into three different modifiers
It uses contentColorFor to figure out the content color while Surface does it under the hood. As a result the backgroundColor is used in two places as well.
I have to calculate the elevation in pixels
Surface adjusts colors for elevation (in case of dark theme) according to the material design. If you want the same behavior, it should be handled manually.
For the full list of Surface features it's better to take a look at the documentation.
Surface is a Box with a Modifier.surface() and material colors and elevation, it checks elevation of ancestors to be always on top of them, and only overload below blocking touch propagation behind the surface with pointerInput(Unit) {}.
#Composable
fun Surface(
modifier: Modifier = Modifier,
shape: Shape = RectangleShape,
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),
border: BorderStroke? = null,
elevation: Dp = 0.dp,
content: #Composable () -> Unit
) {
val absoluteElevation = LocalAbsoluteElevation.current + elevation
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalAbsoluteElevation provides absoluteElevation
) {
Box(
modifier = modifier
.surface(
shape = shape,
backgroundColor = surfaceColorAtElevation(
color = color,
elevationOverlay = LocalElevationOverlay.current,
absoluteElevation = absoluteElevation
),
border = border,
elevation = elevation
)
.semantics(mergeDescendants = false) {}
.pointerInput(Unit) {},
propagateMinConstraints = true
) {
content()
}
}
}
And Modifier.surface()
private fun Modifier.surface(
shape: Shape,
backgroundColor: Color,
border: BorderStroke?,
elevation: Dp
) = this.shadow(elevation, shape, clip = false)
.then(if (border != null) Modifier.border(border, shape) else Modifier)
.background(color = backgroundColor, shape = shape)
.clip(shape)
Another interesting thing is it is Box with propagateMinConstraints = true parameter which forces first descendant to have same minimum constraints or dimensions
Surface(
modifier = Modifier.size(200.dp),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier.size(200.dp),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier.size(200.dp)
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
)
}
}
In first example on Surface forces Column to have 200.dp size even though it has Modifier.size(50.dp).
In second example Box inside Column has 50.dp size because it's not a direct descendant of Surface.
In third example if we replace Surface(Box with propagateMinConstraints true) with Box it allows direct descendant to use its own constraints or dimensions.
Surface is the equivalent of CardView in view system.
By Surface, you can set elevation for the view (note that this is not the same with Modifier.shadow)
How to remove padding in an IconButton ? I want items in my column have the same start padding
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
IconButton(onClick = { }) {
Icon(asset = Icons.Filled.Search)
}
Text("Some text")
}
The space is due to accessibility touch targets and a default size of 48.dp.
Starting with 1.2.0 the best best way to change the default behaviour and remove the extra space is disabling the LocalMinimumTouchTargetEnforcement and applying a size modifier:
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
IconButton(
modifier = Modifier.size(24.dp),
onClick = { }
) {
Icon(
Icons.Filled.Search,
"contentDescription",
)
}
}
Pay attention because in this way it is possible that if the component is placed near the edge of a layout / near to another component without any padding, there will not be enough space for an accessible touch target.
With 1.0.0 the IconButton applies a default size with the internal modifier: IconButtonSizeModifier = Modifier.size(48.dp).
You can modify it using something like:
IconButton(modifier = Modifier.
then(Modifier.size(24.dp)),
onClick = { }) {
Icon(
Icons.Filled.Search,
"contentDescription",
tint = Color.White)
}
It is important the use of .then to apply the size in the right sequence.
Wrap the IconButton with CompositionLocalProvider to override the value of LocalMinimumTouchTargetEnforcement which enforces a minimum touch target of 48.dp.
CompositionLocalProvider(
LocalMinimumTouchTargetEnforcement provides false,
) {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = "Search",
)
}
}
If you use IconButton just for handling click listener, instead of:
IconButton(onClick = { // Todo -> handle click }) {
Icon(asset = Icons.Filled.Search)
}
You can use:
Icon(
asset = Icons.Filled.Search,
modifier = Modifier.clickable { // Todo -> handle click },
)
With this way you don't need to remove extra paddings.