Evidence

Like any other regular day at the Blinkist office, I open the production version of the app, just to check something. As I move through the various screen, I notice something. It’s sudden, only for a fraction of second, but it is there and my brain registers it.

Ripples don’t seem to like rounded corners.

Yup. A second look confirms the doubt. The ripple effect that is triggered by pressing either of the buttons is a little overzealous and goes beyond those rounded corners. You’re probably familiar with the meme that goes something like

What Has Been Seen Cannot Be Unseen

And that’s precisely how I felt. So, it was time to start investigating what was causing that and subsequently fixing it.

Investigation

First off, I sat together with QA to try to pinpoint the variables a little more: what version of the app, on which devices, which Android version, and so on. If you read the title of the article carefully, you might have guessed where this is going: we were quickly able to isolate the problem on every device running Android Pie, regardless of the manufacturer or of the app version. The following was the behavior on devices from Lollipop to Oreo, a.k.a. the expected behavior.

Ripples behaving properly.

Cool. Cool cool cool. What now?

Let’s take a look at the layout.

The button is made of a FrameLayout that contains a LinearLayout (icon and label), and a FrameLayout for the ripple effect (it dates back to the old days, when ConstraintLayout wasn’t a thing yet). The ripple effect is given by the selectableItemBackgroundBorderless attribute value, and we customize the color by specifying the colorControlHighlight (the WhiteRipple style).

Given that the problem was independent from the app version, it was pointless to even look at the history of changes of this layout, and in fact it had been untouched since many months before. This smelled like a regression in Android, plain and simple.

I reached out to Google to get some help. After a few interactions (thanks Nick Butcher!), the likely suspect is a big under-the-hood change in Android Pie graphics rendering UI. In fact, in Pie, the old hwui pipeline that was introduced in Honeycomb has been mostly replaced with a Skia-based approach, now that Skia fully supports GPU acceleration (see this XDA article for a more in-depth explanation). I therefore opened a ticket for this issue, which you can follow here:

https://issuetracker.google.com/issues/121235742

Workaround

This is all nice and fine, but in the meantime we were left with a broken ripple effect in production, which is arguably not nice.

My first attempt at fixing the problem was inspired by Michael Evans, who suggested using the android:mask property of RippleDrawable in order to replicate the cornering radius of the underlying content. Hurray!

Fixed behavior on Pie!

However, this initial victory led to more let down. First off, this fix broke the pressed state on Oreo (and before); the following are the before/after results.

Expected behavior in Oreo

Actual behavior in Oreo :(

Secondly, things were even more complicated than what they might look like. Keen observers amongst you might have noticed that the buttons change their corner radii progressively as the user scrolls the screen. Let’s take a second look:

Notice how the corners of the button become more or less rounded, depending on the scroll.

This computation is done at runtime, by calculating and adjusting the cornering radii according to the scroll percentage. So, an XML-based solution was a no go, and I had to attempt fixing the problem in code.

After some fiddling, here’s the workaround for Pie:

It works like this: at runtime, we check whether the button already has a background, if not we create one from scratch (yay, efficiency). If we simply want to update the cornering radii, we can retrieve the existing RippleDrawable instance, retrieve both the content and the mask, and update their shape with a new RoundRectShape with the updated radii values. We can safely call getDrawable with fixed indexes because of the way RippleDrawable works internally:

if (content != null) {

addLayer(content, null, 0, 0, 0, 0, 0);

} if (mask != null) {

addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);

}

And the final result is… 🥁

Finally, the ripples are properly working!

Conclusion

I hope you enjoyed this little journey! If you happened to stumble on the same issue and have managed to work around it differently, I’d love to hear from you, so feel free to ping me on Twitter or reply here on Medium!

I’ll update this post as soon as we gather more info on the official issue tracker.

EDIT: I forgot to put the link to the GitHub repo with the code that reproduces the problem! Here it is.