Medium Engineering

Stories from the team building Medium.

Follow publication

Custom color elevation effect in Compose

--

At Medium Engineering we’re in the process of moving to Compose. Today I found some funny stuff I wanted to share quickly.

I was having an elevated surface where there was no illumination effect on it so I had to look at why and how I could customize it.

no illumination in the 6 dp elevated surface

I was wondering why our colors weren’t illuminated in dark theme with Compose like described in material documentation with the following code:

Surface(
Modifier.height(40.dp),
color = MediumTheme.colors.backgroundNeutralPrimary,
shape = CircleShape,
elevation = 6.dp
) {
// content
}

Like every time a Compose behavior surprised me, I jumped into the composable code so here I follow my color into the Surface code with some cmd + click to see what I missed.

I ended up to:

@Composable
private fun Surface(
modifier: Modifier,
shape: Shape,
color: Color,
contentColor: Color,
border: BorderStroke?,
elevation: Dp,
clickAndSemanticsModifier: Modifier,
content: @Composable () -> Unit
) {
val elevationOverlay = LocalElevationOverlay.current
val absoluteElevation = LocalAbsoluteElevation.current + elevation
val backgroundColor = if (color == MaterialTheme.colors.surface && elevationOverlay != null) {
elevationOverlay.apply(color, absoluteElevation)
} else {
color
}
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalAbsoluteElevation provides absoluteElevation
) {
Box(
modifier
.shadow(elevation, shape, clip = false)
.then(if (border != null) Modifier.border(border, shape) else Modifier)
.background(
color = backgroundColor,
shape = shape
)
.clip(shape)
.then(clickAndSemanticsModifier),
propagateMinConstraints = true
) {
content()
}
}
}

In this code we can find:

val backgroundColor = if (color == MaterialTheme.colors.surface && elevationOverlay != null) {
elevationOverlay.apply(color, absoluteElevation)
} else {
color
}

This basically means that if the provided color to the surface is not the same as the default surface color you defined in the Material theme the elevation overlay color won’t be applied. I found this is lacking flexibility regarding how colors are applied but I do understand the goal is to enforce a strong link to the material theme (so it won’t apply well to a custom theme).

Anyway, I wanted to have a more custom behavior than the default provided by material, in the previously shared code, I found a good inspiration on how I could achieve what I wanted.

Following:

elevationOverlay.apply(color, absoluteElevation)

I found the default elevation overlay in the Compose source which is:

private object DefaultElevationOverlay : ElevationOverlay {
@ReadOnlyComposable
@Composable
override fun apply(color: Color, elevation: Dp): Color {
val colors = MaterialTheme.colors
return if (elevation > 0.dp && !colors.isLight) {
val foregroundColor = calculateForegroundColor(color, elevation)
foregroundColor.compositeOver(color)
} else {
color
}
}
}
@ReadOnlyComposable
@Composable
private fun calculateForegroundColor(backgroundColor: Color, elevation: Dp): Color {
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
val baseForegroundColor = contentColorFor(backgroundColor)
return baseForegroundColor.copy(alpha = alpha)
}

wich compute a default foreground color to be composite on the foreground of the source color (the default surface material theme one if no modification).

Inspired by this we could implement our design team wish: having the color composite over another color from our theme with an alpha percentage relative to the elevation and here we go:

@Composable
fun Color.applyElevationOverlay(elevation: Dp = 0.dp): Color {
if (!isSystemInDarkTheme()) return this // we don't need this in light theme
val absoluteElevation = LocalAbsoluteElevation.current + elevation
return MediumTheme.colors.backgroundNeutralQuaternary
.copy(alpha = (absoluteElevation.value) / 100f)
.compositeOver(this)
}
  1. We make no change in the light theme
  2. We compute the elevation with the LocalAbsoluteElevation so that it can be applied uniformly to all same elevation components. But we need the elevation parameter to support it to the first elevated component (see where we call this for example)
  3. We grab our other color from our theme, apply the alpha percentage, and composite it over the targeted color.

Remember the first code sample we simply call

Surface(
Modifier.height(40.dp),
color = MediumTheme.colors.backgroundNeutralPrimary.applyElevationOverlay(6.dp),
shape = CircleShape,
elevation = 6.dp
) {
// content
}

We have to pass the 6 dp value to applyElevationOverlay because LocalAbsoluteElevation will only contain this extra elevation on the surface’s component.

And here it comes:

better

That’s it, let me know what you think about it and if you have any questions drop them in the comments. Also I will try to share more quickies about Compose when founding one so hit that follow if want to see more :)

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Responses (4)

Write a response