This lab will be a continuation of Lab 2. You will expand on your knowledge of the Android user interface library. It is important to note that this lab is meant to be done in order, from start to finish. Each activity builds on the previous one, so skipping over earlier activities in the lab may cause you to miss an important lesson that you should be using in later activities. It is also important to note that this lab builds on concepts started in Lab 2; finishing Lab 2 first is highly recommended even though you are given a skeleton to work with in this lab as well.
Objectives
At the end of this lab you will be expected to know:
Activities
IMPORTANT:
You will be given a Skeleton Project to work with. This project contains all of the java and resource files you will need to complete the lab. Some method stubs, member variables, and resource values and ids have been added as well. It is important that you not change the names of these methods, variables, and resource values and ids. These are given to you because there are unit tests included in this project as well that depend on these items being declared exactly as they are. These unit tests will be used to evaluate the correctness of your lab. You have complete access to these test cases during development, which gives you the ability to run these tests yourself. In fact, you are encouraged to run these tests at any time to ensure that your application is functioning properly.
Contents
1.3 Fill in the AdvancedJokeList Class
2. Declaring Static Layouts in XML
2.1 Porting Your Dynamic Layout Into Static XML
2.2 Building Custom UI Components
2.2.1 Create State Lists for Like and Dislike
2.2.2 Declare a Custom JokeView XML Layout
2.2.3 Create the JokeView data class
2.2.4 Make AdvancedJokeList use the JokeView class
3.1 Implement JokeListAdapter.java
3.2 Make AdvancedJokeList use JokeListAdapter and ListView
4.1.1 Download and Set Up ActionBarSherlock
4.1.2 Adding the Action Bar and a Menu Item to AdvancedJokeList
4.2.2 Enable Contextual Action Mode
1. Setting Up
1.1 Creating the Project
To begin, you will need to download and extract the skeleton project for the JokeList application.
Next you will need to setup a "Joke List" Android project for this app. Since the skeleton project was created in Eclipse, the easiest thing is to import this project into Eclipse.
Finally, make sure that your project is targeting the latest version of Android but supporting API 10 and higher.
You may run the application right now to make sure it installs and appears on your development device of choice, although the app will have nothing in it yet.
1.2 Fill in the Joke Class
You may fill in the Joke class using the functionality that you implemented for this class in Lab 2. However, there are several key differences. In particular:
In Lab 2, we covered how to create an Android Test Project specifically for running tests on a single application. In this lab and the remainder of the labs, you will just run Unit Tests from within the same project. Currently this technique works, but it goes against the testing fundamentals provided by Google. For the sake of speeding up development, the Android Unit Tests can be run from the tests folder in the skeleton project you downloaded.
Run the JokeTest.java Unit Tests in the test folder to ensure that you have properly filled in this class before continuing to the next step. For more information on how to run them, please see this section in lab 2.
You will run more tests later to guarantee proper functionality up to certain points as you progress further in this lab.
Note: There are several test files that are excluded from the build path and thus appear differently in the Project Explorer. You will add them back to the build path when the time is right.
1.3 Fill in the AdvancedJokeList Class
You will first fill in the AdvancedJokeList class using the code that you implemented for SimpleJokelist in Lab 2:
Fill in the addJoke() method using the code from SimpleJokeList.addJoke().
Fill in the initAddJokeListeners() method using the code from SimpleJokeList.initAddJokeListeners().
Fill in the rest of the onCreate() method using the code from SimpleJokeList.onCreate().
Fill in the initLayout() method using the code from SimpleJokeList.initLayout(). It should be exactly the same as the code from Lab 2.
Run the AdvancedJokeListAcceptanceTest.java Unit Tests to ensure that you have properly filled in this class, then run your application on the emulator or a physical Android device to ensure that it performs the way it did in Lab 2.
Note: You may have noticed that changing the orientation of the Android device screen when running the applications you've programmed thus far causes new components and information to disappear. We will fix this in the next lab!
2. Declaring Static Layouts in XML
Read the Android Developer Guide on Declaring Layout for complete background on declaring layouts. Declaring your user interface in XML is the preferred method of implementation. By declaring your UI in an XML resource file it gives you better separation between the presentation layer of your application and the code controlling things underneath. One benefit of this is that modifications to your UI can be made without having to change any source code or recompile. This allows you to define different views for different screen sizes, resolutions, and scenarios while using the same code to control everything.
2.1 Porting Your Dynamic Layout Into Static XML
In order to get some practice with setting up layouts in XML, you will begin by converting the layout you set up dynamically in SimpleJokeList in initLayout() to a static one in an XML layout file. You will then inflate this layout in AdvancedJokeList and set it as your ContentView.
Fill in the res/layout/advanced.xml layout file:
Edit your initLayout() method to use the advanced.xml layout file:
Try running the AdvancedJokeListPreTest.java Unit Tests. They should all pass.
Try running your application. The UI should appear and function exactly as it did for SimpleJokeList in Lab 2.
2.2 Building Custom UI Components
Sometimes the standard View library will not supply the functionality that you need. In situations like this it is completely acceptable to define your own UI Components. There are three general approaches to creating custom UI components:
1. Creating a custom component from scratch.
2. Modifying an existing component to serve your needs.
3. Combining existing components to create a compound component.
In this section you will be using the third approach to develop a custom component. We can do better than displaying a TextView with a background color; it lacks aesthetics and is limited to only showing text. We want to create a graphical representation of a Joke that looks clean and functions appropriately for the application. We shall call this representation a JokeView.
You will combine a number of different existing View classes to create a coherent Widget for displaying Jokes. For a complete background on this approach, as well as the other two approaches, read the Android Developer Guide on Compound Controls.
The custom component that you are going to implement will look like this (minus the black outline):
This is how a single Joke will show up visually on-screen.
This is what a single JokeView will look like in visual form. Each JokeView will be composed of a TextView and a RadioGroup with two RadioButton children. The RadioButtons will show custom icons instead of the default button icons to better convey the intention (rating; do you like or dislike the joke?). The icons will toggle between looking more colored or discolored to indicate which rating is selected. The custom icons are provided in the lab stub under the res/drawable-*dpi folders. You will first create the State List selectors to enable use of multiple custom icons on each RadioButton component.
Note: this lab is supportive of mdpi, hdpi and xhdpi screen densities. For more information on supporting multiple screens, see this page.
2.2.1 Create State Lists for Like and Dislike
State Lists act as state machines: they provide logic for the appearance of a set of images (one at a time) on a single component. We will use them to make each RadioButton display a fully colored emote when it is selected, and a more faded emote when not selected. In other words, selecting the green emote in the above JokeView image will cause it to become fully colored and the red emote to become faded, and selecting the red emote will cause it to become fully colored and the green emote to become faded. To do this, we will use the selector XML object type.
Define the res/drawable/like.xml State List file:
Define the res/drawable/dislike.xml State List file:
2.2.2 Declare a Custom JokeView XML Layout
The next step is to create the XML layout file that the custom component will use.
Fill in the res/layouts/joke_view.xml layout file:
This is roughly what you want to see as the final result for joke_view.xml in the Graphical Layout tab (click image for full view):
By default we want no rating, so it makes sense that both rating buttons are faded to start.
Hints:
The next step is to implement your custom component class. This class will be called (unsurprisingly) JokeView. It is your task to fill in JokeView.java that has been stubbed out for you. In general when creating a compound component, after you have established your layout you want your component class to extend the class of the root ViewGroup in your layout (likely LinearLayout). Your component class then becomes a special subclass of that ViewGroup.
Open up JokeView.java:
2.2.3 Create the JokeView data class
Make the JokeView class extend the root ViewGroup of your layout.
Fill in the JokeView(Context context, Joke joke) constructor:
Copy the following code:
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.joke_view, this, true);
Instead of returning an inflated hierarchy of Views, this JokeView object will become the root of that hierarchy.
Fill in the setJoke(...) method:
Set up the m_vwLikeGroup to respond to OnCheckedChange events:
2.2.4 Make AdvancedJokeList use the JokeView class
The last step is to update AdvancedJokeList to make use of your new JokeView custom component. Now we can finally see those jokes in the list! Or at least, we hope so.
Edit the AdvancedJokeList.addJoke(...) method:
Run the AdvancedJokeListTest.java Unit Tests. They should all pass. If a unit test fails due to JokeView's joke text size, change the size in the TextView component in joke_view.xml to best fit your viewing needs (you removed the dynamic text size change when you removed the code to create the TextView in addJoke(...)).
If you run your application now, you should see something like the following where you can change each joke's rating by touching the smiley radio button icons (click image for full size):
Adding several new jokes should work.
Note: Sometimes running an application multiple times from Eclipse will merely cause the application to be brought to the center of attention on your Android device (in Eclipse, you may have seen the text ActivityManager: Warning: Activity not started, its current task has been brought to the front in the Console window). If you want to repeatedly test a fresh "just launched" version of an Android application while making little to no changes to the code base, you may wish to demolish the old version of it and create a brand new version of it each time you run it. To do this, open a terminal window (Command Line or PowerShell in Windows, Terminal in Mac/Linux) and type adb uninstall <packagename> before every new run. For this lab, this would be adb uninstall edu.calpoly.android.lab3.
The application functions reasonably well, but right now there are a few noticeable problems. There is no clear distinction between Jokes; with the previous version, each joke would alternate background colors making them easy to separate. Furthermore, LinearLayout does not provide scrolling functionality (in lab 2 you placed it inside of a ScrollView to enable scrolling), causing JokeViews that would go off-screen to squish the icons. However, we will be replacing the LinearLayout with a much cleaner ViewGroup in the next section that solves the separation and scrolling issues--ListView.
Additionally, you will notice that we have two separate calls in addJoke(): One that adds a joke to m_arrJokeList, and another that adds a View to m_vwJokeLayout. Since we are now introducing additional data (i.e. actions we are able to perform on each Joke, namely applying a rating), it would be nice if we could easily apply changes in m_arrJokeList to the views in m_vwJokeLayout, and vice-versa. Right now if a user of this application were to change the Rating of one Joke object, the change would not carry over to the ArrayList of Joke objects. We are missing the 'glue' that binds them together, the Adapter (m_jokeAdapter), and we will tackle this next.
3. Adapters & AdapterViews
The purpose of this section is to introduce you to the concept of AdapterViews. An AdapterView is a View class that allows us to bind one or more Views to a dataset. This binding then takes care of responding to user selections as well as populating the AdapterView with data. The binding is performed by a third intermediate class called an Adapter. It is the Adapter that is responsible for keeping track of the selection and supplying the AdapterView with a View object representation of each item in the dataset. Read the Android Developer Guide on Binding to Data with AdapterViews for a complete background on the topic.
In the context of this section, the AdapterView is a scrollable vertical ViewGroup called a ListView. The dataset is then our ArrayList of Joke objects. The Adapter class is the JokeListAdapter, which follows the standard Object Adapter Design Pattern (read the wiki on Object Adapter for more information). JokeListAdapter contains a reference to our list of Joke objects and supplies ListView with a JokeView for each of them.
3.1 Implement JokeListAdapter.java
Begin by filling in the constructor:
public int getCount()
public Object getItem(int position)
public long getItemId(int position)
public View getView(int position, View convertView, ViewGroup parent)
3.2 Make AdvancedJokeList use JokeListAdapter and ListView
You will now make the AdvancedJokeList Activity class use the JokeListAdapter and ListView classes to maintain your list of Jokes, thus adding the 'glue' to keep the visual and data halves binded.
You can read the Android Documentation on ListView for details on the class. LinearLayout is an excellent ViewGroup for displaying components in a row or column fashion, but lacks functionality otherwise compared to other Views like ListView. In a nutshell, ListView offers functionality beyond what LinearLayout provides, including built-in scrolling and child View management options (in particular, it allows control over child view selection behavior).
Update advanced.xml to use ListView instead of a LinearLayout.
Update AdvancedJokeList.java:
Run your application. AdvancedJokeList should function exactly as it did before, but it should look slightly different. Instead of having no row indication, there should be line separators that automatically get added by ListView for you. The list should also scroll automatically. Here is an example with the default jokes added, then a few sample jokes added (click image for full size):
Scrolling down or up reveals all jokes.
Add the AdvancedJokeListTest2.java file in the test folder back to the Build Path (Right-click on file > Build Path > Include) and run its Unit Tests. Make sure they all pass before proceeding to the final section. Note that the previous tests, AdvancedJokeListTest.java, will fail to run successfully since you changed the structure of the components.
4. Menus
This section is devoted to working with Menus. Read the Android Developer Guide on Menus to get a good overview on the Android Menu system. There are two different types of menus you will work with: the Action Bar (which replaces the Options Menu) and Context Menus (which replace floating Context Menus with Contextual Action Mode). These will be described in greater detail as you prepare to implement them.
More on backwards-compatibility
Due to the evolution of the Android API and Android physical devices (e.g. tablets), there have been numerous changes made to menus. Starting with Honeycomb (API 11, Android 3.0), devices are no longer required to have a physical Menu button. Instead, devices running Honeycomb or higher use the Action Bar to display menu options using a combination of on-screen actions and overflow actions.
You will notice that the minimum supported API version in this lab is 10. Why concern ourselves with API 10 (Gingerbread, Android 2.3)? At the time of writing this lab, Android device usage information indicates that a significantly large percentage of Android users (~90%) have devices that run Gingerbread or higher, with ~40% running Gingerbread itself. Gingerbread devices (largely if not entirely made up of smart phone devices) were created with a physical Options menu button and thus did not need the Action Bar. Since we are developing for a minimum of API 10, we must consider backwards-compatibility: The ActionBar class is not available in API 10 and lower, but we wish to create an application that looks the same across multiple versions of Android.
How to implement backwards-compatible menus
Fortunately, there are several ways to provide an Action Bar and its functionality for older versions of Android that do not normally contain it. The first is to use ActionBarCompat, a solution provided in Google's Android support library. More information, including reasons to consider ActionBarCompat are available in this article.
The second is ActionBarSherlock (ABS), an extension of the Google compatibility solution and provides an Action Bar and its features across all Android versions. We will use ABS due to its ease-of-use and its flexibility in providing a custom implementation of the Action Bar if the native version of Android does not provide it.
In the two subsections that follow you will use both types of Menus. First, you will instantiate the Action Bar and add a menu item enabling joke filtering. Then you will create a Contextual Menu for removing jokes from the current list of jokes being displayed.
4.1 The Action Bar
4.1.1 Download and Set Up ActionBarSherlock
You will now download and set up ABS as an Eclipse library project, which will be referenced in your Eclipse Android application project.
You may consider renaming the ABS library project to something more intuitive such as 'actionbarsherlock'.
Run the application and make sure that it still looks and functions similarly to before. If the background is completely black (likely due to choosing Theme.Sherlock), try Theme.Sherlock.Light instead.
4.1.2 Adding the Action Bar and a Menu Item to AdvancedJokeList
Now ABS is ready to be used. ActionBarSherlock uses the same steps to create the Action Bar as is detailed in the Android Menus walkthrough, only using different imports.
First, create the menu XML file:
Edit AdvancedJokeList.java to use the menu resource:
If you run your application now, you should see an Action Bar across the top of the application (click image for full size):
Action Bar Sherlock works on all Android versions, even those completely lacking an Action Bar. Above is a Nexus One (API 10) emulator screenshot.
If you click or press the Filter Action Bar menu item, nothing will happen. You will now be briefly introduced to an Android feature that lets you provide minimal visual behavior feedback before adding the full menu functionality.
4.1.3 Toast Notifications
In the previous lab you were introduced to LogCat, which allows you to simulate debug printing on an Android device. Adding debug Log.d() statements allows for quick textual feedback in the LogCat tab in Eclipse and lets you double-check whether or not expected behavior or proper method invocation is happening without changing up your code base.
You will now use a second tool for that same kind of feedback on your Android device, only visual: Toast notifications. Toasts create a condensed text popup that appears briefly on screen before disappearing. You can create and modify Toast variables, but creating and displaying a toast all at once is as simple as injecting one line at the desired area:
Toast.makeText(context, text, duration).show();
context - Almost always this, or context obtained via getContext(), etc.
text - A String to display on the screen.
duration - How long the Toast is displayed on the screen. One of Toast.LENGTH_LONG, TOAST.LENGTH_SHORT.
Use a Toast notification to show minimal menu feedback as a sanity check to make sure the menu item responds correctly. Make AdvancedJokeList respond to selected menu items:
Run the application. Clicking or pressing on Filter will show a Toast notification on the screen (click image to view full size):
This proves that the Filter menu item responds as intended.
We now see minimal visual feedback response from the menu. Now to replace the Toast notification with the actual menu functionality. You may or may not find Toasts useful later for further minimal testing similar to what you just did.
4.1.4 Enable Joke Filtering
You will now add a nested menu to the Filter menu item in the Action Bar. This will allow for JokeView sorting in the ListView based on the current rating of all jokes. There will be four options: sorting by Like, Dislike, Unrated and showing all Jokes. Choosing Like will cause only jokes with the rating Like to show, Dislike will cause only jokes with the rating Dislike to show, Unrated will cause only Unrated jokes (no RadioButton chosen) to show, and Show All will show all jokes regardless of rating.
Edit mainmenu.xml:
If you run the application now, you will see a dropdown menu on the Filter menu item. However, they currently have no functionality. To give them functionality, add them to the onOptionsItemSelected() method.
Now you will add the Filter functionality in AdvancedJokeList.java yourself. Hints follow:
Carefully consider all possible use cases. For example, what if the user starts the app up with the default three unrated Jokes in the list, rates the first Joke with "Like", chooses the "Like" Filter (causing the one Liked Joke to appear by itself in the list), changes the rating of that joke to "Dislike" and chooses the Dislike filter? Just that same Joke should appear again.
Below is an example use case 'storyboard' (click to show full size) that shows what happens when the following is done: Load a fresh instance of the app -> Filter by Like -> Filter by Unrated -> Rate two Jokes -> Filter by Dislike -> Add new Joke -> Filter by Unrated -> Rate new Joke -> Filter by Show All. Notice that if you add a new joke while the list of jokes is being filtered, the new joke will show up in the filter, even though it is unrated, until you filter jokes again. This is the desired functionality.
You will want to view this in full size. Trust us.
Include the AdvancedJokeListAddFilterTest.java Unit Test file in the build path, then run the tests. They should all pass.
Feel free to test your app further to make sure it works as expected before moving on to the next and final section.
4.2 Contextual Action Mode
Now that filtering is enabled, your final task is to implement a deletion feature that will enable removal of a selected Joke in the list. This removal should persist across both the filtered and unfiltered lists of Jokes as well.
It is not intuitive to add this feature as an Action Bar menu item because it is not clear which Joke is being selected. This would require implementing a way to display which JokeView in the list is currently selected, which is extra work. Instead of reinventing the wheel, you will create a contextual menu that appears when certain components (in this case, JokeViews) are "selected" by the user, with a single option to remove the selected Joke from the list.
There are two ways to provide Contextual Menus: Floating Context Menus and Contextual Action Mode (CAM). As you could probably guess from the title of this subsection, you will be working with the latter. In the above link you will not only find a great comparison image between the two Contextual Menu implementations, but you will also notice Google's warning that CAM is only available on Android 3.0 and higher. However, like typical adventurous developers would, we are going to ignore Google's warning and make it work on older devices anyway! With some help from ActionBarSherlock, that is.
4.2.1 Create Menu Resource
First you will create the static menu XML resource. This will be similar to the menu creation for the Action Bar menu items.
Add actionmenu.xml to res/menu:
4.2.2 Enable Contextual Action Mode
You will enable Contextual Action Mode for individual views. Ideally we would want batch contextual actions; this would allow for selecting multiple JokeViews inside the ListView and marking them for removal, but this is only enabled on API 11 and higher (CHOICE_MODE_MULTIPLE_MODAL only works above API 10) and cannot be enabled through ActionBarSherlock.
You will now use Action Mode to enable a selection menu to appear whenever a JokeView is long-clicked. Note that long-click has been chosen over a normal click to allow for accidental missed rating button presses on each JokeView, giving the user room for error in touching the small rating buttons.
Implement ActionMode.Callback:
Run your application and gaze in wonder at the Contextual Action Menu that appears when long-clicking on a JokeView (click image for full size):
You may want to add a temporary Toast to make sure that the Remove option responds when selected.
Clicking the checkmark in the left corner of the Contextual Action Menu will dismiss it and bring the Action Bar back into focus.
4.2.3 Implement Joke Deletion
The last step in this lab is to add the functionality for joke removal. You are tasked to do this on your own. Hints follow:
Run your application and make sure that both filtering and deletion work as anticipated. This is a sample use case storyboard showing several deletions, filters and insertions (click image for full view):
Contextual Menu only shown once for brevity's sake.
You didn't think the fun was over just yet, did you? Include AdvancedJokeListAddFilterDeleteTest.java in the build path and run those tests. They should all pass. Your project should also still pass the AdvancedJokeListAddFilterTest.java unit tests. If so, congratulations, you've completed this lab!
5. Deliverables
To complete this lab you will be required to:
Primary Authors: James Reed and Kennedy Owen
Adviser: Dr. David Janzen