- Playing with elevation in Android 🥧 (part 1)
- Coloured shadows
- Opacity woes
- The cold numbers
- Playing with elevation in Android 🥧 (part 2)
- Lateral thinking
- Colours for everyone
- Don’t try this at home
- Playing with elevation in Android
- What is that shadow all about
- Elevation tweaks in action
- One last thing before you go
Playing with elevation in Android 🥧 (part 1)
I recently wrote an article about elevation in Android, showing how you can hack around framework restrictions to obtain elevation shadows that are different than what the Material Design guidelines mandate.
Since then, there’s been a few interesting developments on the topic, and this follow-up article will cover them. Come for the shadows, stay for the code!
As a side note, I have recently pushed a major update to the app that accompanies last year’s blog post. Uplift has now hit version 3, and there’s a lot more than meets the eye in that release.
As usual, you can download Uplift from the Play Store, and the full source code is available on GitHub.
Coloured shadows
The biggest change in Uplift 3 is the new controls at the bottom of the shadow settings panel that allow you to tint the elevation shadows.
This new feature in the app will only work on Android P and later, because it relies on two new APIs introduced in Android Pie.
The new APIs are setOutlineAmbientShadowColor and setOutlinePostShadowColor and their attribute counterparts, outlineAmbientShadowColor and outlineSpotShadowColor . These two APIs finally allow us to control the shadow colour for views with elevation. But why two distinct values? And what do ambient and spot mean?
If you recall, in the previous article we had this image:
As you can see, the elevation shadow is really two shadows combined together. One is generated by a virtual spotlight positioned above the top of the screen, and is referred to in the above image as the key light; the other by ambient lighting. The former is the source of the key light and casts a harder shadow that is slightly offset towards the bottom of the screen, and the latter casts a subtle, soft shadow that surrounds the objects and is aligned to it.
The two APIs are each controlling the colour of one of the two shadows that compose the elevation shadow. It allows you to have one but not the other by simply setting the colour to fully transparent. I cannot think of a reason why you would ever want to have them in different colours, or only one of the two, though.
Opacity woes
You will notice, though, that setting a fully opaque colour for a shadow does not make your shadow very opaque. This is a fully opaque black shadow ( #FF000000 ), and yet it looks very soft:
The documentation explains why this is:
The opacity of the final spot shadow is a function of the shadow caster height, the alpha channel of the outlineSpotShadowColor (typically opaque), and the R.attr.spotShadowAlpha theme attribute.
The same goes for outlineAmbientShadowColor and `ambientShadowAlpha`. The values in the Material and AppCompat themes as of API 28 are 0.039 (3.9%) for ambientShadowAlpha and 0.19 (19%) for spotShadowAlpha . The only exception is that TV devices have higher values, set to 0.15 (15%) and 0.3 (30%) respectively:
The cold numbers
If we look at the colours we are using, it’s easy to see how things get very subtle even with fully black shadow colours. Firstly, the documentation mentions that the shadow casters influence the shadow opacity: those casters are currently fixed. The ambient shadow is cast by an omnidirectional light source, which has no actual position in space, while the spot shadow is cast by a light source positioned at (0dp, 0dp, 600dp) with an 800dp radius.
The light source position is not something we really want to exert any control upon, and we thus don’t consider it in our calculations except to acknowledge that it has an influence.
Then there is the aforementioned alpha factor, which is determined by ambientShadowAlpha and spotShadowAlpha , both determined by the Material theme and without a way to change them at runtime. This is the real reason why our shadows look so subtle. In the example of the spot shadow:
So it looks like we’re destined to have very subtle and muted shadows: we can’t control the light source position, and we’re constrained to having at most 3.9% or 19% opacity (for ambient and spot respectively). Look at these fully saturated elevation shadows as rendered by Android P:
It’s just disappointing. This new API seems designed to prevent us from going overboard with our crazy shadow antics, but what if we could get the bolder, colourful shadows we crave?
In the next part of the article we’ll explore some… creative solutions to get the shadow tinted the way we want, bypassing some of the limitations the OS puts on us.
Источник
Playing with elevation in Android 🥧 (part 2)
In the first part of this article we saw the new APIs introduced in Android Pie that allow us to have coloured elevation shadows. Our ambition had been crushed by the cruel design of elevation shadows APIs, which impose abysmal values for the alphas, thus making even the most lively shadow a pale disappointment.
Is that all there is to it? Turns out, no: I figured out a way around it.
Lateral thinking
It turns out that, while the ambientShadowAlpha and spotShadowAlpha values are built into a theme at build time and cannot be changed at runtime, we do have full control over the values we pass to setOutlineAmbientShadowColor and setOutlineSpotShadowColor . Those are full ARGB colours, so if only we could get rid of the additional factor coming in from ambientShadowAlpha and spotShadowAlpha , we could fully control the shadows!
Although there’s no Java/Kotlin API to change them, those are still attributes we can set on the theme, and they act as a multiplication factor. What we want is to set the values to 1.0 so that the only actual factor left — besides the light source which is fixed and does not influence the shadows’ colour much — is the alpha channel on the outline shadow colours we set. Bingo!
We just need a couple other tricks to avoid breaking shadows for pre-Pie devices; in particular, we need a “normal” base theme that doesn’t have any overrides for the shadow alphas (which are available since Lollipop), and then override the alpha values for API 28+, setting them to 1.0.
Lastly, we set a default shadow colour for API 28+ that has the default alpha values premultiplied in. This way all the shadow will still look like they normally would, but we can still override the colour, and alpha, where we want.
Colours for everyone
We can now have multicoloured shadows, if we so wish, that actually look coloured! There are interesting potential applications for these hacks, such as having some shadows in the app being used to highlight elements, or to obtain particular effects; for example, Mike Wolfson suggested matching an image’s extracted palette colours with the elevation shadow.
But we can also get crazy and crank up the tackiness to 11! Here we loop the shadow’s hue values with only a fully opaque ambient shadow:
And this is how it looks with 20% alpha for the ambient shadow and 78% for the spot shadow:
Note how you can now control the opacity of the shadows at runtime too, by just varying the alpha component of the shadow colours, and the translationZ :
Don’t try this at home
We’ve said that the light source position for the spot light source is fixed at (0dp, 0dp, 600dp) , and the light has an 800dp radius, but can that be changed? Not that there is literally any good reason to do it… but just because it’s fun. You should really never, ever do this.
The values encoding the position and size of the light are defined in AOSP by three resources (there is no hardcoded x position for the light, it is centered on the screen):
This means we can theoretically adopt a similar approach as for the alpha, and modify the theme we’re using to override any of those values… except it is not that simple. The lightY , lightZ and lightRadius attributes are private, which means that just overriding them in the theme like this:
You will get a build error:
Luckily enough, you can bypass the AAPT private resource validation by prepending an * to the name of the attribute:
And you’ll end up with an atrocity like this:
No, it’s not upside down. It’s just that the light source is now positioned 1000dp down the Y axis, which goes towards the bottom of the screen. So yeah, we proved it can be done (for science! 👨🔬), but you should really really — really! — never do it.
Источник
Playing with elevation in Android
Everyone knows that Material Design has shadows. Most know that you can control the virtual Z coordinate of Material elements in Android by using the elevation property, to control the shadow. Very few know that there’s so much more you can do to tweak the shadows your UI elements cast!
Update 6th Nov 2018: I just published a follow-up to this article with new APIs added in P, and a bunch of other goodies. Do check it out!
What is that shadow all about
In Material Design, the elevation is a manifestation of the virtual Z coordinate of a material plane relative to the screen’s “base” plane. For example:
Image shamelessly ripped from the Material Design guidelines.
In the Material Design system, there are two light sources. One is a key light that sits above the top of the screen, and an ambient light that sits directly above the centre of the screen:
Image shamelessly ripped from the Touchlab blog.
These two lights cast each their own shadow, one which is mostly affecting the bottom edge of a material sheet (key light), and the other which is affecting all edges (ambient light):
Image derived from the Material Design guidelines.
The elevation property directly controls the shape of the resulting shadow; you can see this clearly with buttons, which change their elevation based on their state:
Image from Vadim Gromov’s Dribbble.
You may think that the elevation property is the only way to control how shadows look, but that’s not true.
In Android, there is a very little known API called Outline that is providing the required information for a Material sheet to project a shadow. The default behaviour for View s is to delegate the outline definition to their background drawable. ShapeDrawable s for example provide outlines that match their shapes, while ColorDrawable s, BitmapDrawable s, etc. provide a rectangle matching their bounds. But nothing says we cannot change that, and tell a view to use a different ViewOutlineProvider , using the setOutlineProvider() method:
If we control the ViewOutlineProvider , we can tweak the resulting Outline , tricking the OS into drawing whatever shadow we want:
You can use elevation and Outline to do all sorts of tweaks to the shape and position of an elevation shadow:
Believe it or not, I have actually captured this one myself on my phone.
You will notice how the shadow here does not just adapt to different elevation values, but is also translated around and gets a larger or smaller size than the view itself.
Another thing you can do is to assign a shape that is different from the actual outline of the view itself — I cannot think of any situation in which this would make sense, but you could. The only limitation is that the shape must be convex. There are convenience methods on Outline to have ellipses, rectangles and rounded rectangles, but you could also use any arbitrary Path , as long as it’s convex.
Unfortunately it’s not possible to exaggerate some effects too much, since as you can see there are some shortcuts the system takes when rendering the shadows which will create some rather annoying artefacts when you hit them.
In case you are curious how shadows are rendered in Android, the relevant code is in the hwui package in AOSP — you can start looking at AmbientShadow.cpp (note: starting with Android 10, hwui is not really used anymore, and everything is rendered by Skia instead).
Another limitation is that we cannot tint the elevation shadow, we’re stuck with the default grey, but to be honest I don’t believe that’s a bad thing 😉
Elevation tweaks in action
I’ve used this technique to come up with a peculiar elevation appearance for the cards in Squanchy, an open source conference app I’ve been working on in the last year:
As you can see, the cards have a shadow that looks way more diffuse than the regular elevation shadows. This is obtained by having an Outline that is 4dp smaller than the card, and an elevation of 4dp :
The cards have an android:stateListAnimator that also tweaks their elevation and translationZ based on their pressed state, like Material buttons do. You can see how the cardInset* attributes are then used in the CardLayout code to shrink the Outline that we provide to the system.
When you scroll the schedule in Squanchy, you might notice that the shadow tends to change size as a card scrolls along the Y axis:
If the effect is too subtle in a gif for you to see, this image makes it crystal clear:
How is that possible? We definitely don’t change the elevation and outline based on the y-position of an item (we could, but it’s really not a good idea as it requires recalculating outlines on each scroll event).
You’ll remember I mentioned earlier how there are two shadows in the Material Design environment, one which sits above the top of the screen, and one that sits directly above the centre. Well, the top light — that is the key light — is casting a longer shadow when an item gets farther away from it. This is actually something that is always true in Android, you just don’t notice it as much in normal circumstances. The Squanchy style makes it more obvious though, and you can even exaggerate it further by using a higher elevation value:
One last thing before you go
Lastly, remember that Outlines aren’t just used for shadows, but by default they define the clipping of the view too! If you have a weird outline and don’t want it to influence the drawing of your actual view, you’ll want to call setClipToOutline(false) on it to avoid nasty surprises.
This is only important when the Outline you provide has canClip() returning true , which is the case when the outline is a rectangle, a rounded rectangle, or a circle. Non-round ovals, and arbitrary paths, are not able to provide clipping, so setClipToOutline() has no effect in those cases.
Fun fact: rectangles and circles are all internally represented as special cases of rounded rectangles. A rectangle is a rounded rectangle with a corner radius of zero, and a circle is a rounded rectangle whose corner radius is equal to half the circle height/width.
If you want to read some more on the topic, the Android Developers website has a page on Defining shadows and clipping views, that goes through the same topics with code examples, and links to some more javadocs.
To play around with elevations on your own Android device, I have cooked up a simple playground app:
The code for the playground app is open source on GitHub.
Источник