Animal Sounds
In this simple project we gain some more familiarity with the UI widgets, and will also learn to play audio in Android. We shall use ImageButton, which is a kind of widget that acts like a button but accepts an image rather than text to define the visible face of the button, to present some animal images to the user and play animal sounds when a button is pressed.
Create a new project in Eclipse (File > New > Android Project), setting
Eclipse has a visual layout editor that is rather limited, but that can be useful in laying out simple configurations or the rudiments of more complex ones. Let's use this editor to lay out our initial screen. In the new project, open res/layout/main.xml and select the Layout tab at the bottom. At the top of the layout window, select Portrait for the Config setting. The layout window should now look as follows
and if you switch to the main.xml tab at the bottom the corresponding listing is
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello" />
</LinearLayout>
representing a single TextView displaying the value of the string variable hello from res/values/strings.xml within the LinearLayout. Switch back to the Layout tab, right-click on the displayed TextField, select Remove from the popup menu, and remove this widget. Next, select ImageButton from the Views menu on the left (scroll the menu if necessary to make the ImageButton option visible) and drag an ImageButton to the layout area. (At least in the Linux version of Eclipse, you sometimes have to try this more than once before the ImageButton can be selected.) Repeat this twice more. The layout manager should now look like the following image
with three buttons on stage. Now switch back to the main.xml tab and display the file, which should read at this point
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageButton android:id="@+id/ImageButton01" android:layout_width="wrap_content"
android:layout_height="wrap_content"></ImageButton>
<ImageButton android:id="@+id/ImageButton02" android:layout_width="wrap_content"
android:layout_height="wrap_content"></ImageButton>
<ImageButton android:id="@+id/ImageButton03" android:layout_width="wrap_content"
android:layout_height="wrap_content"></ImageButton>
</LinearLayout>
So we now have three ImageButtons wrapped within the parent LinearLayout, with IDs ImageButton01, ImageButton02, and ImageButton03, respectively, that we will be able to reference later from our Java code.
If you execute this in an emulator or on a device (right-click the project name and then select Run As > Android Application) you should get a display very similar to the preceding figure, with three non-descript buttons in the upper left corner of the screen. But these are not normal buttons because ImageButton is not actually a subclass of Button but rather is a subclass of ImageView, which in turn subclasses View and thus inherits image-display properties:
We now use these attributes to jazz up our buttons.
<ImageButton android:id="@+id/ImageButton01" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/cow"
android:layout_weight="1">
</ImageButton>
where @drawable/cow is Android's reference to the drawable resource res/drawable-hdpi/cow.png.
Repeat for the other two ImageButtons, using the image files duck.png and sheep.png as the sources.
Now if you execute the app you should get a display like the following
![]() |
![]() |
| As with most things in Android, it is also possible to specify the button image resource directly in the Java code by using the setImageResource(int) method of ImageView, where the argument is the drawable resource identifier. |
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="transparent">#00ff0000</color>
</resources>
which defines a red color that is fully transparent (24-bit color format #AARRGGBB, where AA is
alpha transparency, RR is red, GG is green, and BB is blue). Now
we use the
android:background property inherited from
View
to make the button background transparent by adding for each ViewButton an attribute
android:background="@color/transparent"
which references the color "transparent" that we just defined in res/values/colors.xml. While we are at it, we will also add an
android:scaleType attribute to control how the image is attached to the button.
Thus
main.xml now reads
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ImageButton android:id="@+id/ImageButton01" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/cow"
android:background="@color/transparent" android:scaleType="center"
android:layout_weight="1">
</ImageButton>
<ImageButton android:id="@+id/ImageButton02" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/duck"
android:background="@color/transparent" android:scaleType="center"
android:layout_weight="1">
</ImageButton>
<ImageButton android:id="@+id/ImageButton03" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/sheep"
android:background="@color/transparent" android:scaleType="center"
android:layout_weight="1">
</ImageButton>
</LinearLayout>
and if we execute the app we see that the visible representation of our buttons now consists
only of the animal images, as illustrated in the following figure.
Since we have made the button background transparent, the default behavior of the button turning red when pressed is no longer operative and we have lost a standard visual clue as to the state of a button. We shall remedy that in the next section.
We can add back visual clues to the state of the image buttons by defining different images corresponding to different button states (for example, different color shadings of the same image), and having Android choose among them automatically according to the button state using a drawable "selector" defined in XML.
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/cow_pressed" /> <!-- pressed -->
<item android:state_focused="true"
android:drawable="@drawable/cow_focused" /> <!-- focused -->
<item android:drawable="@drawable/cow" /> <!-- default -->
</selector>
Save this as the file button1.xml in the res/drawable-hdpi directory of the project. (Note: it is important to order the pressed, focused, and default items as given.) Likewise, create the file button2.xml in the same directory with the same content as above, except change all occurrences of the string "cow" to "duck", and create the file button3.xml in the same directory with all occurrences of the string "cow" in the above listing changed to "sheep". If necessary, right-click on drawable-hdpi and select Refresh in Eclipse so that the new files appear in the Eclipse project tree.
| The above instructions to create these files with an external editor is because I haven't figured out how to force Eclipse to put an XML file created by the normal Eclipse mechanism into the res/drawable directories, which is where these drawable selectors need to be. Once created in the correct directory, these files can be edited using Eclipse in the normal way. |
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ImageButton android:id="@+id/ImageButton01" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/button1"
android:background="@color/transparent" android:scaleType="center"
android:layout_weight="1">
</ImageButton>
<ImageButton android:id="@+id/ImageButton02" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/button2"
android:background="@color/transparent" android:scaleType="center"
android:layout_weight="1">
</ImageButton>
<ImageButton android:id="@+id/ImageButton03" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/button3"
android:background="@color/transparent" android:scaleType="center"
android:layout_weight="1">
</ImageButton>
</LinearLayout>
Now Android will see the XML files that we just created rather than a direct image name when it looks at the src attribute of the ImageButtons (it knows to interpret @drawable/button1 as a reference to the file button1.xml in the res/drawable-hdpi directory, for example). Android will then read these files, determine the state of the button (normal, pressed, or focused), and use the appropriate image (as specified in the XML file) to represent the button. For example, the above code from button1.xml would cause Android to choose the image resource res/drawable-hdpi/cow_pressed.png if it detects that button 1 is pressed and res/drawable-hdpi/cow.png if it determines that button 1 is neither pressed nor receiving focus. If we execute the app we now obtain the following display
![]() |
![]() |
Where the left image is for no buttons selected and the right image is with the cow image being pressed on the phone (which causes Android to substitute the yellow-tinted image cow_pressed.png for the default cow.png).
This example corresponds to screenshots from a Motorola Backflip phone in touch-screen portrait mode, for which there is no "focused" state (corresponding to a green-tinted image); only normal and pressed. If on the Backflip phone one opens the physical keyboard, then the up and down keys on the physical keyboard can be used to change focus between buttons and then one sees the the green-tinted state indicating the button has focus, and the yellow-tinted state when the button is pressed on the touchscreen, or OK is clicked on the physical keyboard when the button has focus.
In default mode the displayed images shrank when we added the drawable selector. The ImageView
attributes can modify this look. For example, changing the scaleType to
android:scaleType="centerCrop" for each of the ImageButton tags in main.html produces the following screen-filling buttons.
where on the left side the buttons are in their normal state and on the right side the duck button has been pressed. |
We have now employed some of the view aspects of ImageButton to control the look of the widgets. Let's now exploit the button aspect to cause some things to happen. Open AnimalSounds.java and make the following changes.
View button1 = findViewById(R.id.ImageButton01);
button1.setOnClickListener(this);
where R.id.ImageButton01 is the identifier for the first ImageButton, etc.
This requires importing android.view.View.
After these changes, AnimalSounds.java should read
package com.lightcone.animalsounds;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
public class AnimalSounds extends Activity implements OnClickListener {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Add click listeners to all the ImageButtons
View button1 = findViewById(R.id.ImageButton01);
View button2 = findViewById(R.id.ImageButton02);
View button3 = findViewById(R.id.ImageButton03);
button1.setOnClickListener(this);
button2.setOnClickListener(this);
button3.setOnClickListener(this);
}
// Required method if OnClickListener is implemented
@Override
public void onClick(View v) {
// Find which ImageButton was pressed and take appropriate action
switch(v.getId()){
// The cow button
case R.id.ImageButton01:
Log.i("Test", "Cow Button");
break;
// The duck button
case R.id.ImageButton02:
Log.i("Test", "Duck Button");
break;
// The sheep button
case R.id.ImageButton03:
Log.i("Test", "Sheep Button");
break;
}
}
}
Now if you execute the app and press the three buttons in succession, you should get output in the logcat stream indicating that the appropriate button has been pushed. Once we have confirmed that the buttons are responding correctly to events, the log.i statements can be removed or commented out.
Finally, let's modify the onClick method so that pressing an animal image plays a sound characteristic of that animal. The key Android class for implementing audio playback is MediaPlayer, and you will find an overview of using the MediaPlayer for audio (and video) in the Audio and Video Android document. Teaching our buttons to moo, quack, and bleat requires the following steps.
| Resources in the res/raw directory are not compressed when the .apk executable archive is packaged, so it is an appropriate place to put resources like sound or video that have their own compression. In this example we package the sounds with the app, but it is also possible to use MediaPlayer to play a sound from a stand-alone file in the device file system (for example, on the SD card), or a stream from a network source; see the overview in the Audio and Video Android document. |
| In this example we will use both MP3 and WAV files, which Android supports. The core media formats for images, audio, and video that are built into the Android platform may be found in this table. Specific hardware devices may offer additional formats not contained in this list. Applications may use any media codec available on an Android-powered device---both those provided by the Android platform and those that are device-specific. However, particularly with audio and video, a developer should test target devices extensively to determine which formats work well in practice. For short sounds, as in this example, WAV and MP3 are good choices. For longer audio MP3 is a good choice because of favorable quality versus compression. |
| Since in this example we use only very short audio clips, we will not be too concerned with other MediaPlayer methods like stop() or setLooping(Boolean) that would be important in managing more extensive audio resources. You should call release() on a MediaPlayer as soon as it is no longer needed. The MediaPlayer may tie up hardware accelerators and failure to release them may cause a future invocation of MediaPlayer to fail or to be forced to fall back on slower software acceleration. We shall address release of the MediaPlayer in the last section of this project. |
If we implement these changes for each of the three cases represented by our three buttons, the listing of AnimalSounds.java becomes
package com.lightcone.animalsounds;
import android.app.Activity;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
public class AnimalSounds extends Activity implements OnClickListener {
private MediaPlayer mp;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Add click listeners to all the ImageButtons
View button1 = findViewById(R.id.ImageButton01);
View button2 = findViewById(R.id.ImageButton02);
View button3 = findViewById(R.id.ImageButton03);
button1.setOnClickListener(this);
button2.setOnClickListener(this);
button3.setOnClickListener(this);
}
// Required method if OnClickListener is implemented
@Override
public void onClick(View v) {
// Find which ImageButton was pressed and take appropriate action
switch(v.getId()){
// The cow button
case R.id.ImageButton01:
mp = MediaPlayer.create(this, R.raw.cow);
break;
// The duck button
case R.id.ImageButton02:
mp = MediaPlayer.create(this, R.raw.duck);
break;
// The sheep button
case R.id.ImageButton03:
mp = MediaPlayer.create(this, R.raw.sheep);
break;
}
mp.seekTo(0);
mp.start();
}
}
where, for example, R.raw.sheep is Android's reference to the audio resource res/raw/sheep.mp3. Now if we execute the app we should find that when we press the buttons the cow moos, the duck quacks, and the sheep goes baah!
Our app does what we set out to accomplish, but there is one problem. On my phone, if I start the app that we just created and hit the buttons in random order many times in rapid succession, it will eventually crash (although there is a pretty cool animal symphony going on before that happens). This is presumably because, even though we are doing something simple with short sounds, failure to release the MediaPlayer between invocations eventually overtaxes the system.
In this context this isn't too serious since I forced the error by rather abusive use of the app. But in a more complex setting such problems could be much more serious, cropping up even in more normal usage. So let's learn to be better Android citizens by releasing the MediaPlayer object before we start another sound if it has already played a sound, and also by releasing the MediaPlayer (if it was instantiated) when the app goes into the background. We can accomplish this by inserting a statement
if(mp != null) mp.release();
in the onClick method before the switch statement, and overriding the onPause() method and adding the same statement there. (We must test for null in calling onRelease() because the MediaPlayer might not have been created yet.) Thus our final listing for AnimalSounds.java is
package com.lightcone.animalsounds;
import android.app.Activity;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
public class AnimalSounds extends Activity implements OnClickListener {
private MediaPlayer mp;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Add click listeners to all the ImageButtons
View button1 = findViewById(R.id.ImageButton01);
View button2 = findViewById(R.id.ImageButton02);
View button3 = findViewById(R.id.ImageButton03);
button1.setOnClickListener(this);
button2.setOnClickListener(this);
button3.setOnClickListener(this);
}
// Required method if OnClickListener is implemented
@Override
public void onClick(View v) {
// Play only one sound at a time
if(mp != null) mp.release();
// Find which ImageButton was pressed and take appropriate action
switch(v.getId()){
// The cow button
case R.id.ImageButton01:
mp = MediaPlayer.create(this, R.raw.cow);
break;
// The duck button
case R.id.ImageButton02:
mp = MediaPlayer.create(this, R.raw.duck);
break;
// The sheep button
case R.id.ImageButton03:
mp = MediaPlayer.create(this, R.raw.sheep);
break;
}
mp.seekTo(0);
mp.start();
}
@Override
public void onPause() {
super.onPause();
// Release the MediaPlayer if going into background
if(mp != null) mp.release();
}
}
(For our simple example it isn't necessary to restore the MediaPlayer by overriding onResume() when coming back to the foreground, since it will be instantiated and initialized within onClick anyway.) With this version, even if I hit the buttons many times in rapid succession on my phone, there is never more than one sound playing and the system is stable, and if the app is sent to the background and then brought back to the foreground it still functions correctly.
Best practices in Android programming promote externalizing resources: separation of function and content from display, which is facilitated by putting layout in XML resource files separate from the Java programming. This permits these resources to be maintained separate from the actual code, but equally important is that this makes it easy for a programmer to provide alternative resources that support different device configurations (for example, different screen sizes, orientations, or default languages) that are managed by Android.
This works in the following way. For any type of resource (strings, images, sound files ...), the programmer has the option of supplying two categories of resources:
To specify that a group of resources are alternative resources designed for a specific configuration, we append an appropriate qualifier to the directory (folder) name.
| The list of valid directory qualifiers may be found in the Providing Resources Android document. |
For example, the default UI layout is placed in the res/layout directory, but you can specify a different UI layout for landscape orientation simply by creating a res/layout-land directory and saving the alternative resource there. Then Android automatically applies the appropriate resources by matching the device's current configuration to your resource directory names: if it senses that a portrait layout is appropriate for the current configuration of the device it automatically uses the resources that you supplied in res/layout, and if it senses that the current device configuration corresponds to landscape mode it instead uses the resources that you have supplied in the res/layout-land directory. It really is that simple!
Let's illustrate for our current animal sounds project. My Motorola Backflip phone exhibits both of the common ways in which smartphones switch between vertical (portrait) mode and horizontal (landscape) mode:
If the phone is switched to horizontal mode by one of these actions the AnimalSounds app as presently written gives the following display
which is not an optimal use of the horizontal screen space. To provide an alternative view for when the screen of the device is in the landscape display mode
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ImageButton android:id="@+id/ImageButton01" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/button1"
android:background="@color/transparent" android:scaleType="fitStart"
android:layout_weight="1">
</ImageButton>
<ImageButton android:id="@+id/ImageButton02" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/button2"
android:background="@color/transparent" android:scaleType="fitCenter"
android:layout_weight="1">
</ImageButton>
<ImageButton android:id="@+id/ImageButton03" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:src="@drawable/button3"
android:background="@color/transparent" android:scaleType="fitEnd"
android:layout_weight="1">
</ImageButton>
</LinearLayout>
where changes from res/layout/main.xml are marked in red.
That's it; the rest is up to Android. Now if we execute the app, while the phone is in portrait mode we get the same display as before (because Android is using the default res/layout/main.xml resource to format the screen), but if the phone is flipped to horizontal mode (the reason and means are irrelevant for us as developers), we get the following display
because now Android has sensed that the device is in horizontal mode and has selected our layout in res/layout-land/main.xml to format the screen. If we press the buttons we find that they moo, quack, and bleat, just as before, but now the display is at least a little more appropriate visually for a landscape layout.
| Obviously we could do even better, for example by using in the landscape layout different images with aspect ratios better suited to a horizontal display. But this exercise illustrates the essential point that it is very easy as a developer to supply alternative resources that Android will manage without our intervention. |
For many other potential applications of alternative resources, see the overviews in Providing Resources and Localization.
| The complete project for the application described above is archived at the link AnimalSounds. |