-
Notifications
You must be signed in to change notification settings - Fork 5
Color compat
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: IntIts 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: BooleanWhile 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.
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@ColorIntcalculated by blending the passed colors in proportion to their respective theme alphas. Unfortunately, this has to be called and set on the targetViewmanually, 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
ambientColorandspotColorthemselves 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., theActivity,Fragment, etc. This is only necessary if you've set different alpha values for different themes, and you're handling configuration changes manually.
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.
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.
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.