Geometric Android Animations using the Canvas

Our team at s23NYC recently had the pleasure of bringing the SNKRS Pass experience to the Android app. SNKRS Pass is a feature in the SNKRS app that allows users to reserve the hottest kicks for pickup in brick-and-mortar retail stores. After a user has reserved their soon-to-be favorite shoe, the SNKRS Pass is their premium digital voucher that unlocks in-store pickup.

Previously, our team published a post how this was accomplished for the iOS SNKRS Pass experience. Today, you will learn how our team accomplished this for Android. This post focuses on the wave animation portion of the feature.

Our first attempt at the animation was a Lottie Drawable generated by our design team and it worked marvelously. However, we ran into issues. We couldn’t scale it properly to center the animation on the spinning circle view origin without making the animation view larger than the display size or leaving an awkward space at the bottom of the screen. Ideally, we’d want to set the wave origin at runtime. Since we were unable to go with the “easy” solution of using the Lottie asset, we instead set out to create the animation manually. We try to use Lottie for most of our animations, but it didn’t fit our case of a fullscreen animation being centered on a separate view.

After an introductory attempt by our team, we were fortunate enough to have the always incredible Nick Butcher point us in the right direction with a proof of concept of what we were trying to achieve. Below is our implementation which heavily leaned on Nick’s proof of concept.

Creating the wave animation

Let’s create a similar UI to the animated wave and circle view in SNKRS Pass. First, you’ll start by drawing circles and animating them out from the center of the screen.

You’ll define attributes for the space between each rendered circle and the circle color and stroke width:

Next, you will need to define a layout and custom view that shows a number of concentric circles.

In this view:

  1. Initialize the paint object with the custom attributes
  2. Define the initial and max radius of the smallest/largest circle
  3. Define the position where to start drawing the circles
  4. Draw all the circles with a radius of initialRadius to maxRadius with each separated by a space of waveGap

Now, you should see a static representation of circles that flood our screen:

This is done by creating a value animator that runs for 1.5 seconds, repeating in an endless loop. On every animation frame, the waveRadiusOffset will be updated — waveRadiusOffset is the value that tracks the circle expansion from its original position. Next we call postInvalidateOnAnimation() in the setter to redraw our view’s next frame. Finally, onDraw runs using the new offset in order to simulate the animation.

Reminds me of the Twilight Zone’s intro.

More than a Circle

Circles are fine, but the animation needs a defined shape. Taking a cue from the iOS article from our team, here you will be making a 10-point star.

We’ll use simple trigonometry (still remember sohcahtoa?) to draw our star. For reference and if you need a refresher: https://en.wikipedia.org/wiki/Trigonometry#Mnemonics

Creating the star path line by line looks like so:

Canvas animation of the lines being added to the path one by one

First you need to figure out where each point will lie. Since a full rotation is 360 degrees (2π in radians), we just need to divide that by the number of points to know the angle at which each point will sit. Knowing the angle and the diagonal distance each point is from the center (also known as the radius/hypotenuse), we can use sin() and cos() to find the opposite (or y) and adjacent (or x) distances to plot each line between points.

sin(angle) = opposite/hypotenuse    cos(angle) = adjacent/hypotenuse
rearrange to find unknown:
opposite = hypotenuse*sin(angle)   adjacent = hypotenuse*cos(angle)
Canvas animation with angles at which lines are added

If we put it all together and draw this star instead of the circle we will end up with this new wave animation:

Waves with star path

Changes made to onDraw:

The Gradient

The SNKRS Pass voucher has a gradient that moves as you tilt your phone. It applies to the waves but not the background.

To highlight the wave portions that fall within the gradient, we can use PorterDuff.Mode.SRC_IN to color the parts of the canvas that have already been touched. (more information here)

Next we need to create and draw our gradient using different green alphas.

Finally, we need to update our view to use the software layer since PorterDuff.Mode.SRC_IN doesn’t work with hardware acceleration.

And, voila:

Motion

To move the gradient on the canvas, we’ll translate the gradient paint shader’s local matrix by a calculated amount.

Now we’ll use the acceleration and magnetic sensor to get the phone’s orientation and determine how much to translate the gradient as we tilt the phone.

Further sensor reading: https://developer.android.com/guide/topics/sensors/sensors_position

Let’s create a class WaveTiltSensor that will hold the logic for getting the device’s orientation angles. You’ll define an interface for the tilt sensor along with an associated listener and the initialization logic for the accelerometer and magnetic sensors:

Next, you will need to implement the tilt detection logic in onSensorChanged. The sensor data you need is the angle the phone is on the x and y axis as it rotates.

Now, you have the pitch (degrees of rotation about the x axis) and roll (degrees of rotation about the y axis) in radians. Since you’re translating the phone’s rotation to a 2D motion, the azimuth (degrees of rotation about the -z axis) doesn’t matter.

Visual representation of device orientation with respect to pitch, roll, etc

When your device’s pitch changes, you want to move the gradient up and down the y-axis. When the roll changes, the gradient should move left/right on the x-axis. The center of the gradient shouldn’t move past the edges of the screen so you need to constrain movement by half the height/width since it’s centered.

Illustration of pitch; roll is the same concept but with side tilts.

Let’s change the view to handle device tilt updates. We’ll have it implement the TiltListener interface.

As we get tilt events, the code performs a bit of trigonometry to get the adjacent vertical offset distance that the device is from its resting state for both the pitch and roll. In this example, the device is starting from a flat resting point on a table. Then, the code will translate the gradient by no more than half the screen height/width.

I updated the background color to give a little more contrast to the green gradient in the video.

Conclusion

Attached you’ll find an example Github project that you can play around with. I hope this post has made doing trigonometric and gyro based animations easier for you!