Previous  | Next  | Home

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.

 

Creating the Project in Android Studio

Following the general procedure in Creating a New Project, either choose Start a new Android Studio project from the Android Studio homepage, or from the Android Studio interface choose File > New > New Project. Fill out the fields in the resulting screens as follows,


Application Name: Animal Sounds
Company Domain:< YourNamespace >
Package Name: <YourNamespace> .animalsounds
Project Location: <ProjectPath> AnimalSounds
Target Devices: Phone and Tablet; Min SDK API 15
Add an Activity: Empty Activity
Activity Name: MainActivity (check the Generate Layout File box)
Layout Name: activity_main

where you should substitute your namespace for <YourNamespace> (com.lightcone in my case) and <ProjectPath> is the path to the directory where you will store this Android Studio Project (/home/guidry/StudioProjects/ in my case). If you have chosen to use version control for your projects, go ahead and commit this project to version control.

 

Layout of Main Screen

Let's use the visual layout editor of Android Studio to lay out the main screen. In the new project, open app/res/layout/activity_main.xml and select the Design tab at the bottom. At the top of the layout window, select a device skin (I will use the Nexus 6P for this example). The layout window should now look as follows.



Right-click on the displayed "Hello World" TextField, select Delete from the popup menu, which should remove this widget.

Next, from the Palette panel to the left of the layout, drag an ImageButton (under Widgets) from the panel on the left to the layout area. This will give a small gray box indicating the position of the widget. Repeat this twice more. Use the mouse to center the three widgets left to right, and to space them equally vertically. Then select all three widgets and click the Set layout_width to wrap_content button above the phone display (the button with a left-right arrow symbol). At this point the layout manager should look like the following image



with three buttons on stage. Now switch back to the Text tab for activity_main.xml, which should read


<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.lightcone.animalsounds.MainActivity"> <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:layout_marginTop="75dp" /> <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton2" android:layout_marginTop="130dp" android:layout_below="@+id/imageButton" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" /> <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton3" android:layout_below="@+id/imageButton2" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_marginTop="130dp" /> </RelativeLayout>

So now there are three ImageButtons wrapped within the parent RelativeLayout, with IDs imageButton1, imageButton2, and imageButton3, respectively, that we will be able to reference later from our Java code.

 

Customizing the Layout

If you execute this in an emulator or on a device, you should get a display very similar to the preceding figure, with three non-descript gray buttons (actually just gray boxes) on the screen. Pretty boring so far. But these are not ordinary buttons! 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.

  1. Let us begin by copying some resources that we will need: From the images resource page, save the image files


    into the /AnimalSounds/app/src/main/res/drawable/ subdirectory of the Animal Sounds project on your computer. (You may have to execute File > Synchronize to make these files visible in the Android Studio interface under app/res/drawable after adding them.)


  2. Alternatively, these image files may be pasted directly into app/res/drawable in the Android Studio interface (you can paste multiple files at once), which will give a dialog asking you to confirm the storage location. Note that current best practices for Android suggest pasting image files into app/res/mipmap instead of app/res/drawable. This will then give a dialog asking you to choose the resolutions to store (hdpi, mdpi, ..., xxhdpi). However, for the images being used here, if stored under mipmaps the resulting displayed image in the app is rather small. Hence, I have used the drawable directory for the image resources in this app.


  3. Then we use the android:src attribute inherited from ImageView to attach to each button one of the images that just placed in the drawable subdirectory.

    Now if the app is executed you should get a display like the following




    where the left figure shows the images attached to the buttons and the right figure illustrates for the middle (duck) button that the button background changes color if the button is pressed (the color depends on the theme in use). The XML that is responsible for this layout in activity_main.xml is

    <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.lightcone.animalsounds.MainActivity"> <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton" android:layout_marginTop="55dp" android:src="@drawable/cow" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" /> <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton2" android:src="@drawable/duck" android:layout_centerVertical="true" android:layout_alignParentRight="true" android:layout_alignParentEnd="true" /> <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton3" android:src="@drawable/sheep" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="50dp" /> </RelativeLayout>
    where the lines added by specifying the src attribute are highlighted in red and, for example, @drawable/cow is Android's reference to the drawable resource app/res/drawable/cow.png. (If the image resources had been placed in app/res/mipmaps instead of app/res/drawable, this reference would change to @mipmaps/cow.)


    As with most things in Android, it is also possible to specify the button image resource directly in the Java code; in this case by using the setImageResource(int) method of ImageView, where the argument is the drawable resource identifier. You would also have obtained the same result if you had edited directly the file activity_main.xml to give the listing displayed above.


  4. We can leave only the images as the visible buttons if the background color of the ImageButtons is set to transparent. Let's first define a color resource corresponding to a completely transparent color. Right-click on res/values, select New > Android XML File, give the file the name colors.xml, and edit it to read

    <?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#3F51B5</color> <color name="colorPrimaryDark">#303F9F</color> <color name="colorAccent">#FF4081</color> <color name="transparent">#00000000</color> </resources>
    which defines a black 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, with the Designer tab for activity_main.xml selected, click the top button and scroll down the Properties menu to the right of the display to the attribute Background. Select Background and in the field to its right type @color/transparent and return. This should replace the gray background with a transparent one for the top button. Repeat for the other two buttons.

    If you consult the listing for the activity_main.xml file you will find that these actions have used 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.

  5. While we are at it, let's also add an android:scaleType attribute to control how the image is attached to the button. Click on each image and scroll down the Properties menu to Scale Type, click in the field to the right, and select "center".

Now if the app is executed the visible representation of our buttons should consist only of the animal images with no background, as illustrated in the following figure.



Since the button background has been set to transparent, the default behavior of the button background turning color when pressed is no longer operative and a standard visual clue to the state of a button has been lost. We shall remedy that in the next section.

 

Adding Visible States for the Image Buttons

Visual clues to the state of the image buttons can be added back 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.

  1. Right-click on app/res/drawable, select New > File, give the file the name button1.xml (with the .xml extension), and click Finish. Edit button1.xml to read

    <?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>
    (Note: it is important to order the pressed, focused, and default items in the file exactly 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 or substring "cow" to "duck", and create the file button3.xml in the same directory with all occurrences of the "cow" in the above listing changed to "sheep".


    Be certain that the button1.xml, button2.xml, and button3.xml files are in the app/res/drawable directory. This is why the file was created using New > File, rather than New > XML. Android Studio will put the file in the wrong directory if the latter is used.


  2. Edit activity_main.xml and change the reference to the image source in the ImageButtons into a reference to the XML files just created (changes marked in red):

    <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton" android:layout_marginTop="55dp" android:src="@drawable/button1" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:background="@color/transparent" android:scaleType="center" /> <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton2" android:src="@drawable/button2" android:layout_centerVertical="true" android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:background="@color/transparent" android:scaleType="center" /> <ImageButton android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/imageButton3" android:src="@drawable/button3" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="50dp" android:background="@color/transparent" android:scaleType="center" />

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 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/cow_pressed.png if it detects that button 1 is pressed and res/drawable/cow.png if it determines that button 1 is neither pressed nor receiving focus. If the app is executed 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 touch-screen phone in portrait mode, for which there is no "focused" state (corresponding to a green-tinted image); only normal and pressed. On a phone with a physical keyboard 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.


The ImageView attributes can be used to modify the look of the buttons. For example, changing the scaleType to android:scaleType="centerCrop" for each of the ImageButton tags in main.html produces the following screen-filling buttons.




 

Adding Click Events

Some of the view aspects of ImageButton have been used to control the look of the widgets. Let's now exploit the button aspect to cause some things to happen. Open MainActivity.java and make the following changes.

  1. Implement the OnClickListener interface by adding "implements OnClickListener" to the class definition. This requires importing android.view.View.OnClickListener.

  2. Implementing the OnClickListener interface requires adding an onClick(View v) method stub.

  3. Attach click listeners in the onCreate method by adding for each of the three ImageButtons statements of the form

    View button1 = findViewById(R.id.ImageButton); button1.setOnClickListener(this);
    where R.id.ImageButton is the identifier for the first ImageButton, etc. This requires importing android.view.View.

  4. Fill out the onClick method with a switch statement that decides which button was pressed by getting the id of the View v and checking against the ids for the ImageButton widgets. For now, just put a Log.i() diagnostic output to the logcat stream indicating which button was pressed (which requires the import android.util.Log).

After these changes, MainActivity.java should read (with the additions displayed in red)


package <YourNamespace>.animalsounds; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; public class MainActivity extends AppCompatActivity implements OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Add click listeners to all the ImageButtons View button1 = findViewById(R.id.imageButton); View button2 = findViewById(R.id.imageButton2); View button3 = findViewById(R.id.imageButton3); 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.imageButton: Log.i("Test", "Cow Button"); break; // The duck button case R.id.imageButton2: Log.i("Test", "Duck Button"); break; // The sheep button case R.id.imageButton3: Log.i("Test", "Sheep Button"); break; } } }

Now if the app is executed and the three buttons are pressed in succession, there should be output in the logcat stream indicating that the appropriate button has been pushed. Once you have confirmed that the buttons are responding correctly to events the log.i statements may be removed or commented out.

 

Teaching the Buttons to Quack

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 an overview of using the MediaPlayer for audio (and video) may be found in the Media and Camera Android document. Teaching our buttons to moo, quack, and bleat requires the following steps.

  1. First we need some sound resources. These are normally stored in the app/res/raw directory. If this directory does not yet exist, create it by right-clicking on res, select New > Directory, and give the folder the name raw.


    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 the sounds will be packaged 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 Media Playback Android document.


  2. Download from the audio-video resource directory the files cow.wav, duck.wav, and sheep.mp3, and paste them into the just-created app/res/raw (you may also place them directly into the corresponding directory on your computer file system and then File > Synchronize to force Android Studio to see them).


    This example will use both MP3 and WAV files, which are among the formats that 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.


  3. To play a sound file copied to the res/raw directory (which gets packaged in the .apk file when the Android executable is created), we may create an instance of MediaPlayer using the static convenience method create(), with a reference to the sound-file resource as an argument (which requires importing android.media.MediaPlayer), and then execute the MediaPlayer seekTo(0) method to position at the beginning and the MediaPlayer start() method to initiate the audio.

This example uses only very short audio clips so 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 since the MediaPlayer may tie up hardware accelerators. Release of the MediaPlayer will be addressed in the section Lifecycle Management Issues below.

If these changes are implemented for each of the three cases represented by our three buttons, the listing of MainActivity.java becomes (the Log.i diagnostic statements have been commented out)


package <YourNamespace>.animalsounds; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.media.MediaPlayer; public class MainActivity extends AppCompatActivity implements OnClickListener { private MediaPlayer mp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Add click listeners to all the ImageButtons View button1 = findViewById(R.id.imageButton); View button2 = findViewById(R.id.imageButton2); View button3 = findViewById(R.id.imageButton3); 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.imageButton: //Log.i("Test", "Cow Button"); mp = MediaPlayer.create(this, R.raw.cow); break; // The duck button case R.id.imageButton2: //Log.i("Test", "Duck Button"); mp = MediaPlayer.create(this, R.raw.duck); break; // The sheep button case R.id.imageButton3: //Log.i("Test", "Sheep Button"); 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 app/res/raw/sheep.mp3. Now if the app is executed on a device or emulator we should find that when the buttons are pressed the cow moos, the duck quacks, and the sheep goes baaah!

 

Lifecycle Management Issues

Our app does what we set out to accomplish, but there is one problem. On a phone, if the app just created is started and the buttons are pressed in random order many times in rapid succession, it may 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 can overtax 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. This can be accomplished 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 because the MediaPlayer instance mp might not have been created yet.) Thus the completed listing for MainActivity.java is


package <YourNamespace>.animalsounds; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.media.MediaPlayer; public class MainActivity extends AppCompatActivity implements OnClickListener { private MediaPlayer mp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Add click listeners to all the ImageButtons View button1 = findViewById(R.id.imageButton); View button2 = findViewById(R.id.imageButton2); View button3 = findViewById(R.id.imageButton3); 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.imageButton: //Log.i("Test", "Cow Button"); mp = MediaPlayer.create(this, R.raw.cow); break; // The duck button case R.id.imageButton2: //Log.i("Test", "Duck Button"); mp = MediaPlayer.create(this, R.raw.duck); break; // The sheep button case R.id.imageButton3: //Log.i("Test", "Sheep Button"); 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 it isn't null if(mp != null) mp.release(); } }

(For this 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.

 

Providing Alternative Resources

Best practices in Android programming encourage 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 app resources supporting different device configurations (for example, different screen sizes, orientations, or default languages) that are managed automatically by Android.

 

Default and Alternative Resources

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!

 

Example:Portrait and Landscape Modes for AnimalSounds

Let's illustrate for our current project. There are two common ways to cause smartphones to switch between vertical (portrait) mode and horizontal (landscape) mode:

  1. Rotation of the phone between vertical and horizontal causes the switch (triggered by the phone's acceleration/orientation sensors), if the auto-rotate screen option is on.

  2. If the phone has a physical keyboard, activating it typically automatically switches the screen to horizontal mode, since the keyboard is often arranged horizontally.

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 functional but not an optimal use of the horizontal screen space. (Depending on how your app was laid out and the display device, there might not even be enough space to display all three images vertically in landscape mode.) To provide an alternative view for when the screen of the device is in the landscape display mode

  1. Right-click on app/res, select New > Directory, give the new folder the name layout-land, and click OK.


    Warning: if the AS Projects panel is in Android mode (see the dropdown menu above the left panel) the new directory just added will not appear in the tree structure. If you switch to Project mode using the dropdown menu it should appear as app > src > main > res > layout-land. It will also appear in the same place in the directory structure for the project in your computer file system. The reason for this potentially confusing difference is that in Android mode the project display tree does not literally mirror the project structure on the disk but in Project mode it does. Once a file is added to the new layout-land directory (see below) it will appear in the Project pane in Android mode, but under app > res > layout > filename (land).


  2. Right-click on app/res/layout/activity_main.xml, copy it, and paste it into the new app/res/layout-land folder. NOTE: be sure to put it inside the layout-land folder.

  3. Open the new app/res/layout-land/activity_main.xml file and click the Design tab at the bottom. (Note: be sure you are editing app/res/layout-land/activity_main.xml and not app/res/layout/activity_main.xml.) The visual display should open in layout mode (if it doesn't, click the rotation button above the image to put the display in layout mode). Then use the mouse to drag the three buttons and arrange in the layout that you would like for landscape mode. That's it; the rest is up to Android.

Now if the app is executed, while the device is in portrait mode we get the same display as before (because Android is using the default res/layout/activity_main.xml resource to format the screen), but if the device is flipped to horizontal mode---the reason and means are irrelevant for us as developers---we get a different display according to the new landscape layout just implemented. (For a device, be sure that the autorotate mode is chosen in Settings; for an emulator there is a rotation icon in the menu on the right side that allows flipping between landscape and portrait mode, though I have found that for emulators the behavior under rotation is not always a faithful representation of the behavior on a device.) For example,



because now Android has sensed that the device is in horizontal mode and has selected the layout in res/layout-land/activity_main.xml to format the screen. If the buttons are pushed we find exactly the same functionality as before, but now the display is at least a little more appropriate visually for a landscape layout.


Obviously one could do better aesthetically than the above image, for example by using in the landscape layout different images with aspect ratios better suited to a horizontal display, or a different number of images. 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.

The differences in Project panel display in Android mode and in Project mode mentioned above for this project are displayed in the following two images.




The Project-mode display in the above-right figure illustrates the actual directory and file structure of the project. There the landscape layout file appears at its literal location in the file structure: app/res/layout-land/activity_main.xml.

The Android-mode display in the above-left figure is instead organized according to Android coding and runtime logic. There the portrait layout file and the landscape layout file both appear under app > res > layout >activity_main.xml with the same names, but with the landscape layout file distinguished by a (land) label. This is more in keeping with the Android alternative-resource logic that it has two equally-viable main layout files and it will select the optimal one to use in real time by sensing the orientation of the device.

Notice in these figures another example of alternative resources for different environments that our projects have been supplying automatically all along. The mipmap directories contain icon bitmaps at different resolution (hdpi, mdpi, ...). Android decides which to use depending on the screen resolution of the device. (See the example of managing these mipmap resources in the project WebViewDemo.)

For many other potential applications of alternative resources, see the overviews in Providing Alternative Resources and Localizing with Resources.


The complete project for the application described above is archived on GitHub at the link AnimalSounds. Instructions for installing it in Android Studio may be found in Packages for All Projects.

Last modified: July 25, 2016


Exercises

1. Gather some additional animal images and sounds. Modify the AnimalSounds app so that each time a button is pressed the image changes to a new animal after the sound plays. Add some logic so that each button on the screen corresponds to a different animal at any one time. [Solution]

2. In the above project we prevent animal sounds from overlapping. Modify the app to create an "animal symphony" by allowing sounds to loop and overlap when a button is pressed. However, limit the number of sounds that can play concurrently so that the app is stable (too many instances of the MediaPlayer may eventually crash the app). [Solution]


Previous  | Next  | Home