With a bit more emphasis on content recommended by your friends, we wanted to make people avatars in Play Store more visually pleasing. In our previous release the avatars were round with a thin translucent grey outline, and in our latest release the visuals are a bit more polished. There's a white ring surrounding the avatar, and an offset drop shadow, with both of these scaling to match the overall size of the avatar. Let's talk more about the specifics.
The avatars themselves are fetched from the network, which gives us a square - and sometimes rectangular - source image. Our first step is to create a normalized
square image based on the target dimensions on the screen. That normalized image preserves the source aspect ratio, upscaling the source if necessary to fill at least one dimension edge-to-edge and filling the second dimension with white pixels (taking care of non-square sources). This is done with Canvas.drawBitmap
that takes a source and destination rectangles as the parameters.
The next step is to compute the pixel size of the ring outline and the drop shadow. The ring outline starts at 1dip and is capped at 4dips, while the drop shadow starts at 2dips and is capped at 3dips. The actual size is determined based on the avatar size, setting the cap at 96x96dips (based on our current design metrics). This results in visuals that scale with the avatar size (seen below), while still capping the ring and drop shadow to not be too big for larger avatars.
Now it's time to take a look at the avatar layers. We have the avatar itself cropped to a circular shape, the ring outline and the drop shadow. In our first implementation pass we used Paint.setShadowLayer
to combine the last two together into a single Canvas
operation. We first painted the white ring, and then the avatar itself (since the drop shadow extends to both sides of the path, and we didn't want the shadow to be visible on top of the "inner" image). However, the runtime performance of shadow layer was not very satisfactory. It took about 2.5ms to draw a single outline, and when we had a few avatars on the screen, the numbers started adding up.
Instead, we're doing three separate layers.
First, we draw the drop shadow as Canvas.drawOval
with a single translucent grey color. We use Paint.setStrokeWidth
to set the interpolated drop shadow size, and Paint.setColor
to set the interpolated drop shadow color (for larger shadows we use more translucency to keep the same overall shadow "weight" across different avatar sizes).
Second, we draw the avatar itself. We create a BitmapShader
with the normalized square avatar source and TileMode.CLAMP
and set it with Paint.setShader
. Using that Paint
object on a Canvas.drawRoundRect
call results in the circular crop of the source image. There's some extra bookkeeping to make sure that we're scaling down the normalized source to make the white ring outline external
to the image, not losing the few top/bottom/left/right pixels. This can be done with a combination Canvas.scale
operations to keep the scaled-down avatar centered on canvas.
Third, we draw the ring outline as Canvas.drawOval
with opaque white color. We use Paint.setStrokeWidth
to set the interpolated ring outline size.
There's a bunch of small objects used for the custom drawing operations, usually involving a mix of Paint
ones. It's recommended to create them once at the class level, initializing as much of the state as you can in your constructor. Then, during the actual transformation / draw operations that can happen multiple times during the layout / render passes, only set those fields that are dynamic (size / color). This way you won't be creating transient objects which are discarded after they're used - saving yourself from unexpected GC pauses in the middle of your rendering. Also try to use Canvas
operations (transforms, scaling) instead of creating intermediate Bitmap
objects. And measure every step to make sure that you're not using operations that are too expensive.
Happy pixel pushing!http://www.pushing-pixels.org/2014/04/10/avatars-avatars-everywhere.html