Mobile development for Android
January 20, 2021
A course on the main aspects of mobile development based on the example of creating a full-fledged application.
Join us
In January 2021, Telegram announced the iOS Animation Contest 2021. The participant had to create animations in Telegram for iOS using the provided layouts. They had to optimize them on older devices and develop an editor with which to customize their functionality. Junior iOS developer Aleksey Muraveinik took part in the competition from Doubletapp. That's what came out of it.
The organizers have provided photo and video files demonstrating animations that need to be implemented and embedded in the Telegram client on the iOS platform.
The code must be written in pure Swift; using third party UI frameworks like Flutter and React Native is prohibited.
Implement animations for sending messages to chat
Messages should look like bubbles floating up
Implement animation editor
In this editor, you can set the speed and movement of an animated object
Implement animated background
The background is a four-color gradient, the color centers of which are set at arbitrary points. Animation is obtained by moving color centers
The work is evaluated according to 3 criteria:
The organizers give 16 days to complete the first task.
It was interesting to participate in the contest from Telegram. The task at hand turned out to be difficult, requiring a special approach. I had to smash my head and look for solutions, but it was definitely worth it. the experience gained was not only fascinating but also useful.
On January 19, I was assigned to do some research on gradient rendering. It was immediately known that the standard method that I used in `` Edge '' and Practice probably won't work.
Learn more about the standard way.
If you search Google for "swift gradient '', chances are you will find one of the following two ways:
The first method really justifies its position in the top the simplest gradient can be created in just 4 lines and get the following result
func createGradientLayer() {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.view.frame
gradientLayer.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
self.view.layer.addSublayer(gradientLayer)
}
Additional colors and direction can be added
func createGradientLayer() {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.view.frame
gradientLayer.colors = [
UIColor.red.cgColor,
UIColor.yellow.cgColor,
UIColor.blue.cgColor
]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
self.view.layer.addSublayer(gradientLayer)
}
Set transition points from one color to another
func createGradientLayer() {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.view.frame
gradientLayer.colors = [
UIColor.red.cgColor,
UIColor.purple.cgColor,
UIColor.blue.cgColor
]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
gradientLayer.locations = [0.0, 0.5, 1.0]
self.view.layer.addSublayer(gradientLayer)
}
For most, this functionality is sufficient, but not for us.
If you look closely at the background preview from the demo, you will notice that for each of the four colors a point is set, from which the color spreads in all directions. For each point, a radius is specified, within which the color does not change in any way in relation to the center color.
This gives us a hint to look for a way to draw gradients not linearly, but in the form of circles.
Fortunately, there is a standard solution for implementing gradient rendering as circles. This method is more low-level and uses 2D drawing techniques.
import Foundation
import UIKit
class GradientView: UIView {
override func draw(_ rect: CGRect) {
guard let currentContext = UIGraphicsGetCurrentContext(),
let gradient1 = CGGradient(
colorsSpace: nil,
colors: [UIColor.red.cgColor, UIColor.blue.cgColor] as CFArray,
locations: [0.0, 1.0]
) else { return }
currentContext.drawRadialGradient(
gradient1,
startCenter: CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2),
startRadius: 0,
endCenter: CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2),
endRadius: self.bounds.height / 2,
options: .drawsAfterEndLocation
)
}
}
Already somewhat remotely reminiscent of the circles that we need.
But the main problem with this approach is that these kind of gradients do not overlap each other, and instead of the expected overlap with the correct calculation of intermediate colors between the color centers, we get the following
Such a picture suggests that you need to come up with a solution that will calculate the color of each pixel on the screen.
Let's say we have 2 dots:
Let's select an arbitrary point for which we want to calculate the color. Let's call it P.
Then the calculation of RGB color components for P will be as follows:
Calculating color components for P:
This approach gives the expected result.
In our problem, 4 points of different colors are given, so we use the following principle for the calculation: `` Each specified color affects the color of any other point on the screen, if the distance from the center of the specified color to the point is less than some distance L.
I used CMYK (Cyan, Magenta, Yellow, Key) dots at the corners of the screen as the perfect four-color gradient to target.
Obviously, black (bottom left) should not affect turquoise (top left). It follows that L should be less than the screen height. But if we take the height as L, it turns out that turquoise will have an effect on the magenta, because the distance between them is screen width, i.e. less than L.
If we take the width as L, then there will be points on the screen that are more than L from all colors, and it is not known how to calculate the color for them.
Therefore, the decision comes to use the width of the screen as L, generate the gradient image itself in the shape of a square, and then stretch it to full screen.
Now for each point we calculate the influence factor of each of the colors on it.
This is done using the following formula:
f is the desired factor. The degree in the formula is responsible for the smoothness / sharpness of color transitions. The higher the degree, the sharper the transition.
Now that we know the influence factor of each of the four colors on a point, we can calculate the color components using the following formula:
Then the components of the point are:
Using this method, you can calculate the components for any point on the screen, and get the expected result.
Now that we are able to generate a gradient image with colors at arbitrary points, we can proceed with the animation itself.
Animation — these are changing pictures, that is, frames. The two standards for frame rate are considered to be 30 frames / s and 60 frames / s.
The demo animation shows that the color centers change for each frame
There are 8 points in total, the movement of colors to which occurs counterclockwise. For every two adjacent points, you need to generate 30 or 60 frames, in which the center of the color will be at one of the intermediate points.
And here we are faced with two problems.
At the moment, the algorithm for each frame calculates the number of dots equal to the screen width in a square. On iPhone 11, this number is 171396. And this number of color calculations for dots needs to be done in 1/60 of a second. Of course this is not possible. There are several steps to resolve this issue.
Caching
You can cache color influences for specified distances. Regardless of where the center of the color is located on the screen, the factor of its influence on a point located at a distance m from it will always be the same. Therefore, for the first time we consider the factors, and in all subsequent times just get the saved value.
Compression
You can reduce the width of the image by 10 times, and stretch the image itself to full screen. For example, on iPhone 11, the number of dots for which you need to calculate the color will be 1681. The gradient will still look as it should.
Buffering
If you prepare all frames at once before rendering, then the animation may delay at the beginning or memory will run out. If you prepare one frame at a time, then the gradient generator will not have time to form a new image during the period of time while the current one is being drawn, this will create a slowdown effect and kill the smoothness of the animation.
Solution: prepare 10 frames each and store them in the buffer. At the moment when the current 10 frames are being drawn, the generator draws new 10 frames and places them in the buffer. When the current frames are drawn, they are replaced with those in the buffer, and the generator draws 10 more and places them in the buffer
let timer = Timer.scheduledTimer(
withTimeInterval: 1 / Double(fps.rawValue), repeats: true) { [weak self] timer in
if i >= images.count {
images = buffer
buffer = []
self?.animationCreator.imagesRequired = true
i = 0
}
if i < images.count {
self?.imageView.image = images[i]
i += 1
} else if isFinished {
timer.invalidate()
self?.isAnimating = false
return
}
}
As you can see on the demo materials points movement graph curve, that is, the points move nonlinearly. The competition did not specify any function that could describe such a movement, so it was decided to set the function in discrete form.
Horizontal axis timeline, we divide it into 60 parts (corresponds to 60 frames per second).
Vertical axis the scale of movement, we divide it into 27 parts (conventionally).
Then, in order to find the center of the color moving from point A to point B, for each of 60 frames do the following:
Define the animationCurve display:
And the center of the color for each frame is defined as
x = A.x + dx * animationCurve [frameNumber]
y = A.y + dy * animationCurve [frameNumber]
It was difficult, but interesting.
The code for the final version of this project (and not only this one 😉) can be found in our GitHub repositories
Discuss article on social media
Similar news