When should I use Android Jetpack Compose's Surface composable?
Asked Answered
G

3

93

There's a Surface composable in Jetpack Compose which represents a material surface. A surface allows you to set up 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 does it give me?

Glycoprotein answered 27/1, 2021 at 12:7 Comment(0)
G
106

The Surface composable makes the code easier as well as explicitly indicating 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:

rendered Surface

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 modifier 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 to 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 a 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.

Glycoprotein answered 27/1, 2021 at 12:7 Comment(1)
Blocking touch propagation might perhaps be the most important role of the Surface, compared to other content views/composables.Whenas
S
26

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.

enter image description here

Sheriesherif answered 19/7, 2022 at 3:50 Comment(0)
W
10

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)

Wilkison answered 12/9, 2021 at 21:4 Comment(2)
I think compose Card() is the equivalent of CardViewElviraelvis
Thanks to say that, this let me more clear surfaceRounder

© 2022 - 2024 — McMap. All rights reserved.