As an app developer, I’ve always preferred to prioritize having a working and stable app before focusing on animations and aesthetics. But recently I’ve learned that both of them are equally important and should have more or less the same priority. I started with splash animations, and now I’m animating individual UI components.

One of the aspects of UI design that is usually set aside is animation. This is actually pretty sad, as any animation that is well-timed and implemented gives an app a professional look and differentiates it from the others. This is why today I’m going to share how I implemented an animation to remove an element in a RecyclerView, like you saw at the beginning of the article.

Let’s start with some theory

The first step is understanding how the animation is going to work. When a RecyclerView item is being swiped, the framework shows a view that is the one that is having a horizontal translation, and another one that is static. For this tutorial we are going to call them the parent view (the one that moves), and the child view (the static one), as you can see in the following screenshot.

Child and Parent View in a RecyclerView item

Remember that in Android the (0,0) coordinates are in the top left corner of the screen. This is important to understand the code later.

We are going to trigger the animation every time a swipe is detected over our item with the next algorithm:

  1. Parent view coordinates are obtained so that child view coordinates can be calculated too.
  2. Coordinates when the swipe is considered to be effective are saved.
  3. Based on how much the item has been moved in the X axis, we define the size of the icon, its colour and the radius of the circle that is going to resize throughout the animation.
  4. The centre of the icon is calculated, which is also the centre of the circle, and the space in which the icon is going to be shown is defined.
  5. The icon and the circle are drawn in the child view using the calculations done previously.

Now with some code

To develop this animation we need to override the SimpleCallback class, with special attention to the onChildDraw methodwhich is going to be called every time the screen is updated, meaning that in a display with a 60 Hz refresh rate, the function is called every 16 ms. This method is going to contain all the logic that was explained before.

Another method that we are going to use is onSwiped, this where we define an action that triggers when a swipe is done in a specific direction (for example, a removal of a row in the app database). In this case, the swipe is going to be in the positive X axis, so we need to check if the direction is ItemTouchHelper.RIGHT.

Here you can see implemented the algorithm explained before, with some comments to clarify more what is going on:

    private val trashIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_delete_24)
    private val circleColor = ContextCompat.getColor(context, R.color.deleteRed)
    private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = circleColor }
    private val reverseSurfaceColor = ContextCompat.getColor(context, R.color.primaryTextColor)
    private val CIRCLE_ACCELERATION = 6f

    override fun onChildDraw(
        c: Canvas,
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        dX: Float,
        dY: Float,
        actionState: Int,
        isCurrentlyActive: Boolean
    ) {

       // We don't want to do anything unless the view is being swiped
        if (dX == 0f) {
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
            return
        }

        val left = viewHolder.itemView.left.toFloat()
        val top = viewHolder.itemView.top.toFloat()
        val right = viewHolder.itemView.right.toFloat()
        val bottom = viewHolder.itemView.bottom.toFloat()
        // Android draws with the center of the axis in the top left corner
        val width = right - left
        val height = bottom - top
        // Saves the canvas state to restore it at the end
        val saveCount = c.save()

        var iconColor = circleColor
        // Limits the child view to the space left by the viewholder while its being swiped
        c.clipRect(left, top, left + dX, bottom)
        c.drawColor(ContextCompat.getColor(context, R.color.colorSurfaceAccent))

        // The percentage that the child view has moved in the X axis
        val progress = dX / width
        val swipeThreshold = getSwipeThreshold(viewHolder)
        val iconPopThreshold = swipeThreshold + 0.125f
        val iconPopFinishedThreshold = iconPopThreshold + 0.125f
        var circleRadius = 0f
        val iconScale: Float

        when (progress) {
            in 0f..swipeThreshold -> {
                iconScale = 1f - (progress * 0.2f)
            }
            else -> {
                // The radius is the progress relative to the swipeThreshold multiplied by the width and the acceleration
                // The usage of the width allows the radius to adapt to the different screen sizes dynamically in every device
                circleRadius = (progress - swipeThreshold) * width * CIRCLE_ACCELERATION
                iconColor = reverseSurfaceColor
                iconScale = when(progress) {
                    in iconPopThreshold..iconPopFinishedThreshold -> 1.2f - progress * 0.2f
                    else -> 1f
                }
            }
        }

        trashIcon?.let {
            // 64 is the padding of the icon, divided by 2 to get the center of the icon
            val centerInXAxis = left + 64 + it.intrinsicWidth / 2f
            val centerInYAxis = top + 64 + it.intrinsicHeight / 2f

            // Sets the position of the icon inside the child view
            it.setBounds(
                (centerInXAxis - it.intrinsicWidth * iconScale).toInt(),
                (centerInYAxis - it.intrinsicHeight * iconScale).toInt(),
                (centerInXAxis + it.intrinsicWidth * iconScale).toInt(),
                (centerInYAxis + it.intrinsicHeight * iconScale).toInt()
            )

            // Sets the color of the icon
            it.colorFilter = PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_IN)

            if (circleRadius > 0) {
                c.drawCircle(centerInXAxis, centerInYAxis, circleRadius, circlePaint)
            }
            it.draw(c)
        }

        c.restoreToCount(saveCount)
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
    }
Kotlin

The last step is adding the callback to the RecyclerView.

ItemTouchHelper(
  OurSimpleCallbackImpl()
).attachToRecyclerView(binding.recyclerView)
Kotlin

That’s all

Animating is actually pretty hard to do, and takes a lot of time to learn, but the results are worth it. I hope that you understood how this animation works and if you need to check more about the code, I left the GitHub repository at the bottom of the post.


If you want to read more content like this and support me, don’t forget to check the rest of the bolg or subscribe here to get an email every time I publish new content.

Categorized in: