Android menu icon, delightfully
There’s probably not a single Android developer in the wild that hasn’t heard about Material design yet.
One of my favorite elements are subtle icon transitions, like the ones presented in delightful details.
Starting a redesign of my app, a menu to back-arrow icon - like the one in the top left corner - would come handy. I thought recreating it from scratch would be a great how-to exercise.
TL;DR Here’s the GitHub repo.
A bird’s eye view
Menu to back-arrow icon is a simple drawable with transition animation. Both states consist of three sole lines. The easiest way to define the transition is to mark the points of interest:
Intuitively, the mapping is:
- A to S
- B to U
- C to T
- D to U
- E to V
- F to U
Points C and D are roughly equal to T and U, so we’ll only focus on the bold ones.
Measures
To extract the width and height of lines in both states, we inspect them in its final form and determine relative lengths compared to the drawable bounds:
At hdpi density, the estimates are:
- $l$ = 60% of bounds.width
- $g$ = 15% of bounds.height
- $w$ = 5% of bounds.height
- $m$ = l/2
- $t$ = 45 deg
Rememeber the variable names, as they will be used throughout this post.
Progress
To get a smooth transition from e.g. A to S, we have to know the inbetween point at each animation frame. But first, we have to decide how to track animation progress.
Assuming we’ll use property animation, it’s on us to provide a property and set of values we want to animate between.
Let progress denote an arbitrary float value between 0.0 and 1.0, inclusive. This will be our property. We’ll start the animation by executing
If progress is 0, we show the menu icon. Else if progress is 1, we show the back-arrow icon.
Progressing from 0 to 1 transforms a home icon to a back-arrow. Conversely, 1 to 0 goes from a back-arrow to home.
Math
Remember, the points we refer to are all defined in the upper section.
P is the point at progress $0 < q < 1$, moving between A and S. We want to know $dx$ and $dy$.
Let’s find coordinates of S first, assuming A is the origin. Clearly its $x$ coordinate is $m$. To determine $n$, a bigger picture helps:
Assume $z = g + n$ and $h$ the length between S and D. Recall $t = 45 \deg$. Using trigonometric ratios, we know
\[\sin t = \frac{z}{h}\]We can get $h$ from
\[\begin{align} \cos t &= \frac{m}{h}\\ \frac{\sqrt 2}{2} &= \frac{m}{h}\\ h &= m\sqrt 2\\ \end{align}\]Since $h$ is of the same length as the diagonal of a square with an edge $m$, we deduct $z=m$, thus $n = (m-g)$ and $S=(m, (m-g))$.
Observe that in pic 1, at progress $p=0$, we must be at A. When $p=1$, we must be at S. It’s clear that at progress $q$
\[\begin{align} dx &= qm \\ dy &= q(m-g) \end{align}\]Another movement we have to take care of is B to D:
Only $dg$ is changing. Applying similar logic as above, we know
\[dg = qg\]Note that C-D line (or T-U) is a mirror line. As such, reflecting the $y$ coordinate gets us E-F line changes for free.
To read more about the approach we’ve taken, visit the linear interpolation wiki page.
Draw
As mentioned, both drawable states have only three simple lines. Canvas
has a convenient method drawLine
, which is all we need.
To be able to draw, we need to calculate dx
, dy
and dv
and know points A, B, C, D, E, F.
First three change when progress changes, but others just need bounds. We can compute them in a helper method, which is only invoked when the bounds are set.
In the code above, mHalfLineLength
denotes $m$, mLineGap
denotes $g$ and strokeWidth
is $w$. Other instance variables fully determine the points:
- A(
mStartX
,mTopY
) - B(
mEndX
,mTopY
) - C(
mStartX
,mCenterY
) - D(
mEndX
,mCenterY
) - E(
mStartX
,mBottomY
) - F(
mEndX
,mBottomY
)
Each time progress is updated, we invalidate the drawable. Consequently, draw
method is invoked
Note that we apply the transformations computed in the math section.
Demo
Because I’m nice, I’ve wrapped everything we’ve done so far in a widget for you to try out:
Class 4 Strategic Theatre Emergency
If progress is 1, the above widget reveals the pointy end of the arrow is not really pointy.
When drawing on a Canvas
instance, Android uses Skia, a 2D graphics library, to do the actual drawing. Canvas.drawLine
invokes a native method SkCanvas::drawLine
, which invokes SkCanvas::onDrawPoints
. Because our mPaint
instance is not complex (it has no path effects, for example), SkPaint::doComputeFastBounds
is called.
We care because in this case, if the mPaint
stroke width is $w$, the rendered point has a radius of $w/2$.
Here’s a zoomed in look at U, where B and F meet and progress is 1:
If we offset B and F by $(zx, -zy), (zx, zy)$ respectively, we cover the red square.
Because of the above analysis, we know $r=\frac{w}{2}$. Both lines come at a 45 degree angle, hence $k = 45\deg$ too. We can use trigonometric ratios again:
\[\begin{align} zx &= h\cos k \\ zy &= h\sin k \end{align}\]Because $\cos 45 = \sin 45 = \frac{\sqrt 2}{2}$ and $h= \frac{w}{2}$, we get
\[zx = \frac{w}{2} \cdot \frac{\sqrt 2}{2} = zy = \frac{\sqrt 2}{4}w\]Applying progress and updating the draw
method, we get
Again, demo
Confirm the fix in previous section resolves the issue:
A final touch
To finish off, we have to add rotation. The one in delightful details goes from:
- 0 to 180, if progress goes from 0 to 1
- -180 to 0, if progress goes from 1 to 0
Because, at this point, you are a master of linear interpolation, the updated draw
method makes sense
Parting words
Good on ya for making it this far. I hope math section was clear enough, and the widgets helped. I’m sure some parts can be improved, so I’ll be grateful for any feedback I get. The full implementation is available on GitHub.