In CSS, shadows are specified by (dx, dy, blurRadius, colour) (I call it ShadowSpec).
This library implements such shadows for Android.
I know only two shadow implementations in Android SDK out of the box:
Paint#setShadowLayer and View#elevation.
ShadowLayer produces nice-looking shadows but works only with software rendering. A sheet with shadow could be big, containing many pixels, but this becomes even worse if you set SOFTWARE_LAYER on a whole full-screen RecyclerView in order to draw background properly.
Elevation is implemented somewhere deep inside RenderNode,
there's no direct control over this shadow, there's no such a thing
like Canvas#drawElevation. Also, speaking in terms of CSS, dx=0,
and both dy and blurRadius are driven by elevation.
Elevation colour is one more pain in the ass, but, well, I think you already know it.
There's also MaterialShapeDrawable which is actually
a poor man's elevation. Same problems as above apply plus drawing artifacts.
It also drops shadowColor's alpha; Drawable#setAlpha invocations
don't alter shadow transparency either.
It satisfied my needs during the era of Views, but not being actively maintained now.
-
RectWithShadow9-patch for static shadows -
RectShadowdynamic drawable (butspreadparameter is not supported) -
RectInnerShadowdynamic drawable (but it does not look Gaussian; nospreadeither;shadow.radius=0is buggy) -
RectItemsWithShadowsItemDecorationforRecyclerViewwith animatable properties (but it doesn't allow having inner and outer shadows at the same time, and requires some fixes for correct alpha animation) - Fix all the stuff mentioned above? Open issue!
- There's
Modifier.dropShadow()so you don't need me there!
repositories {
// Groovy:
maven { url 'https://jitpack.io' }
// Kotlin:
maven(url = "https://jitpack.io")
}
// module-level build.gradle:
dependencies {
implementation('com.github.Miha-x64:FiftyShades:-SNAPSHOT')
}RectWithShadow.createDrawable(
RectSpec(Color.WHITE, dp(20)),
ShadowSpec(dp(2), dp(3), dp(20), Color.BLACK),
)This will return a Drawable (a NinePatchDrawable wrapped in InsetDrawable, actually)
with a white rectangle, round corners (20dp radius),
and a black shadow blurred by 20dp and offset by (2dp; 3dp).
Pros:
- fast drawing (it's a 9-patch)
Cons:
- requires some memory (it's a 9-patch) (avoid creating multiple; use
drawable.constantState().newDrawable()) - shadow parameters can't be changed
- screws up View paddings, need to re-assign them after setting
background - draws out of bounds,
clipChildren=falseon parent layout is required
LayerDrawable(arrayOf(
RectShadow(dp(20), ShadowSpec(dp(2), dp(3), dp(20), Color.BLACK)),
RoundRectDrawable(Color.WHITE, dp(20)), // explained later
)).also { it.paddingMode = LayerDrawable.PADDING_MODE_STACK } // don't nest paddingsPros:
- properties of
RectShadowcan be altered:.cornerRadius(100500).shadow(nicerShadow) - they are also eligible for
Animatorframework:Shadow.{CORNER_RADIUS, SHADOW_COLOR, SHADOW_DX, SHADOW_DY, SHADOW_RADIUS}
Cons:
- multiple drawing operations
- still out of bounds (SW/HW layers work only inside bounds, he-he)
LayerDrawable(arrayOf(
RoundRectDrawable(Color.WHITE, dp(20)), // explained later
RectInnerShadow(dp(20), ShadowSpec(dp(2), dp(3), dp(20), Color.BLACK))
))Add Inner, make it draw after round rect, and that's it: inner shadow,
known as inset in CSS. Interface is the same.
It's just built-in GradientDrawable (a.k.a. <shape>):
fun RoundRectDrawable(@ColorInt color: Int, @Px radius: Int): Drawable =
GradientDrawable().apply {
setCornerRadius(radius)
// must set shape, color, or stroke AFTER radius https://twitter.com/miha_x64/status/1445927243251949576
setColor(color)
}You may want to create a RecyclerView, set clipChildren=false,
and set drawables with shadow as item backgrounds.
This will work 99% of time but fail miserably during item animations:
when alpha < 1, clipChildren becomes effectively true
because of intermediate buffer which has its bounds,
so you will have your shadows clipped.
The saviour is RectItemsWithShadows(rect, shadow) item decorator
with an already familiar constructor. Individual item properties are controllable
and animatable:
itemView.stateListAnimator = StateListAnimator().apply {
addState(intArrayOf(android.R.attr.state_selected),
ObjectAnimator.ofPropertyValuesHolder(null as Any?,
PropertyValuesHolder.ofFloat(DECOR_SHADOW_RADIUS, dp(32f)),
PropertyValuesHolder.ofInt(DECOR_SHADOW_COLOR, 0xFF_AAFFCC.toInt()).argb(),
PropertyValuesHolder.ofInt(DECOR_RECT_FILL_COLOR, 0xFF_AAFFCC.toInt()).argb(),
)
)
addState(intArrayOf(),
ObjectAnimator.ofPropertyValuesHolder(null as Any?,
PropertyValuesHolder.ofFloat(DECOR_SHADOW_RADIUS, dp(8f)),
PropertyValuesHolder.ofInt(DECOR_SHADOW_COLOR, 0x66_000000).argb(),
PropertyValuesHolder.ofInt(DECOR_RECT_FILL_COLOR, Color.WHITE).argb(),
)
)
}
private val argbEvaluator = ArgbEvaluator()
fun PropertyValuesHolder.argb(): PropertyValuesHolder = apply { setEvaluator(argbEvaluator) }