Skip to content

Color compat

zed-alpha edited this page Feb 28, 2026 · 14 revisions

Package: com.zedalpha.shadowgadgets.view


The library now offers a mechanism by which to add color to shadows on versions prior to API level 28 (Pie), when the native ambient and spot colors were added to the SDK. The new View.outlineShadowColorCompat extension property can be used to set a color with which to tint shadows on versions before Pie.

@get:ColorInt
@setparam:ColorInt
var View.outlineShadowColorCompat: Int

Its companion property View.forceOutlineShadowColorCompat is available to force this method to be used on newer versions as well, for the purposes of consistency, comparison, testing, etc.

var View.forceOutlineShadowColorCompat: Boolean

While forceOutlineShadowColorCompat is set to true on a View, its outlineAmbientShadowColor and outlineSpotShadowColor should not be modified. In order for the tint to apply correctly, the native shadow needs to be pure black. There is no guaranteed behavior if those values are changed while color compat is in use.

Color compat shadows are always clipped to their parents' bounds, since they require a sized compositing layer.


Blending colors

At the SDK level, it's only possible to tint the composited ambient and spot shadows as a whole rather than individually, hence the single color to replace the two native ones in later versions. As a convenience, the library includes a helper class – ShadowColorsBlender that can be used to blend the ambient and spot colors for later versions into a single color for the compat functionality.

Do note that this is completely optional; you can use whatever valid color you like with outlineShadowColorCompat.

class ShadowColorsBlender(context: Context)

This helper class uses the Context's theme alphas for ambient and spot shadows to proportionally blend those colors into a single value to be used with outlineShadowColorCompat. The Context passed must have the relevant theme for the current Window, but that's only a concern if you've changed the value of android:ambientShadowAlpha or android:spotShadowAlpha for a given Activity or Dialog.

The class has two functions:

  • fun blend(@ColorInt ambientColor: Int, @ColorInt spotColor: Int): Int – Returns a @ColorInt calculated by blending the passed colors in proportion to their respective theme alphas. Unfortunately, this has to be called and set on the target View manually, since the native color properties and attributes didn't exist at all on older versions.

    Please note that the blending calculation gives decent results only if the ambientColor and spotColor themselves are opaque. I haven't yet wrapped my head around how to satisfactorily blend two "non-opaque" light sources with additional multiplying alphas.

  • fun onConfigurationChanged() – To be called from the corresponding method in the UI component; i.e., the Activity, Fragment, etc. This is only necessary if you've set different alpha values for different themes, and you're handling configuration changes manually.


Independent use

Color compat can be used on its own, in which case the intrinsic shadow is replaced with an unclipped instance that's more performant, but still displays the original artifact. Consequently, unclipped color compat shadows are restricted to the Background plane, to ensure that they draw behind their targets. If such a shadow is added to either other plane, it will still disable the intrinsic one, but it will not draw itself, and there will be an explicit error log debug builds.

Views that are the root of a hierarchy cannot use color compat without the clip feature enabled as well, since there is no Background plane available there.

Also, to clarify, ViewPathProvider is only relevant to the clipOutlineShadow functionality. If you need only color compat, you don't have to worry about that at all.


Performance and overhead

It should be noted that any kind of layer compositing is always more expensive than a straight draw, and the mechanism used here is no different. A plain black clipped shadow brings no more overhead than adding, say, one more regular CardView to the layout. Tinting these shadows, however, requires additional compositing layers, and therefore approximately doubles the cost per shadow.

In an effort to bring that down somewhat, color layers are consolidated and shared where possible; namely, in the Foreground and Background planes. In each of those planes, the shadows are drawn together all at once, rather than interleaved between siblings. This allows the shadows in those planes to be sorted and grouped in such a way that all those tinted with the same color are drawn in single layer (per plane). This isn't possible in the Inline plane, so each and every inlined color compat shadow requires its own separate layer.

Admittedly, this feature was developed mainly just to see if it could be done, but it turned out to be as stable and robust as the core clip routine, so I think it's not unreasonable to offer it here and let the user decide if the overhead is acceptable. Great pains were taken to ensure that this optional feature does not interfere with or degrade the core fix in any way, and there should be no discernible decline in the behavior or performance of the plain clipped shadows.


Examples

The demo app has three pages at the end for the color compat functionality, the first of which has setups showing native shadow colors alongside a color compat example, for both Views and Compose.

The second page demonstrates ShadowDrawable's color compat functionality, which is exposed through a simple @ColorInt property.

The last page is a stress test for color compat that has a couple of setups that are, I would imagine, about as worst-case as it should get in the average app. The various relevant tools in Developer options – e.g., Profile GPU/HWUI rendering – can give you an idea of how much more expensive color compat shadows are compared to ones that are only clipped.