Smooth loading

Loading bars and spinners are so last decade. And for a good reason. They can break the flow of a website or app. Facebook, for instance, started showing a “ghost” of what the content will look like:

The concept provides a smoother transition from the loading state to the actual content. It can also ensure, when using a soft animation; the user knows that the content is coming up, and nothing went wrong. In Android, this can be done using ViewAnimator. In this article, we are going to look at how to do this for ImageView and TextView instances.


ImageView

The following is the primary function we can use to create our animation:

fun Context.animateSmoothly(
@ColorRes startColorId: Int,
@ColorRes endColorId: Int,
doUpdate: (Int) -> Unit
): ValueAnimator =
createAnimator(
ArgbEvaluator(),
colors[startColorId],
colors[endColorId],
onConfig = {
duration = 1000
repeatMode = ValueAnimator.REVERSE
repeatCount
= ValueAnimator.INFINITE
start()
},
onUpdate = doUpdate
)

To use it we can call it directly in our Activity like:

val imageViewBackground = ColorDrawable()
imageView.background = imageViewBackground
animateSmoothly(
R.color.loading_animation_start,
R.color.loading_animation_end
) {
imageViewBackground.color = it
}

Resources

A previous article (Extending Resources) explains the technique used to retrieve the resources in animateSmoothly().

createAnimator( … )

This function extends the system’s ValueAnimator.ofObject() with some utils to make it easier to use.

fun <T> createAnimator(
evaluator: TypeEvaluator<*>,
vararg values: T,
onConfig: ValueAnimator.() -> Unit = {},
onUpdate: (T) -> Unit
): ValueAnimator =
ValueAnimator.ofObject(evaluator, *values).apply {
addUpdateListener {
@Suppress("UNCHECKED_CAST")
onUpdate(it.animatedValue as T)
}
onConfig(this)
}

The first two parameters are used to create the actual ValueAnimator. We use the third and fourth parameters after creating the animator. The optional onConfig lambda is called to mutate properties of the ValueAnimator. onUpdate is used to forward updates with an adapter of addUpdateListener. It also adds extra type safety.

onConfig = { … }

onConfig = {
duration = 1000
repeatMode = ValueAnimator.REVERSE
repeatCount = ValueAnimator.INFINITE
start()
}

The onConfig shown in our example does 4 things:

  • Sets the duration to 1 second (1000ms):
    duration = 1000
  • Tells the animator to repeat by reversing the animation:
    repeatMode = ValueAnimator.REVERSE
  • Let’s the animator know that the animation has no end: 
    repeatCount = ValueAnimator.INFINITE
  • Starts the animation 
    start()

How it looks

The image view shows like this when loading:

Once finished loading, the animation will continue, so it’s advised to stop it once we have content. But we’ll see how to do this later on.


TextView

Now, we want the TextView instances in the layout to show a rectangle covering the width of the view and the height of the text inside it.

Here is the final source of our Activity:

class MainActivity : AppCompatActivity() {
    private val placeholderUrl = "..."
    private val imageViewBackground 
by lazy { ColorDrawable() }
private val titleViewBackground
by lazy { TextLoadingBackground(titleView) }
private val subtitleViewBackground
by lazy { TextLoadingBackground(subtitleView) }
private val descriptionViewBackground
by lazy { TextLoadingBackground(descriptionView) }
    override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
    override fun onStart() {
super.onStart()
        imageView.background = imageViewBackground
titleView.background = titleViewBackground
subtitleView.background = subtitleViewBackground
descriptionView.background = descriptionViewBackground
        val animation = animateSmoothly(
R.color.loading_animation_start,
R.color.loading_animation_end,
this::setViewsBackgroundColor
)
        imageView.setImageUrl(placeholderUrl) {
animation.cancel()
            setViewsBackgroundColor(0)
            titleView.text = strings[R.string.title]
subtitleView.text = strings[R.string.subtitle]
descriptionView.text = strings[R.string.description]
}
}
    private fun setViewsBackgroundColor(color: Int) {
imageViewBackground.color = color
titleViewBackground.color = color
subtitleViewBackground.color = color
descriptionViewBackground.color = color
}
}

Note: View references are resolved using Kotlin Android extensions.

We start by lazily instantiating the properties since we don’t need them until later

TextLoadingBackground

This type extends from ColorDrawable and limits the area it paints based on the font of the TextView:

class TextLoadingBackground(
private val view: TextView
) : ColorDrawable() {
    private val paintArea = view.paintArea
private val paint = Paint()
    override fun draw(canvas: Canvas) {
paintArea.right = canvas.width - view.paddingRight
canvas.drawRect(paintArea, paint)
}
    override fun setColor(color: Int) {
super.setColor(color)
paint.color = color
}
    private val TextView.fontHeight: Int
get() = with(paint.fontMetrics) { bottom - top }.toInt()
    private val TextView.paintArea: Rect
get() = Rect(
paddingLeft,
paddingTop,
0,
paddingTop + fontHeight - paddingBottom
)
}

The important elements of this drawable are paintArea and paint.

Initial area is calculated using the FontMetrics in the Paint used by the TextView. It ensures the area we paint has the same height as the font. It also gets updated in the draw() method to fill in the canvas horizontally.

paint is updated when setColor() is called.

animateSmoothly( … ) v2.0

val animation = animateSmoothly(
R.color.loading_animation_start,
R.color.loading_animation_end,
this::setViewsBackgroundColor
)

Here we have replaced the onUpdate lambda to use a function reference from the Activity. With this method, we can update all relevant views without the need to have multiple Animators.

We keep a reference to the animation to stop it after loading the content.

The result

After adding both image and texts to the activity here is the final result:

Gif compression makes it look worse than it is

The complete code for this post can be found here: https://github.com/pablisco/smooth-loading

Feel free to make suggestions or comments here, on GitHub or Twitter:

Show your support

Clapping shows how much you appreciated pablisco’s story.