Loading...
Home
  • Tech Blogs
  • Videos
  • Conferences
    • Droidcon News
    • Upcoming Conferences
    • Become a Partner
    • Past Events
    • Keep Me Informed
    • Diversity Scholarships
  • Community
    • droidcon Team
    • How to Hold a Droidcon
  • Android Careers
Sign In

Global CSS

 

Telegram-like uploading animation

 

 
Michael Spitsin
Love being creative to solve some problems with an simple and elegant ways
Published: November 06, 2020
Tweet
Share
 

 

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. :)

 

Image for post

 

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 != VISIBLEwhen 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:

 

Image for post

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).

 

Image for post

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:

 

Image for post

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.

 

Image for post

 

  • 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 :(

 

Image for post

 

  • 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:

 

Image for post

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 ;)

 

Image for post

 

f 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

 

 

Tags: Android App Development, Kotlin, Android Views, Animation, Custom View Android

 

View original article at: 


 

Originally published: October 29, 2020

Android News
Our Engineering Roadmap
Our Engineering Roadmap

By Mark Ng

We just completed our engineering road map for our Android apps at Australia Post. Each year we get together and try to decide on what we are going to do from an engineering perspective for the next 12 months. Each team gets to decide on what should be done now, what they want to complete by the end of the year and whats on the horizon for next year.

By ProAndroidDev -
Android News
Android Activity Lifecycle considered harmful
Android Activity Lifecycle considered harmful

By Eric Silverberg

The Android activity lifecycle is one of the first architectural concepts learned by a new Android developer. Ask any Android dev and they’ll likely all have seen this handy diagram from Google: 

By ProAndroidDev -
Android News
Our Safe Approach to Android Jetpack Navigation in a Multi-Modular App
Our Safe Approach to Android Jetpack Navigation in a Multi-Modular App

By Alejandro Weichandt

It has been a year since we started working on the Android version of the mobile app at Sync. During that year, we faced more than once that moment when we had to choose which path to follow on an Architectural decision. This story is about Navigation.

By ProAndroidDev -
Android News
Custom KotlinX Serializers
Custom KotlinX Serializers

By Jobin Lawrance

Let’s say we have a third-party class that we are using as a type in one of our data class that we want to be serialized, then we have to write a custom serializable for @Serializable to work.

 

By ProAndroidDev -
droidcon News

Tech Showcases,

Developer Resources &

Partners

/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/home-details/EmployerBrandingHeader
EmployerBrandingHeader
https://jobs.droidcon.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/jobs-droidcon/jobs.droidcon.com
jobs.droidcon.com

Latest Android Jobs

http://www.kotlinweekly.net/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/kotlin-weekly/Kotlin Weekly
Kotlin Weekly

Your weekly dose of Kotlin

https://proandroiddev.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/pad/ProAndroidDev
ProAndroidDev

Android Tech Blogs, Case Studies and Step-by-Step Coding

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
Zalando

Meet one of Berlin's top employers

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
Academy for App Success

Google Play resources tailored for the global droidcon community

Follow us

Team droidcon

Get in touch with us

Write us an Email

 

 

Quicklinks

> Code of Conduct

> Terms and Conditions

> How to hold a conference

> FAQs

> Imprint

Droidcon is a registered trademark of Mobile Seasons GmbH Copyright © 2020. All rights reserved.

powered by Breakpoint One