Telegram-like uploading animation

Michael Spitsin
ProAndroidDev
Published in
7 min readOct 29, 2020

--

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:

  1. ProgressView — which is responsible for the drawing of the desired progress
  2. InfiniteAnimateView — 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:

left: background; center: stroke; right: 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).

left: Cap.BUTT, center: Cap.ROUND, right: Cap.SQUARE

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:

Trying to show 90 degrees with Cap.BUTT and Cap.SQUARE.

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 use SQUARE 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:

Left: 1st scenario, Middle: 2nd scenario, Right: 3rd scenario

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:

--

--

Love being creative to solve some problems with an simple and elegant ways