Android performance bits and pieces, part II – use fewer Views

September 17th, 2013

One of Romain Guy‘s hobbies is to file bugs on people to use fewer Views. But you didn’t hear it from me. Anyhow…

The screenshot below shows a full-width banner row with image that cross-fades into solid-color background, with title and (optional) subtitle to its right. In the previous release of the Play Store app we used six child views. Now we use three. What did we remove?

The first one to go was a View that spanned the whole width and height of the parent minus the paddings. That view had the main background fill color set on it. Now that background color is set on the parent view, with one minor caveat. If you call View.setBackgroundColor, it will set the color on the full bounds including the padding. Instead, what you want to do is to use an InsetDrawable and wrap it around a PaintDrawable initialized with your color. I mentioned this before, and I’ll mention it here. Do not use ColorDrawable as a bug on older OS versions will cause it to ignore the insets.

The next one was used to create the cross-fade between the right edge of the image and the solid fill. One option is to tweak the bits of the bitmap itself after you load it from network or local cache. Any kind of device-side image processing (in general so YMMV) is expensive in terms of both memory and CPU, so we opted out for overlaying the right part of the ImageView with a View that had a GradientDrawable set as its background. That drawable was initialized with two end points – full-opacity fill color on the right, and the same fill color with 0x00FFFFFF mask applied to it on the left. You usually don’t want a “random” transparent color used on such a gradient, as the intermediate pixels will not look right across a variety of solid colors. The gradient drawable should be created once in the constructor and then have its setBounds method called from within the onLayout of your custom view group (so that it is properly positioned on its view).

In the latest release we’re achieving the same visual effect using fading edges. Here, you extend the ImageView class and do the following:

  • call setHorizontalFadingEdgeEnabled(true)
  • call setFadingEdgeLength passing the width of the fade area in pixels
  • override getLeftFadingEdgeStrength() to return 0.0f (in our case)
  • override getRightFadingEdgeStrength() to return 1.0f (in our case)
  • override getSolidColor() to return the ARGB value of the matching solid fill to be used in the fading edge part
  • override hasOverlappingRendering() to return true and onSetAlpha(int alpha) to return false

The last two are needed to indicate that the fading edge should be respected during image view fade-in sequence.

Finally, the last view to go was a full-span child that provided light blue highlight visuals for pressed/focused state. Instead, we override the draw method to paint those states explicitly. If isPressed() and isClickable() return true, we call setBounds on the press Drawable and call its draw(Canvas) method. Otherwise, if isFocused() returns true, we do the same for the focus Drawable.

Note that none of this results in improving the overdraw. We’re touching all the pixels the same number of times we used to. However, we’re spending slightly less time inflating the view itself, as well as on measuring and laying out the child views.