|Android Programming In Java - Simple Animation|
|Written by Mike James|
|Monday, 11 June 2018|
Page 1 of 2
Android has some sophisticated UI animation features, but it is still good to know how to do the most basic of animations using raw bitmap graphics. Animation is also a good place to first meet the problems of threading and multitasking.
This extract from Android Programming In Java: Starting with an App 3rd Ed (I/O Press) covers one of the graphic techniques described in Chapter 5.
To bring this chapter to a close we will animate a ball bouncing around a Canvas, or a Bitmap depending how you look at it. This might seem like a strange topic to end on, especially since we are not going to do the job in the way that most Android programmers would go about it.
Indeed Android has a range of different animation facilities – View animation, Value animation and so on. However, none of them demonstrates the fundamental way that dynamic graphics work and before you move on to learn more sophisticated ways of creating animation it is a good idea to find out how things work at the lowest level.
This example not only teaches you something about animation, but also about the problems of creating dynamic graphics of any kind in the Android UI. One warning – do not assume this is all there is to know about Android animation or that this is the best way to do things.
To animate something in the simplest and most direct way all you have to do is draw the shape, change the shape, erase the old graphic, and draw it again.
In most systems this is usually achieved at the lowest possible level by using a timer to call an update function which erases the shape, does the update to the shape and then draws it at its new location. You can take this approach in Android, but for various reasons it isn't the way things are usually done. It has to be admitted that there are some slight difficulties, but overcoming them isn't hard and is very instructive.
To see how it all works let's just bounce a "ball" around the screen. This is more or less the "hello world" of simple 2D sprite-based graphics.
So start a new Android Studio project and place an ImageView on the design surface. This is the only UI element we need.
In the OnCreate method, after the UI has been expanded, all we need to do is set things up a bit more:
First we create a bitmap, associate it with a Canvas, and then set the whole bitmap to white. Notice that other parts of the program are going to need to access width, height and c so they are defined as global variables:
We are also going to need global variables to record the ball's position, its radius and velocity. For simplicity we might as well just use the default pixel coordinates of the Bitmap:
Other functions are also going to need to access the ImageView control so it might as well be a global variable so that we don't have to waste time finding it each time it is needed:
and for the same reason a global Paint object saves time:
Now we have all of these variables defined we can move on with the OnCreate function and set up the Paint object:
You might be wondering why AntiAlias is set to false, i.e. turned off. The reason is that its dithering algorithm makes it hard to remove a graphic by redrawing it in the background color. Try changing false to true in the final program to see what the problem is.
We also need to get the ImageView object and set the bitmap we are drawing on to the display:
Timer and Threads
Now we come to the inner workings of the animation. We need a Timer object that runs a function every so many milliseconds:
The timer object has a range of schedule functions which run a function, actually a TimerTask, at different times. The one we need is:
which runs the TimerTask after delay milliseconds and every repeat milliseconds after that. The timings aren't accurate and it could take longer than specified for the TimerTask to be run.
A TimerTask object has a run method that is called by the timer at the interval specified. At this point you are probably thinking that we can and should use a lambda to specify the run method. If you try it you will find it doesn’t work. The reason is that TimerTask is a class and not all its methods are abstract. It isn’t a SAM and so you can’t easily use a lambda to define its run method.
The simplest way to create the TimerTask is to use an anonymous class:
This creates a new TimerTask and overrides its run method. The run method is called when the Timer is triggered. All it does is to call a new function update, which we have yet to write, that does the update to the ball's position etc. The final two parameters specify a 0 millisecond delay in triggering the first call and then 10 milliseconds as the repeat period. That is, update will be called every 10 milliseconds or so. If the processor is busy doing something else it could be more than 10 milliseconds between repeats.
The update function is fairly easy:
First it sets the color to white and draws the ball, a circle. This erases the ball at its old position, remember the background is white. Next it updates the position by adding the velocities in each direction. To make sure that the ball bounces we test to see if it has reached a boundary and if it has its velocity is reversed. Finally, the color is set to red and the ball is drawn at the new position.
If the function was to stop at this point then everything compiles and runs, but you won't see the ball move. The reason is simply that the UI is drawn once at when the program is initially run and then only when it is necessary because the user has interacted with it or the orientation has changed, etc. As a result the bitmap displayed by the ImageView object would be changed every 10 milliseconds, but it would not be redisplayed.
To make the UI update we need to call the ImageView's invalidate method which basically tells the UI to redraw it. However, if you put this in at the end of the update function you get an error message something like:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
The reason for this is that the Timer object uses a new thread to run the TimerTask. This is often what you want to happen, but in this case it is a problem. It is a problem that often occurs in creating a sophisticated Android app and something you are going to have to learn to cope with.
If you are new to the idea of threading, this explanation might help.
When you run an Activity it is assigned a single thread or execution. A thread is a unit of execution and it is what obeys your instructions. In a complete system there are many threads of execution – some running and some suspended. The operating system picks which threads get to run in a way that attempts to make them all seem to be making progress.
The single thread that the Activity gets is generally called the UI thread because its job is just to take care of the UI. It responds to events from the Activity like OnCreate and from the user like a Button click. When the UI thread responds to an event it obeys the event handler and then goes back to waiting for the next event. This is the sense in which every Android app is simply a collection of event handlers that the UI thread executes when the corresponding event occurs.
The big problem is that the UI event is really only happy when it has nothing to do. Then it just waits for an event and processes it at once. This makes the user think your app is very responsive because clicks and other input are acted on at once. If you give the UI thread a long task to do, for example you write a lot of processing into an event handler, then it isn't just waiting for the user to do something, and the user starts to think that your app is slow and sluggish. At the extreme the UI thread can be kept 100% busy doing something and then the entire UI seems to freeze up.
In short, the UI thread should not be used for intensive computation or anything that takes more than a few milliseconds. The way to achieve this is to use other threads. This is a main topic of Android Programming: Structuring Complex Apps.
|Last Updated ( Monday, 30 July 2018 )|