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?
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.
Jetpack Compose is built around composable functions. These functions let you define your app's UI programmatically by describing how it should look and providing data dependencies, rather than focusing on the process of the UI's construction (initializing an element, attaching it to a parent, etc.).
The key benefits of Jetpack Compose are that it speeds up testing and uses a single code base to write code. There is, therefore, less chance of producing errors. Let's look at the User Profile Image app that loaded images, and information about users from a rest API, using an intuitive design.
Compose allows you to do more with less code compared to XML. Compose is Intuitive. This means that you just need to tell Compose what you want to show the user. Compose is compatible with all your existing code: you can call Compose code from Views and Views from Compose.
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:
backgroundColor
is used in two places as well.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 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)
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))
) {
Column(
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))
) {
Column(
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With