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 -
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.
<?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.
- Initialize 5 image views
- Initialize all 6 die images
- Iterate through all 5 imageviews .. 1..5 (imageview) - current Imageview 1st..
- For each imageview generate random number 1 - 6
- randomly generated number i.e 4
- assign die_4.xml (vector drawable) to imageview 1st
- repeat for the next 2nd.. 3rd.. and so on imageviews..
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.
- Iterate through all Images... 1 to 5, current imageview - 1st
- create a thread for 1st imageview
- for loop to switch images 20 times
- for each iteration ... 1... 20 times, select a random number and assign to imageview
- repeat 1st for 2nd... 3rd.. and so on Imageviews
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 thread. So 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:
- dice index - which imageview i.e 1st, 2nd or so on on UI screen.
- dice value - which images from die_1 .. to.. die_6 to select to set in die index (imageview)
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.
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.
0 Comments