In this series of articles we will only try to recreate this amazing clock animation, but actually improve it!
Clock animation with a little bit of colours
This is a third part of our animation series. Part 1 is here, Part 2 is here
TLDR:
– The source code of the final version of this animation could be found here.
– We have two different approaches to recreate the same animation — one by using parallel threads and another by using a little bit of math.
So far in Part 1 and in Part 2 we managed to :
- Create an endless rotating animation
- Draw an clock hand which shrinks first 12h and then extends back during the following 12h
- Draw 12 dots which disappear and appear back when necessary
- Create dots assemble animation
- Create 12 parallel disassemble animations.
In this Part 3 we will:
- Recreate the same animation by using a single formula for replacing 12 threads for spreading out animation.
- Change shape to the rounded edges and add some 🌈 colours 🔥
- Stop an endless animation upon request
Step 6: Single animation
In the previous part 2 we came up with an algorithm which required to have 12 parallel animations for each of the spreading-out dots.
As you already guessed that was not optimal. We actually can replace it with a single formula and do not need to create separate threads. We know that animation starts every hour. And based on the current hour it animates from the tip of the arrow to the outer edge of the circle by using this formula
val positionY = halfStroke + stepHeight * it * (1 - disassembleAnimations[it].value)
Where disassembleAnimations[it].value
is an animation value from 0 to 1. It’s calculated based on the animation value, which starts every hour with appropriate easing and duration. That’s the part which we want to change.
The basic animation
We might imagine the movement of the dots with this graph.
Dots are animated from 0 to 1 starting and ending every hour. Every 30 degrees a new hour starts. That behaviour might be easily described with a linear interpolation formula.
// currentDot is an index of the dot. val startAngle = currentDot * 30f val currentDeg = (animationAngle - startAngle).coerceIn(0f, 30f) currentDeg/30f
Let’s improve our code with this formula. Instead of using parallel animations and channels, we’ll just create 12 dot positions.
val dotsPositions = remember(animationAngle) { List(12) { currentDot -> val startAngle = currentDot * 30f val currentDeg = (animationAngle - startAngle).coerceIn(0f, 30f) currentDeg/30f } }
And will use these positions instead of disassembleAnimations
values
hours.forEach { if (!dotsVisibility[it]) return@forEach val degree = it * 30f rotate(degree) { val positionY = halfStroke + stepHeight * it * (1 - dotsPositions[it]) ... } }
Improving animation
As we’ve seen from the initial animation, dots are not really following this pattern. They might end the transition when another one has already started.
So we have to look for something like this
Good news is that it’ll not require a lot of changes. Starting points are the same. Now we just can’t relate to the ending point every 30 degrees. Instead we have to use 45 degrees or more as we would want.
For that we’ll introduce a new variable degreeLimit
and rewrite our formula.
... val degreeLimit = 45f // currentDot is an index of the dot. val startAngle = currentDot * 30f val currentDeg = (animationAngle - startAngle).coerceIn(0f, degreeLimit) currentDeg/degreeLimit
And that’s what we will see. Amazing!
“But it’s still not exactly the same! In the original animation it has this nice and smooth touch in the end !” — you might say. And you will be right.
We actually should have something like this.
We have a fast start, but then slow down at the end. Actually that .. looks exactly like a classic bezier curve.
Fortunately Compose Animation framework has exactly what we need for that! Easing!
While creating animation specs, we add an Easing parameter to it. In animation it usually set up like this
animation = tween(duration, easing = LinearEasing)
When we look at the Easing
interface, we’ll find a single method transform, which accepts a fraction between 0 and 1, and returns the same 0..1 fraction with applied transformation.
fun interface Easing { fun transform(fraction: Float): Float }
Job Offers
This method is exactly what we need for our case! We can just apply any easing
on our 0..1 fraction.
Then our code will look like this
val easing = LinearOutSlowInEasing val degreeLimit = 45f // currentDot is an index of the dot. val startAngle = currentDot * 30f val currentDeg = (animationAngle - startAngle).coerceIn(0f, degreeLimit) easing.transform(currentDeg/degreeLimit)
That’s how our animation will look! Exactly as we expected!
Final animation!
Compose Animation Api allows us to specify any easing we want with a CubicBezierEasing class.
For example this easing CubicBezierEasing(0f,0.3f,0.2f,1f)
will have a very fast start and a very slow end.
You can play more with cubic bezier easings on this website cubic-bezier.com
Amazing! In this step we managed to significantly optimise our animation by removing creation of unnecessary threads and replacing them with a single interpolation function!
The full code for this step could be found here
Step 7: Adding slider
In Part 2 I promised that we’ll be able to control this animation with a slider. And we actually can. We just need to remove infinite animation and pass animationAngle
as a parameter.
Then add a slider which will be changing animationAngle
from 0 to 720
... | |
Column( | |
verticalArrangement = Arrangement.Center, | |
horizontalAlignment = Alignment.CenterHorizontally | |
) { | |
val size = 300.dp | |
var progress by remember { mutableStateOf(0f) } | |
var animationAngle by remember { mutableStateOf(0f) } | |
Box( | |
modifier = Modifier | |
.size(size) | |
.background(Color.Black) | |
) { | |
ClockAnimation(animationAngle) | |
} | |
Text("Control animation with a slider!") | |
Slider( | |
modifier = Modifier.padding(16.dp), | |
value = progress, | |
onValueChange = { | |
progress = it | |
animationAngle = it * 720f | |
} | |
) | |
} ... | |
} | |
@Composable | |
fun ClockAnimation(animationAngle: Float) { | |
… |
Controlling animation with a slider
The full code for this step could be found here
Step 8: Adding more colours 🌈!
To be truthful, the initial animation appears somewhat uninteresting to me now 😑. The square shapes and lack of colour seem mundane and unexciting. I think we can spice it up by adding rounded corners and 🌈 colouring.
Coloured version!
That looks way more interesting!
Rounding corners
We can easily round corners by specifying StrokeCap.Round
instead of default StrokeCap.Butt
drawLine( ... cap = StrokeCap.Round, ... )
The problem is that StrokeCap.Round
takes some extra space when drawn, so we have to take it into account. To fix that we have remove a half stroke width during drawing.
Difference between Butt and Round caps
// Instead of val start = Offset(size.width / 2, positionY — halfStroke) // Remove halfStroke val start = Offset(size.width / 2, positionY)
The same can be done in other places for line length and dots height.
Colouring
We can easily draw a coloured background on top of our dots with DstOut
and DstAtop
blending modes .
We have to set DstOut
for underlying elements and DstAtop
for their overlay.
// Set Blend mode as DstOut for underlying elements drawLine( ... cap = StrokeCap.Round, blendMode = BlendMode.DstOut )
For colouring we’ll use a Brush
with Linear Gradient. We will also rotate that gradient for making animation more interesting
// Rotation of the gradient | |
rotate(animationAngle / 2, pivot = center) { | |
drawRect( | |
Brush.linearGradient( | |
colors = listOf( | |
Color.Red, Color.Yellow, Color.Green, Color.Blue, Color.Magenta | |
) | |
), | |
blendMode = BlendMode.DstAtop | |
) | |
} |
And that’s it for Part 3 folks!
In this part we managed to
- Improve our animation by using a single formula for replacing 12 threads for spreading out animation.
- Changed shape to the rounded edges and added some 🌈 colours
You can check the full source code for this animation in my Github repository.
Again, if you liked this article I would really appreciate it if you clap 👏👏👏 for it. You can do it multiple times, up to 15 as I remember.
Happy coding!
This article was previously published on proandroiddev.com