Android tips and tricks: reflections

August 1st, 2011 | 4 Comments »

Continuing the series on the more interesting UI pieces of the new Android Market client, today i want to talk about image reflections.

Here, the Market landing page shows two square cells with image reflections in the bottom half of each cell. If you’re coming from desktop client development, your first thought would be to take the original promo image, create a larger version of it with reflection and then set the resulting image on the ImageView displayed in the cell. There are enough code samples floating on the web that show how to write a method that gets an image and returns another image, twice as tall, with the original one on top and reflection on the bottom. There are two problems with this approach, one small and one big. Let’s start with the small one.

If you look closely at the reflection part in the screenshot above, you will see that the “amount” of gray does not change linearly as you go from the top edge of the reflection towards the full opacity near the bottom edge. It starts very slow and then accelerates to full alpha once it’s about two-thirds in. There’s nothing fancy about such an acceleration, and it can be quite faithfully approximated by a simple spline. The problem is that there are no core Shaders that do hardware-accelerated spline-based gradients. There’s a number of ways to work around this:

  • Use a multi-step LinearGradient, carefully choosing the positions to minimize visual artifacts. You may still end up with awkward alpha transitions in the final visuals around the “segment breaks”.
  • Paint the overlay programmatically. First, given the spline equation, compute the alpha for each vertical position. Now, you can draw a bunch of horizontal lines, changing the alpha on every row. Alternatively, you can create a one-pixel wide Bitmap, wrap it in a Canvas, paint the pixels based on the spline-computed alphas, create a BitmapDrawable with the resulting Bitmap, sets its bounds to stretch the bottom half of the reflected image and call the draw() method on it.
  • Finally, you can work with your designers to create a nine-patch asset that emulates the alpha overlay. There are certain advantages to this approach. The designer can tweak the reflection appearance in his tool of choice without any extra work on your side to come up with the correct math approximation of the target visuals. The main drawback is that nine-patches (or n-patches) only support linear gradients.

The second problem with creating a twice-as-tall reflection image is much bigger – memory pressure. Android runs on a wide gamut of hardware profiles, but one thing is constant – you don’t have as much memory as you do on desktop clients. Images are a particularly memory-hungry species, and given how many images the Market client displays on the landing page, we simply cannot afford consuming any more memory than absolutely necessary. Let’s see what we’re doing:

  • The entire cell is a custom class that extends RelativeLayout. Let’s call it GridSquare. Extending the RelativeLayout gives you a nice option to get access to the ViewGroup.MarginLayoutParams as the layout parameters of child views. This way you can respect the layout margins set in the XML layout definition. This also gives you an extra flexibility for more precise control over the layout.
  • GridSquare overrides the onMeasure and onLayout to compute the bounds of promo image. The end goal here is to have the promo image fit the entire cell width while maintaining the original ratio. To this end, the onMeasure gets the intrinsic width and height of the ImageView, computes the scaling ratio based on the intrinsic width of the image and the available width (computed from the width measure spec passed to onMeasure) and computes the matching height. The measure() method is then called on the ImageView child with the full width and the matching height (using MeasureSpec.makeMeasureSpec with MeasureSpec.EXACTLY). The onLayout() method fetches the measured height of the ImageView and passes it directly as the bottom position to the layout() call.
  • Reflection is done in two passes – full opacity reflection and overlay.
  • Full opacity reflection is done at runtime with Canvas APIs without creating any extra images or drawables. First, the GridSquare constructor calls setWillNotDraw(false) to let the rendering pipeline know that it has a custom onDraw logic. The overriden onDraw() method calls ImageView.getDrawable() on the view that holds the promo image, save()s the Canvas, translate()s it vertically by twice the height of the image view, computes the scale factor of the image (same logic as in onMeasure above), scale()s the Canvas based on the computed scale factor (positive X, negative Y), calls Drawable.draw() on the promo image drawable passing our Canvas and, finally, restore()s the Canvas so that we don’t end up affecting other draw operations on the Canvas object once our method is done. In addition, we paint a one-pixel black horizontal line along the bottom edge of the original image (our designers found that it adds a nice touch to the visuals).
  • The fading overlay is done with a nine-patch asset that is set as a background attribute of a special child view of the entire cell. We’ve decided to go with the third approach outlined above, where the nine-patch has a large non-stretchable vertical section that emulates the non-linear fading, and a full-opacity stretch that is “used” when the fading is complete. As child views are drawn after the parent, this overlay is painted on top of the full-opacity reflection created on the fly in the cell’s onDraw() method.

Stay tuned for more.

 


Related posts:

  1. Android tips and tricks: synchronized scrolling Last week we announced the rollout of a completely redesigned Android Market client, and it’s...
  2. Android tips and tricks: carousel animations Continuing the series on the more interesting UI pieces of the new Android Market client,...
  3. Android tips and tricks: swipey tabs Continuing the series on the more interesting UI pieces of the new Android Market client,...
  4. Android tips and tricks: pixel hunting Continuing the series on the more interesting UI pieces of the new Android Market client,...


4 Comments on “Android tips and tricks: reflections”

  1. 1 Karl Lattimer said at 9:22 am on August 1st, 2011:

    To improve reflections there are some rules.

    1, Only reflect upto 25% of the original image, in the real world reflections on any surface other than a mirrored surface fall off much more rapidly than *most* UI versions.

    2, Never reflect text, it makes the original text harder to read – ask your brain about it… It’ll take you 2-3 times longer to read if there is a reflected version of the same text.

    3, Before using reflections ask yourself “does the background material look like it’d be reflective in the real world” – you don’t want users to see a rough looking surface from gradients suddenly looking shiny – it’s confusing.

    4, If you’re going to blur to make a rough-ish shiny surface, make sure the blur increases toward the end of the shadow.

    5. Really think about the angle of the image and angle of the view before you create a reflection.

  2. 2 Ed Burnette said at 12:08 pm on August 1st, 2011:

    First of all, this is a great series of articles. Thanks for writing it. Second, I know the Market Client is not open source, but is there any way for you to publish some of the pieces you’re talking about, like the layout and 9-patches and some UI code snippets? It would make it easier to understand.

  3. 3 Christophe said at 11:42 pm on August 1st, 2011:

    nice, but is there a specific reason you use reflection here in the first place ?
    The tiles on the left (which also feature app or book) doesn’t have one. Why ?

  4. 4 Mirko said at 11:37 am on August 2nd, 2011:

    @Christophe: I guess it’s to show the promo image without too much resizing problems and still keep the tiles style. If the tile would be only big enough to promo image, rating, and price, it wouldn’t fit the tiles to the left, so it needs to be streched to 2x left tile and the additional space needs to be filled…