Rolling Numbers Animation Using Only CALayers
RollingNumbers is a lightweight UIView for getting smooth rolling animation between numbers implemented using only CALayer.
Join the DZone community and get the full member experience.
Join For FreeI’m an iOS Engineer at Triumph Labs, where I develop TriumphSDK for game devs. Not so long ago, I got a task to completely reimplement UI and animations. One of the most interesting parts was implementing of balance view with rolling animation of the number in it.
At the first sight, it looks pretty simple, but we needed a well-configured custom solution. Of course, I checked existing libraries that I could reuse and adjust this kind of animation for our needs. After the first try, I figured out that the libraries are not so adjustable, and we cannot control the animation of each number. I won’t go into details too much. However, I decided to make my own solution from scratch.
Rolling Numbers is an open-source project that is available as Cocoapod and Swift packages.
CALayers
I had a list of requirements — how should look animation but also I added another one — the component should be implemented only using CALayers
. Why?
Most of the libraries that I checked were implemented using only UIViews
. The typical approach is making UIScrollViews
or UIStackVeiws
inside of a UIView
where arranged views are UILabels
. The rolling animation is implemented by using UIView.animate
changing constraints. That’s absolutely ok, but I wanted as much as possible to reduce the workload of performance.
CALayers represent the visual content of UIView. Layers provide low-level API of an efficient and more detailed configuration of rendering and animation using a low-cost Core Animation framework. Another important thing about Layers is that they are rendered using only GPU resources. This means the CPU will be free for another calculation task while the animation is running — that’s what I wanted to mention about performance.
Solution
If to check open-source solutions, the approach of how to construct the view is everywhere almost the same: a stack of numbers but visible ones only based on the state. My wireframe looks the same but different :) As you can see in the picture, the stacks of numbers are doubled; this means the number of elements in one stack is 20.
Wireframe of the stacks of numbers
This particular number of elements needs a smooth transition from an initial state to the next one, and in the meantime, the direction of rolling I wanted to be fully configurable. Let’s say a stack would have only 10 elements (and it would be pretty reasonable at the first sight), then the rolling direction from one digit to another would be always different. I needed to obey the direction rolling, for example, all digits from up to down.
Each stack in the view is CALayer
where sublayers are numbers. The numbers are arranged by the same height, which means it’s easy to calculate the next position of the future number.
Characters
All characters are CATextLayer
s, and they have their own width. If to use UIStackView
it’s not needed to care about width; all will be arranged by view configuration, but in my case — CALayer
s don’t have a such default implementation, so it means it should be calculated.
To get the width of a character is a pretty straight-forward task to use NSAttributedString
and font:
private func prepareWidthOfChar(_ char: Character) -> Double {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = String(char).size(withAttributes: fontAttributes as [NSAttributedString.Key : Any])
return size.width * characterSpacing
}
This function also helps to calculate an actual with of all columns in the Rolling Numbers view. To get this information, a developer can use the public property width
.
Animation Configuration
To achieve the spring animation effect, I used CASpringAnimation
changing y
position of theCALayer
— a column where sublayers are numbers CATextLayer
s.
private func animate(config: RollingNumbersView.AnimationConfiguration,
completion: (() -> Void)? = nil) {
CATransaction.begin()
let animation: CASpringAnimation = CASpringAnimation(keyPath: "position.y")
let fromValue: CGFloat = position.y
let toValue: CGFloat = moveToDigit()
animation.fromValue = fromValue
animation.toValue = toValue
animation.duration = config.duration
animation.speed = config.speed
animation.damping = config.damping
animation.initialVelocity = config.initialVelocity
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
CATransaction.setCompletionBlock {
completion?()
}
add(animation, forKey: nil)
CATransaction.commit()
}
Additionally, I used CATransaction
to catch the animation completion moment, which reference I exposed in the Rolling Numbers view API as trailing completion. It happens only once after setting a new number with animation.
rollingNumbersView.setNumberWithAnimation(245699) {
// completion
}
For the default animation config, I prepared a separate struct
called: AnimationConfiguration
. There are four initial configurations of the spring animation that are publically accessible.
duration: CFTimeInterval = 1,
speed: Float = 0.3,
damping: CGFloat = 17,
initialVelocity: CGFloat = 1
Animation Type
There are four animation types prepared for usage:
public enum AnimationType {
case allNumbers
case onlyChangedNumbers
case allAfterFirstChangedNumber
case noAnimation
}
AnimationType
is accessible using public property animationType
.
Let’s consider an example formatted as US currency. The initial value is $4.588.77. The future value is $4.576.67 (the changed numbers in the price I highlighted in bold). DZone does not allow GIF images, so use this link to the file where you can see the animation types.
By default, the animation is set up with .allAfterFirstChangedNumber
. This means if a future number is in the middle of the horizontal string, then all others digits after this number will be animated. If to use .onlyChangedNumbers
— this means literally: if a future number is different, then only this number column will be scrolled. But somebody will need to roll all digits, so for this, you can use just .allNumbers
.
Rolling Direction
The numbers rolling direction can be configured using specific public properties rollingDirection: RollingDirection
where obviously only two directions: .up
and .down
. For example, if to set up this property as .up
then the numbers in the view will always move in the up direction. However, initially, this property is nil
which means, by default, the direction depends on a future number. If the future number will be less than the old one, then the numbers column will move down and vice versa.
Formatting
There are several options to format the Rolling Numbers: alignment, character spacing, font, NumberFormatter
configurator, and text color.
var rollingNumbersView = {
// Initialize Rolling Numbers view with initial number value
let view = RollingNumbersView(number: 1234.56)
// Spacing between numbers
view.characterSpacing = 1
// Text color
view.textColor = .black
// Alignment within UIView
view.alignment = .left
// UIFont
view.font = .systemFont(ofSize: 48, weight: .medium)
let formatter = NumberFormatter()
formatter.numberStyle = .currency
view.formatter = formatter
return view
}()
Changing the size of the font, keep in mind that the actual height of the view can be smaller, which means the digits can be invisible.
Besides general public property textColor
, there is another interesting public method:
rollingNumbersView.setTextColor(.blue, withAnimationDuration: 3)
Under the hood, the animation was implemented using CABasicAnimation
of forgroundColor
. Changing the text color with animation. The public setTextColor
method helps to change the color of the text (numbers) while moving animation is happening! As an option changing color can be also an animation with duration.
Wrapping Up
I enjoy building UI for mobile apps and solving performance problems. If it’s needed to use more than just UIView
then Apple Documentation and tons of examples can help to make a more efficient solution.
When I finished building the Rolling Numbers component, I decided to share my solution with everyone — making a public library. Full documentation of the usage of Rolling Number you can get from the GitHub repository. Welcome to everyone who can contribute and suggest improvements to this project via PR!
Thanks for reading.
Published at DZone with permission of Max Kalik. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments