Telegram-like uploading animation
Some time ago I worked on a new feature: sending images in the app’s internal chat. The feature itself was big and included multiple things, but actually, initially, there was no design for uploading animation with the ability to cancel the upload. When I moved to this part I decided that Images Needs Their Uploading Animations, so let’s give them that. 🙂
View vs Drawable
Actually, it’s a good question. Because if we look at one of my other posts about sonar-like animation, I used a Drawable
there. In my personal opinion there is a pretty good and concise answer in StackOverflow:
Drawable
only response for the draw operations, while view response for the draw and user interface like touch events and turning off screen and more.
Now let’s analyze, what we want to do. We want to have an infinite circle animation of the arc that increases in angle until it will fit the circle and spinning around at the same time. Seems like a drawable is our best friend. And actually, I should do that. But I didn’t.
My reason was in the small three-dots animation that you can see in the sample above. The point is that I did this animation with a custom view and I already prepared the background for infinite animations. For me, it was easier to extract the animation preparation logic into the parent view and then reuse it, rather than rewrite everything as drawables. So I’m not saying that my solution was right (actually nothing is right), but rather it met my needs.
Base InfiniteAnimationView
For the sake of my own needs I will split the desired progress view into two views:
ProgressView
— which is responsible for the drawing of the desired progressInfiniteAnimateView
— abstract view which is responsible for the preparation, starting, and stopping animation. Since the progress contains the infinite spinning part, we need to understand when we need to start this animation and when to stop
After looking in the source code of Android’s ProgressBar
we can end up with something like that:
Unfortunately, it will not work mainly because of the methodonVisibilityAggregated
. Because it supported since API 24. Moreover, I had issues with !isVisible || windowVisibility != VISIBLE
when the view was visible but the container of it was not. So I decided to rewrite this:
Unfortunately, this also didn’t work, however, I was sure that it will. So to be honest, I don’t know the exact reason. Probably it will work in an ordinary case, but will not work for the RecyclerView. Some time ago I had some problems with tracking if some things are displayed in recycler view using isShown
. Thus, probably my final solution will be not right, but at least it working as I’m expecting in my scenarios:
Progress animation
Preparation
So first of all let’s talk about the structure of our view. Which drawing components does it contain? The best representation of it, in this case, is the declaration of different paints:
For the purpose of showing I will variate stroke’s widths and other things so you will see the difference in some aspects. So those 3 paints are associated with 3 key parts of the progress:
You may be wondering why Paint.Cap.BUTT
. Well to make this progress more “telegramish” (at least as on iOS devices) you should use Paint.Cap.ROUND
. Let me demonstrate the difference between all three possible caps (will increase stroke width for more obvious difference spots).
So the main difference is that Cap.ROUND
gives to the stroke’s corners the special rounding, whereas Cap.BUTT
and Cap.SQUARE
just cut. The Cap.SQUARE
also use the additional space as Cap.ROUND
, but not for rounding. This can result in that Cap.SQUARE
shows the same angle as Cap.BUTT
, but with additional extra space:
Giving all of that it is best to use Cap.BUTT
as it shows a more proper angle representation than Cap.SQUARE
By the way
Cap.BUTT
is default paint’s stroke cap. Here is an official documentation link. But I wanted to show you the real difference, because initially I wanted to make it round, then I started to useSQUARE
but noticed couple of artifacts.
Base spinning
The animation itself is really simple giving that we have InfiniteAnimateView
ValueAnimator.ofFloat(currentAngle, currentAngle + MAX_ANGLE)
.apply {
interpolator = LinearInterpolator()
duration = SPIN_DURATION_MS
repeatCount = ValueAnimator.INFINITE
addUpdateListener {
currentAngle = normalize(it.animatedValue as Float)
}
}
where normalize
is a simple method of putting every angle in [0, 360)
range. For instance, for angle 400.54 the normalized version will be 40.54.
private fun normalize(angle: Float): Float {
val decimal = angle - angle.toInt()
return (angle.toInt() % MAX_ANGLE) + decimal
}
Measurement & Drawing
We will rely on measured dimensions that will be provided by the parent or through the xml’s exactlayout_width
& layout_height
value. So we do nothing in terms of view’s measurement, but we used the measured dimensions for the preparation of the progress rectangle, in which we will draw the view.
Well, it is not so hard, but we need to keep in mind a few things.
- We can not just take
measuredWidth
&measuredHeight
to draw a circle background, progress, and stroke. Mainly because of the stroke. If we will not take into account the stroke’s width and will not subtract its half from our dimension computations we will end up with cut looking borders 🙁
- If we will not take into account the stroke’s width we may end up overlapping it in the drawing stage. It can be fine for opaque colors.
But if you will use translucent colors, you will see overlapping as a strange artifact (I increased stroke width for more clear picture)
Sweep angle
Okay, the last thing is progress itself. Suppose we can change it from 0 to 1
@FloatRange(from = .0, to = 1.0, toInclusive = false)
var progress: Float = 0f
To draw the arc we need to compute a special sweep angle. It is a special angle of the drawing part. 360 — a full circle will be drawn. 90 — a quarter of the circle will be drawn.
So we need to convert the progress to degrees. And at the same time, we need to keep the sweep angle not 0, so we will be able to draw a small piece of progress if the value progress
will be equal to 0.
private fun convertToSweepAngle(progress: Float): Float =
MIN_SWEEP_ANGLE + progress * (MAX_ANGLE - MIN_SWEEP_ANGLE)
Where MAX_ANGLE = 360
(but you can put whatever you prefer) and MIN_SWEEP_ANGLE
is the minimum amount of progress in degrees that will be shown if progress = 0
.
Gather up
Now giving all that information we can build the completed view
The bonus!
The small bonus for that is we can play a little bit with a methoddrawArc
. You see, we have a currentAngle
, which represents the angle of the starting point for arc’s drawing. And we have a sweepAngle
, which represents how much of arc in degrees we need to draw.
When the progress is increased, we change only sweepAngle
, which means that if currentAngle
is the static value (not mutable), then we will see “increasing” the arc only in one direction. We can play with it. Let’s consider three cases and look at the result:
//In this scenario arc "increases" only in one direction
1. drawArc(progressRect, currentAngle, sweepAngle, false, progressPaint)//In this scenario arc "increases" in both directions
2. drawArc(progressRect, currentAngle - sweepAngle / 2f, sweepAngle, false, progressPaint)//In this scenario arc "increases" in another direction
3. drawArc(progressRect, currentAngle - sweepAngle, sweepAngle, false, progressPaint)
And the result is:
As you can see the left and the right animations (scenarios 1.
and 3.
) are not consistent in terms of speed. While the first one gives a sense of faster spinning speed, the progress is increasing, the last on the contrary gives a sense of slower spinning speed. And vice versa for decreasing progress.
The middle animation is consistent however in terms of spinning speed. So if you will not just increase progress (for file uploading, for instance), or just decrease the progress (for count down timer, for example), then I would recommend using the option 2.
.
Afterwords
Animations are great. Pixels are great. Shapes are great. We just need to treat them carefully with love. As details are the most valuable thing in the product 😉
If you liked that article, don’t forget to support me by clapping and if you have any questions, comment me and let’s have a discussion. Happy coding!
Also, there are other articles, that can be interesting:
Sonar-like animation
How it helps me to take some fun of android development
proandroiddev.com
Delightful swapping views animation
And how to do it by the simple math
medium.com
Android View Collisions
How to try to prevent view intersection
medium.com
Ten articles before and after
#47: Telegram and the path towards the end of ICOs – Best Telegram
Introducing the Forbes newsbot on Telegram – Best Telegram
How to create your own Telegram bot who answer its users, without coding. – Best Telegram
What We Learned From The MEDIA Protocol And Loki Telegram AMA – Best Telegram
Introduction to the Telegram API. Analyse your conversation history on… – Best Telegram
Telegram Is Trying to Build a Digital Nation – Best Telegram
How to create a Telegram bounty on Bounty Ninja? – Best Telegram
Telegram to increase max room size after Refereum’s request – Best Telegram
CELEBRATION OF 100K MEMBERS MARK IN WANAKA TELEGRAM – Best Telegram