Create animation using Handler and Threads in Android

     What makes an application alive? What is it that makes users come to our application again and again? The right answer would be the performance and the core functionality, but beyond that it's the animations and interactions that user can do with an app is what makes users come back and use the app. 
    
    Many developers, at least I know I am, got into the Android development for the very same reason -- that ability to feel and see animations and the curiosity to question that how does it work? how can I make such an animation? So in my quest to learn more, here's simple tutorial to make animations using threads in Android.

    Prerequisites : 

  • Familiar with Android Studio and basic knowledge of Android and Kotlin
  • Curiosity to learn and know

So overall, the tutorial is really very simple and easy, however, it does evokes those thought processes and the ability to look at the animations differently. So let's get started.

First of all, create an empty project in Android Studio. Steps for that would be -  

File -> New Project -> Phone and Tablet -> (Select Either/or) Base Activity Or Empty Activity -> Next -> 
Select your package name, application name and path if you would like to. -> Finish. 

Let the project build and now you have a project up and running with the basic skeleton of application.

So Here's what the UI would look like of the application.


So whenever we click on "ROLL" button, the basic functionality is to switch images and assign them to imageviews randomly from available 6 dice choices. 

Here's the code for UI :
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FirstFragment">

<TextView
android:id="@+id/headline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/headline_margin"
android:layout_marginBottom="@dimen/headline_margin"
android:text="@string/roll_the_dice"
android:textSize="36sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/die1"
android:layout_width="@dimen/die_dimen"
android:layout_height="@dimen/die_dimen"
android:layout_marginTop="48dp"
android:src="@drawable/die_6"
app:layout_constraintEnd_toStartOf="@+id/die2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/headline"
tools:ignore="ContentDescription" />

<ImageView
android:id="@+id/die2"
android:layout_width="@dimen/die_dimen"
android:layout_height="@dimen/die_dimen"
android:src="@drawable/die_6"
app:layout_constraintEnd_toStartOf="@+id/die3"
app:layout_constraintStart_toEndOf="@+id/die1"
app:layout_constraintTop_toTopOf="@+id/die1"
tools:ignore="ContentDescription" />

<ImageView
android:id="@+id/die3"
android:layout_width="@dimen/die_dimen"
android:layout_height="@dimen/die_dimen"
android:src="@drawable/die_6"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/die2"
app:layout_constraintTop_toTopOf="@+id/die2"
tools:ignore="ContentDescription" />

<ImageView
android:id="@+id/die4"
android:layout_width="@dimen/die_dimen"
android:layout_height="@dimen/die_dimen"
android:layout_marginTop="@dimen/dice_vertical_margin"
android:src="@drawable/die_6"
app:layout_constraintEnd_toEndOf="@+id/die2"
app:layout_constraintStart_toStartOf="@id/die1"
app:layout_constraintTop_toBottomOf="@+id/die1"
tools:ignore="ContentDescription" />

<ImageView
android:id="@+id/die5"
android:layout_width="@dimen/die_dimen"
android:layout_height="@dimen/die_dimen"
android:src="@drawable/die_6"
app:layout_constraintEnd_toEndOf="@+id/die3"
app:layout_constraintStart_toStartOf="@+id/die2"
app:layout_constraintTop_toTopOf="@+id/die4"
tools:ignore="ContentDescription" />

<Button
android:id="@+id/rollButton"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/button_margin"
android:text="@string/roll"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

There's some strings that you can either add by yourself hardcoded or in strings.xml (You should always add strings in strings.xml, but you can hardcode just for the purpose of learning).
Add following in res -> values -> strings.xml

<string name="roll_the_dice">Roll the Dice</string>
<string name="roll">Roll!</string>

Now add following in res-> values -> dimens.xml 

<dimen name="die_dimen">80dp</dimen>
<dimen name="button_margin">16dp</dimen>
<dimen name="dice_vertical_margin">48dp</dimen>
<dimen name="headline_margin">48dp</dimen>

There are 6 vector drawable images that you need to add in res -> values -> drawable  so to get all 6 files, use this link - Six Vector drawable Die files create file names and add the vector code to get all images. 

So now the UI is out of the way, we can really focus on the main problem of rolling the dice when we click the button "ROLL!".  Here I am assuming that you know how to set button on click listener. Without animation, you can in theory when clicked on the button, you can randomly generate 1 to 6 number and let's say random number generator gives 5th number, then you can just pick die_5.xml (vector drawable ) and set that on one imageview. 

Pseudo-code 
    1.  Initialize 5 image views
    2.  Initialize all 6 die images 
    3.  Iterate through all 5 imageviews .. 1..5 (imageview) - current Imageview 1st..
      1. For each imageview generate random number 1 - 6
      2. randomly generated number i.e 4 
      3. assign die_4.xml (vector drawable) to imageview 1st
      4. repeat for the next 2nd.. 3rd.. and so on imageviews..
So If you do not want to see the code, you can try and implement this by yourself. Currently we are not worrying about animation, but this is a simple problem and based on the steps given, you can code yourself (Just so if you want to test yourself! )

The code of the steps given is: 
private fun rollTheDice() {

        val imageViews = arrayOf(binding.die1, binding.die2, binding.die3, binding.die4, binding.die5)
        val drawables = arrayOf(
            R.drawable.die_1,
            R.drawable.die_2,
            R.drawable.die_3,
            R.drawable.die_4,
            R.drawable.die_5,
            R.drawable.die_6
        )

        for (dieIndex in imageViews.indices) {
            val dieNumber = getDieValue()
            imageViews[dieIndex].setImageResource(drawables[dieNumber - 1])
        }

    }

    /**
     * Get a random number from 1 to 6
     */
    private fun getDieValue(): Int {
        return Random.nextInt(1, 7)
    }


Above code is all you need in order to successfully roll the dice, but now to really make it fun and look like it is really rolling, we can animate it which would look way more intuitive and realistic. For that we can use Threads and Handler in Kotlin.

Simple idea is that we can create a thread and switch one imageview 20 times before it finally decides 20th last value as the final rolled dice number. So when it switches from randomly generated values from let's say 4, 2, 2, etc .. 1...20 times, it will create a nice animation that looks like dices are really rolling. 


Pseudo-code 
    1. Iterate through all Images... 1 to 5, current imageview - 1st
      1. create a thread for 1st imageview
      2. for loop to switch images 20 times 
        1. for each iteration ... 1... 20 times, select a random number and assign to imageview
    2. repeat 1st for 2nd... 3rd.. and so on Imageviews
The code of the above would look as follows : 
private fun rollTheDice() {

        for (dieIndex in imageViews.indices) {

            thread(start = true) {
                Thread.sleep(dieIndex * 100L) //delaying starts so all dies look differently animating.

                for (i in 1..20) {
                    val dieNumber = getDieValue()
                    //if dieNumber is for example, 3, then set die_3.xml (imagevector) to imageview
                    //will repeat this 20 times which would create an animation effect.
                    Thread.sleep(100)
                }

            }

        }

    }
 

So basically go through 5 imageviews one by one and for each imageview, you start a thread and continually switch random dice number image and set to imageview which will create the animation. Each thread will pause for 100 milliseconds, so images will swap 10th times in one second which would create the smooth running animation.

However, currently we are not doing any operation related to the UI in code because any operation that is related to the UI (User interface) must be done on the main threadSo if we try to place a code that sets a dice image to imageview then the app would crash. 

To handle UI operations from there, You should have some way to communicate and send that data back to UI thread ( i.e main thread) so we can set images to imageview. That's where Handler() comes into play. 

Handler - A separate thread is needed when you are doing some "heavy" UI related work, in this case swapping images 20 times to create dice rolling animation for 5 imageviews, as well as pause switching of imageviews so it can be seem as it is rolling, so 5 threads are running simultaneously to create such an effect . When you perform heavy tasks on UI thread, User Interface might lag or become unresponsive, so to avoid that we need a separate thread. Now Handler would act as a means of communication between 5 threads that we are creating and the main UI thread that will do the work of actual swapping images so UI remains responsive and doesn't lag.

 A handler is an object that allows us to communicate between two threads ( i.e threads that we create and main UI thread of android system that handles UI).  You can read more on Handler() here -  https://developer.android.com/reference/android/os/Handler

Handler uses Bundle() to receive messages from another threads, so we are going to use handler as a mediator here where threads will give us random value one by one for 20 times and pause for 10th of a second and send that message to handler. After that handler would process that message and swap images for 20 times. Remember that in Android, only main thread can perform UI operations. So let us use bundle and then send data to handler.

private fun rollTheDice() {

        for (dieIndex in imageViews.indices) {

            thread(start = true) {
                Thread.sleep(dieIndex * 100L) //delaying starts so all dies look differently animating.

                val bundle = Bundle()
                bundle.putInt(DIE_INDEX_KEY, dieIndex)

                for (i in 1..20) {
                    val dieNumber = getDieValue()
                    bundle.putInt(DIE_VALUE_KEY, dieNumber)
                    Thread.sleep(100)
                    Message().also {
                        it.data = bundle
                        dieHandler.sendMessage(it)
                    }
                }
            }
        }
    }

    private fun getDieValue(): Int {
        return Random.nextInt(1, 7) //until 7 isn't inclusive, i.e. 1-6
    }

Above code is simply now creating a thread as before, but this time creating a bundle object that passes two values: 

    1. dice index - which imageview i.e 1st, 2nd or so on on UI screen.
    2. dice value -   which images from die_1 .. to.. die_6 to select to set in die index (imageview)
For example, dice index = 2 and dice value = 6 then it would bundle those value and send it to handler which has access to UI thread so based on that message it will swap 2nd imageview with die_6.xml (vector image). Hope that clears it up.

So we can create a handler object as following: 
private val dieHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            val data = msg.data

            val dieIndex = data?.getInt(DIE_INDEX_KEY) ?: 0
            val dieValue = data?.getInt(DIE_VALUE_KEY) ?: 1

            imageViews.get(dieIndex).setImageResource(drawables.get(dieValue - 1))
        }
    }
 

In above code, whatever value Thread sent to Handler via bundle, here Handler processes and extracts those values from bundle and performs UI operation which is setting a randomly chosen image to imageview. 

So if you have followed the whole process, kudos to you. Ideally you should read and challenge yourself after just getting UI code to solve this problem by yourself and check code if not sure how to move further. 

Here I am making an assumption that you are aware of using Kotlin and understand how bundle works. Even if you do not know, It is not hard to understand from the code. So final version of code would look as follows. I am posting a whole class.
package com.apprajapati.myanimations
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.apprajapati.myanimations.databinding.FragmentFirstBinding
import kotlin.concurrent.thread
import kotlin.random.Random

/**
 *
 * Ajay P. Prajapati ( github.com/apprajapati9)
 * A simple [Fragment] subclass as the default destination in the navigation.
 */

const val DIE_INDEX_KEY = "die_index"
const val DIE_VALUE_KEY = "die_value"

class MainFragment : Fragment() {

    private var _binding: FragmentFirstBinding? = null

    private lateinit var imageViews: Array<ImageView>
    private val drawables = arrayOf(R.drawable.die_1,
            R.drawable.die_2,
            R.drawable.die_3,
            R.drawable.die_4,
            R.drawable.die_5,
            R.drawable.die_6)

    private val dieHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            val data = msg.data

            val dieIndex = data?.getInt(DIE_INDEX_KEY) ?: 0
            val dieValue = data?.getInt(DIE_VALUE_KEY) ?: 1

            imageViews.get(dieIndex).setImageResource(drawables.get(dieValue - 1))
        }
    }

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View {
        _binding = FragmentFirstBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        imageViews = arrayOf(binding.die1,
                binding.die2,
                binding.die3,
                binding.die4,
                binding.die5)

        binding.rollButton.setOnClickListener {
            rollTheDice()
        }
    }

    private fun rollTheDice() {

        for (dieIndex in imageViews.indices) {

            thread(start = true) {
                Thread.sleep(dieIndex * 100L) //delaying starts so all dies look differently animating.

                val bundle = Bundle()
                bundle.putInt(DIE_INDEX_KEY, dieIndex)

                for (i in 1..20) {
                    val dieNumber = getDieValue()
                    bundle.putInt(DIE_VALUE_KEY, dieNumber)
                    Thread.sleep(100)
                    Message().also {
                        it.data = bundle
                        dieHandler.sendMessage(it)
                    }
                }
            }
        }
    }

    private fun getDieValue(): Int {
        return Random.nextInt(1, 7) //until isn't inclusive.
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
      

After this once you finally run the code, you will get a nice smooth running animation like this :



Thank you for reading and trying this code. I hope you learnt something new in this. The full source code is available at Android Animations. If you think this code can be written even better or there's any better way, please let me know that as well. I would love to learn more. Thank you. 

Post a Comment

0 Comments