Android Application Development For Dummies, 3rd Edition (2015)
Part IV. Android Is More than Phones
Chapter 19. Look Ma, I’m on TV!
In This Chapter
Creating an Android TV emulator
Coding apps for TVs
Using multiple loaders and adapters
Filtering your SQL queries
Launching a non‐default activity from Android Studio
I wish there was a knob on the TV so you could turn up the intelligence. They got one marked “brightness” but it don’t work, does it?
— Leo Anthony Gallagher
Smart TVs are changing the living room. Up until now, every TV manufacturer has developed their own unique interfaces for their TVs. Now Android is available on TVs to consolidate those interfaces and bring developer apps to your living room. Android is available in set top boxes such as the Android TV, which you can plug into any modern TV, and it’s being built directly into televisions shipping from Sony, Sharp, and Phillips.
As a developer, this is an opportunity for you to bring your apps to a whole new audience. In this chapter, you will port the Tasks app to Android TV.
Understanding Guidelines for Building TV Apps
It goes without saying that the way people use their TVs is different from the ways they use their phones. TVs are good for browsing information, but they’re not as great for entering information, given their lack of a keyboard and touchscreen. Android TV is designed for casual consumption, simplicity, and a beautiful, cinematic experience.
Consequently, you should build your TV apps differently than you build them for tablets or phones. Here are some of the differences you should take into account when building TV apps:
· Build for browsing, not for data entry.
· TVs have no touchscreen, so build your interfaces so they can be navigable with a D‐pad (imagine a remote control with up, down, left, and right buttons).
· Put onscreen navigation controls on the left or right side of the screen and save the vertical space for content. Do not use an action bar.
· Don’t just reuse your phone or tablet activities; they will be hard to use and won’t look good on the TV.
For more information about designing for Android TV, visit https://developer.android.com/design/tv. For more information about developing for Android TV, visit https://developer.android.com/training/tv.
You will use these techniques to transform your Tasks app into a TV‐like browsing experience.
Building and Manifesting Changes
To build an app for TVs, you must make some changes to your build settings and your AndroidManifest.xml.
Open the build.gradle file in the Tasks directory, and add the line in bold:
dependencies {
. . .
compile 'com.android.support:leanback-v17:21.0.3'
}
If you set your minSdkVersion to 16 in Chapter 17, you will receive an error message when you add the leanback‐v17 dependency from the previous code. This is because the leanback library requires platform API 17 or later, as the name implies. To continue, change your minSdkVersion to 17 in your build.gradle file.
This adds the Android TV dependency (also known as the “leanback” library, because that’s what you do when you watch TV) to your Tasks project.
Now open the AndroidManifest.xml file in your src/main directory and add the two sections in bold:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dummies.tasks">
<!-- For Android TV -->
<uses-feature android:name="android.hardware.touchscreen" →5
android:required="false" />
<application . . . >
. . .
<activity android:name="com.dummies.tasks.tv.BrowseActivity" →11
android:theme="@style/Theme.Leanback" →12
android:screenOrientation="landscape" > →13
<intent-filter>
<action android:name="android.intent.action.MAIN" /> →15
<category
android:name ="android.intent.category.LEANBACK_LAUNCHER"/>
→17
</intent-filter>
</activity>
. . .
</application>
</manifest>
Here is a description of what these lines do:
→ 5 Your Android TV is not going to have a touchscreen, because most couch potatoes do not have a long enough backscratcher that can reach the screen from their La‐Z‐Boy. You must tell the Google Play Store that a touchscreen is not required to install this app if you want the app to be displayed to users browsing the store from their TVs.
→ 11 Declares a new activity for the TV. This activity will be your “browse” activity, which users will use to browse their tasks on your TV.
→ 12 All TV activities should use the Theme.Leanback style, which among other things disables the action bar (which is really hard to use on a TV).
→ 13 This line forces Android to display this activity in landscape mode. There are very few TVs out there that display in portrait mode.
→ 15 Every launcher activity should also be a MAIN activity. You did the same thing for your phone and tablet activities.
→ 17 Unlike your phone and tablet, Android TV uses a different launcher. Thus, the category will be android.intent.category.LEANBACK_LAUNCHER rather than android.intent.category.LAUNCHER. This is convenient because it means you can have two MAIN activities in your app, and Android can automatically pick the appropriate one depending on whether your app is running on a phone/tablet or a TV.
Adding the BrowseActivity
In the previous section, you added a new activity to your manifest. In this section, you will create the activity.
The BrowseActivity is just a simple activity wrapper around a fragment, which you will write in the next section. It consists of two parts:
· The BrowseActivity class
· The activity_browse.xml layout
Create a new package in src/main/java/com/dummies/tasks named tv, then create a new file named BrowseActivity.java in the tv directory and add the following code to it:
public class BrowseActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browse);
}
}
Then add the following layout to res/layout in a file named activity_browse.xml:
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_browse_fragment"
android:name="com.dummies.tasks.tv.MainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
This layout is similar to the one you created in Listing 9‐2. It declares that this layout consists of one element, a fragment with code defined in com.dummies.tasks.tv.MainFragment, which you will create next.
Creating the TV Browse Fragment
As you can see, the BrowseActivity does very little. All it does is create the MainFragment, which is where most of your TV code is going to go.
The MainFragment is an instance of an Android TV’s BrowseFragment. The built‐in BrowseFragment consists of three parts:
· A column of headings, or categories, on the left
· Rows of content on the right, divided into the appropriate heading
· A transparent title bar at the top containing a title and an optional search icon
You can see how these parts are arranged in Figure 19-1.
Figure 19‐1 Android TV’s BrowseFragment layout.
Creating the MainFragment outline
Let’s focus first on putting your tasks into the section on the right.
Create a new class named MainActivity in the com.dummies.tasks.tv package, and add the following code to it:
public class MainFragment extends BrowseFragment
implements LoaderManager.LoaderCallbacks<Cursor> →2
{
@Override
public void onActivityCreated(Bundle savedInstanceState) { →5
super.onActivityCreated(savedInstanceState);
setTitle(getString(R.string.app_name)); →8
setBrandColor(getResources().getColor(R.color.primary)); →9
ArrayObjectAdapter adapter
= new ArrayObjectAdapter(new ListRowPresenter()); →12
setAdapter(adapter); →14
}
@Override →18
public Loader<Cursor> onCreateLoader(int id, final Bundle args) {
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
} →28
}
This class creates a new BrowseFragment named MainFragment, sets some properties on the fragment, creates an adapter, and then sets up the loader callbacks needed to populate the adapter. For a refresher on how loaders and adapters work, see Chapter 13.
Here is more information about the previous listing:
→ 2 This line creates the MainFragment, declares that it extends BrowseFragment, and implements the loader callbacks that you’ll need to load data into your adapter.
→ 5 These lines override the onActivityCreated() method, and call super.onActivityCreated() to make sure that the fragment is initialized properly. onActivityCreated is called when the activity has been created and the fragment has been attached to it. This is where you’ll do most of your initialization code for the fragment. See Chapter 9 for more examples of using onActivityCreated.
→ 8 Sets the title of the activity. This will be displayed in the upper right‐hand side of the screen, as in Figure 19-1.
→ 9 Sets the “brand” color of the activity. This is the color that will be used for the background of the left half of Figure 19-1 containing your headers. For phones and tablets, you used this color in your action bar, but because TVs have no action bar, you will use this color on the left side of the screen.
→ 12 Creates a new ArrayObjectAdapter which will be the main adapter used by the BrowseFragment. Recall from Chapter 13 that an adapter knows how to read a list of items (usually from a database) and create views for them. In this case, anArrayObjectAdapter knows how to read a list of items from an array and create views for them.
Why an array rather than a database? The array contains one entry for each row in the grid in Figure 19-1. Each row has its own adapter, and that adapter reads items from the database. So you’ll still be reading from the database, but not directly from this adapter.
Android TV adapters require a Presenter object to create views from objects. Presenters are very similar to the RecyclerView.Adapters you used in Chapter 13, but they are not position based (their methods take objects rather than positions). In this case you are using the built‐in ListRowPresenter, which knows how to take ListRow objects (which you will create in the next section) and create views for them.
→ 14 Tells the BrowseFragment to use the adapter you just created.
→ 18–28 Adds the loader callback methods that you’ll need to use a loader. See Chapter 13 for more information about loaders. These callbacks are not fully implemented yet.
Reading data from the database
The next step is to actually read your tasks from the database. You will use a CursorObjectAdapter with a loader to load your tasks.
First you will need a simple model class to represent a task. Create a new file named Task.java in com.dummies.tasks.tv and add the following:
public class Task {
long id;
String title;
String notes;
}
This class will hold the data that you read out of the database.
Displaying tasks using loaders and CardPresenters
Now that you have the Task model, you need to set up your fragment so that it can read items from the database and present them to the user. This is similar to using the loaders and adapters you used in Chapter 9, except for Android TV you will also use a Presenter and aCursorMapper.
Open MainFragment.java again and add the lines in bold:
public class MainFragment extends BrowseFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setTitle(getString(R.string.app_name));
setBrandColor(getResources().getColor(R.color.primary));
ArrayObjectAdapter adapter
= new ArrayObjectAdapter(new ListRowPresenter()); →13
CardPresenter cardPresenter = new CardPresenter(); →15
CursorMapper simpleMapper = new CursorToTaskMapper(); →16
HeaderItem header = new HeaderItem(0,"All", null); →18
CursorObjectAdapter cursorObjectAdapter
= new CursorObjectAdapter(cardPresenter); →20
cursorObjectAdapter.setMapper(simpleMapper); →21
adapter.add(new ListRow(header, cursorObjectAdapter)); →23
setAdapter(adapter);
LoaderManager loaderManager = getLoaderManager(); →28
loaderManager.initLoader(0, null, this); →29
}
@Override
public Loader<Cursor> onCreateLoader(int id, final Bundle args) { →35
return new CursorLoader(getActivity(), →36
TaskProvider.CONTENT_URI,
null, null,null,null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
ListRow row = (ListRow) getAdapter().get(0); →43
CursorObjectAdapter rowAdapter
= (CursorObjectAdapter) row.getAdapter(); →45
rowAdapter.swapCursor(cursor); →46
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to
// onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
ListRow row = (ListRow) getAdapter().get(0); →55
CursorObjectAdapter rowAdapter
= (CursorObjectAdapter) row.getAdapter(); →57
rowAdapter.swapCursor(null); →58
}
}
Here is what the code is doing:
→ 15 In the previous section you used a ListRowPresenter to convert rows into views. Similarly, on this line you will use a CardPresenter to convert each task into a card. The CardPresenter does not exist yet; you will write it in the next section.
→ 16 Every CursorObjectAdapter must have a CursorMapper that knows how to map rows in the database to objects that the adapter can read. You will create one called CursorToTaskMapper a little later.
→ 18 Each row in the grid needs to have a header associated with it. For your first header, the row will contain all the tasks in the database, so name this header "All" and give it an id of 0.
→ 20 On this line you finally get to meet the CursorObjectAdapter that we’ve been referring to throughout this section. The CursorObjectAdapter is an adapter that knows how to read rows from the database and convert them into views. To do that, it uses the mapper and the presenter that you configured earlier and are added to the CursorObjectAdapter on lines 20 and 21. Remember that the CursorObjectAdapter is the second adapter you’ve created for this fragment. The first (and main) adapter is theArrayObjectAdapter you created in the previous section. The ArrayObjectAdapter represents the rows in our grid, and the CursorObjectAdapter represents the items in the row.
→ 23 You created the CursorObjectAdapter on line 20, so now you need to add it to the ArrayObjectAdapter. Recall that the ListRowPresenter that you added on line 13 knows how to read ListRow objects, so on this line you create a new ListRowobject and add it to the adapter. The ListRow object represents one row in the grid, so it will have the "All" header you created on line 19 and the CursorObjectAdapter that you created on line 20. Together, those two objects contain all the data (the header and the items) needed to construct one row in the grid.
→ 28–29 Kicks off the loader so that it starts running. initLoader will call your onCreateLoader callback on line 35 to create the loader, and then it will call the onLoadFinished callback when the data is done loading. Refer to Chapter 13 for more information about initLoader.
→ 36 onCreateLoader is called when the fragment has been asked to create a loader, so create a new CursorLoader that knows how to load tasks from the TaskProvider. This code is the same as what you used in Chapter 13 to load tasks from the database.
→ 43–47 onLoadFinished is called when the database has finished loading data and has a cursor to give you. You need to take that cursor and hand it to the adapter. But remember there are two adapters, so which one? Call getAdapter() to get the main adapter from the fragment, which in this case is the ArrayObjectAdapter. Then get the first object from that adapter (which you added on line 23). That object is a ListRow, so call getAdapter() on the ListRow and that will return to you theCursorObjectAdapter that you set on line 23.
Now that you have the CursorObjectAdapter, call swapCursor() to set the cursor that the loader just gave you. This is similar to how you implemented onLoadFinished in Chapter 13, except it requires an additional step to find the right adapter.
→ 58 Do the same thing you did above, but in this case set the cursor to null. onLoaderReset is called when the loader is reset, and it should zero‐out any data that it’s holding onto. In this case, that means swapping out the old cursor and setting the cursor to null. This is similar to what you did in Chapter 13.
Mapping database cursors to tasks
The next step is to add the CursorToTaskMapper referenced on line 16 in the previous code. Create a new class in com.dummies.tasks.tv called CursorToTaskMapper , and add the following to it:
public class CursorToTaskMapper extends CursorMapper {
int idIndex; →2
int titleIndex;
int notesIndex; →4
@Override
protected void bindColumns(Cursor cursor) { →7
idIndex = →8
cursor.getColumnIndexOrThrow(TaskProvider.COLUMN_TASKID);
titleIndex =
cursor.getColumnIndexOrThrow(TaskProvider.COLUMN_TITLE);
notesIndex =
cursor.getColumnIndexOrThrow(TaskProvider.COLUMN_NOTES); →13
}
@Override
protected Task bind(Cursor cursor) {
long id = cursor.getLong(idIndex); →18
String title = cursor.getString(titleIndex);
String notes = cursor.getString(notesIndex); →20
Task t = new Task(); →22
t.id=id; →23
t.title=title;
t.notes=notes; →25
return t; →26
}
}
The CursorToTaskMapper knows how to read a row in the cursor and convert it to a Task object. The following explains the way this is done:
→ 2–4 Creates fields that store the indices of the ID, title, and notes columns in the database. Recall from Chapter 13 that you must know the index of the column you want to retrieve from the cursor.
→ 7 Overrides the bindColumns() method. This method is called once when the cursor is obtained so that you can ask the cursor what the indices of the columns are. Lines 8–13 retrieve these indices and store them in the fields on lines 2–4. Refer to Chapter 13 for more information about using cursors.
→ 18–20 The bind() method is called to generate a Task object from a cursor. Lines 18–20 get the ID, title, and notes of the task from the cursor by using the column indices that were obtained in bindColumns on line 7.
→ 22–26 Creates a new Task object, and sets its fields based on the data you obtained from lines 18–20, then returns the object.
Now your TV app is very close to working. The only thing you still need to implement is the CardPresenter, which knows how to convert tasks into cards.
Creating the CardPresenter
Presenters are objects in Android TV used by adapters to convert objects (in this case, tasks) into views. They are similar to the RecyclerView.Adapters you used in Chapter 9, so they should look familiar to you.
Create a new class named CardPresenter.java in the com.dummies.tasks.tv package, and add the following code:
public class CardPresenter extends Presenter { →1
private static int CARD_WIDTH = 313; →2
private static int CARD_HEIGHT = 176; →3
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) { →6
Context context = parent.getContext(); →7
ImageCardView cardView = new ImageCardView(context); →8
cardView.setFocusable(true); →9
cardView.setFocusableInTouchMode(true); →10
cardView.setBackgroundResource(R.color.window_background); →11
return new ViewHolder(cardView); →12
}
@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
→16
Task task = (Task)item; →17
// Update card
ViewHolder vh = (ViewHolder) viewHolder; →20
ImageCardView cardView = vh.cardView; →21
cardView.setTitleText(task.title);
cardView.setContentText(task.notes); →23
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT); →24
Context context= cardView.getContext(); →26
Picasso.with(context) →27
.load(TaskListAdapter.getImageUrlForTask(task.id)) →28
.resize(CARD_WIDTH, CARD_HEIGHT) →29
.centerCrop() →30
.into(cardView.getMainImageView());→31
}
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { →35
}
// The ViewHolder class
static class ViewHolder extends Presenter.ViewHolder { →39
ImageCardView cardView;
public ViewHolder(View view) {
super(view);
cardView = (ImageCardView) view;
}
}
}
About this class:
→ 1 Every presenter class must be a subclass of the Presenter class and must implement three methods and one class:
· onCreateViewHolder
· onBindViewHolder
· onUnbindViewHolder
· A ViewHolder class
→ 2–3 The width and height of the ImageCardView. This needs to be in Java because you are creating views in Java rather than in an XML layout file.
→ 6 onCreateViewHolder is called when the ViewHolder is created. One ViewHolder is created for every visible card on the screen. So if your screen is large enough to show four cards at once, approximately four ViewHolders and CardViews are created, no matter how long your list is. See Chapter 9 for more information about using ViewHolders.
→ 7 Gets the current context from the parent view. You will need this later.
→ 8 Creates a new instance of Android TV’s built‐in ImageCardView. This view will be recycled over and over to display tasks in a row. You can see examples of the ImageCardView in Figure 19-1 on the right‐hand side. In Chapter 9 you used aLayoutInflater to inflate an XML layout for the card view there, but it’s also okay to create views directly in Java instead of using XML.
→ 9–10 Makes sure that the ImageCardView is focusable when using a D‐pad and when using touch.
For more information on using focus for navigation on devices without touchscreens, see http://d.android.com/guide/topics/ui/ui‐events.html#HandlingFocus.
→ 11 Sets the background of the view to the default window background color for this theme.
→ 12 Creates a new ViewHolder, defined on line 45, and returns it for the view you just created. See Chapter 9 for more information about using ViewHolders.
→ 16 onBindViewHolder is called when it’s time to populate a card with data from the object in the adapter. onBindViewHolder will get the card view from the ViewHolder object, and then update that card to reflect the data in the object that’s passed in. As you scroll through your list, onBindViewHolder will be called every time an item scrolls onto the screen. Again, see Chapter 9 for more information about binding views to objects.
→ 17 The item in this case is a Task object (because the mapper returns tasks), so this line casts the item to a Task.
→ 20 Casts the viewHolder object to a CardPresenter.ViewHolder (rather than a Presenter.ViewHolder) so that you can access the cardView on line 9.
→ 21–23 Sets the title and content of the cardView using the data from the task.
→ 24 Sets the dimensions of the ImageView inside the ImageCardView to be the size from line 2–3.
→ 26 Gets the current context from the cardView.
→ 27 Uses Picasso to download the image for this task. Refer to Chapter 9 for more information about using Picasso. In this case, you will load the image using the task’s image URL obtained from getImageUrlForTask() (line 28) into the ImageView that’s managed by the cardView on line 31. In addition, on line 29 and 30, you will resize and crop the image to fit the size of the ImageView.
→ 35 onUnbindViewHolder is called whenever a view is about to be unbound from a ViewHolder. It’s the opposite of onBindViewHolder on line 16. Most presenters do not need to do anything here.
→ 39 Most presenters will need to implement a ViewHolder that contains references to the views that need to be updated in onBindViewHolder. This ViewHolder is a simple one that has a reference to a single view, Android TV’s built‐in ImageCardView. See Chapter 9 for more information about using ViewHolders.
Running Your App
Your app isn’t done yet, but it should be possible to build and run it. To do that, you’ll need to create an Android TV emulator.
Choose Tools ⇒ Android ⇒ AVD Manager, click Create Virtual Device, and create a new device with the settings in Table 19-1.
Table 19‐1 Settings for Creating a New TV Emulator
Category |
TV |
Name |
Android TV (720p) |
Release name |
Lollipop |
API level |
21 |
ABI |
x86 |
See Chapter 3 for more information about creating an Android emulator.
Once the emulator has been created, click the Run icon to start it up. Be aware that because the Android TV does not have a touchscreen, many items in the interface may not be clickable. Instead, you should use the arrow keys on your keyboard to navigate the Android TV emulator interface.
Now that the emulator has been created, go to Android Studio and select Run ⇒ Run ’Tasks’, then choose to run it on the emulator that you just created. You should see something like Figure 19-2.
Figure 19‐2 The Tasks app running on Android TV with no data.
Well, that’s fun, but there’s no data to view. And there’s no way to add data! Let’s fix that.
Adding and Editing Items
Android TV isn’t really designed for inputting data. There’s no keyboard on most devices, and although there’s a virtual onscreen keyboard, using it with a standard TV remote control can be a real pain.
For that reason, the BrowseFragment doesn’t really have a built‐in way for adding items to the database. But without a way to add items to the database, how are you going to test your app?
The trick is to launch the TaskEditActivity on your emulator, directly from Android Studio. Once the TaskEditActivity is running, you can use it to save tasks to the database. Your users can’t launch TaskEditActivity directly from the app, but you can launch it from Android Studio for testing purposes.
Because TVs aren’t a good way to input data, it probably doesn’t make sense to have a permanent Add Item button on the Tasks app for TVs. The technique in this section is a good way to test your app, but most users will expect your TV Tasks app to sync with their apps on their phones. Cloud storage is not covered in this book, but take a look at Google Cloud Save (http://developer.android.com/google/gcs ) for one potential way to sync your tasks between devices.
Using voice input can be a great way to allow your users to add data to apps on your TV. Many Android TVs support voice input either directly on the TV, or built into the Android TV remote. For more information about using Voice Input on Android, visit the book’s web extras online at www.dummies.com/extras/androidappdevelopment.
To launch TaskEditActivity, open the TaskEditActivity.java file and right‐click on TaskEditActivity, then choose Run ’TaskEditActivity’, as in Figure 19-3.
Figure 19‐3 Running the TaskEditActivity from Android Studio.
The TaskEditActivity should run on your emulator, and you should be able to save a new task into your database. If you repeat this a few times, you should see a few items in your app, like in Figure 19-4.
Figure 19‐4 The Tasks app with one item selected.
Creating Backgrounds
As mentioned in the first section, Android TV apps should be a little bit more cinematic than their phone and tablet counterparts. Let’s add a touch of visual flair by changing the background of the app when you select each task.
Open MainFragment.java and add the lines in bold:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final BackgroundManager backgroundManager
= BackgroundManager.getInstance(getActivity()); →6
backgroundManager.attach(getActivity().getWindow()); →7
. . .
setOnItemViewSelectedListener(→11
new OnItemViewSelectedListener() {
@Override
public void onItemSelected(Presenter.ViewHolder
itemViewHolder,
Object item,
RowPresenter.ViewHolder
rowViewHolder, Row row)
{
if( itemViewHolder==null ) →20
return;
ImageCardView cardView =
((CardPresenter.ViewHolder)itemViewHolder).cardView; →24
Drawable d = cardView.getMainImage(); →25
if(d!=null) {→26
Bitmap b = ((BitmapDrawable)d).getBitmap(); →27
backgroundManager.setBitmap(b); →28
}
}
}
);
. . .
}
Here is what the code does:
→ 6 Gets a BackgroundManager from the activity. The Android TV BackgroundManager is responsible for setting the background of the app.
→ 7 Every time you want to use a BackgroundManager, you must make sure that it is associated with the current activity’s window.
→ 11 Calls setOnItemViewSelectedListener() when you want to set the listener that is invoked whenever an item is selected. You will use this listener to change the background to be a scaled‐up version of the image for the currently selected task.
→ 20 You must use the itemViewHolder to get the current cardView for the selected item, but the itemViewHolder may sometimes be null. This can happen when a row header is selected rather than a row item. To protect against this case, make sure to check for null.
→ 24 Gets the cardView from the ViewHolder.
→ 25–27 Gets the bitmap from the ImageCardView by first getting the drawable, and then getting the bitmap from the drawable. It’s possible that the drawable may be null if it hasn’t been downloaded from the network yet, so make sure to check for that case.
→ 28 Uses the BackgroundManager to set the background to the bitmap you got from line 27.
BackgroundManager has a setDrawable() method, so you might be tempted to call backgroundManager.setDrawable() using the drawable on line 25 and skipping line 26 entirely. Do not do this; it’s not safe to reuse drawables in multiple places.ImageViews may modify their drawables, and if you’re using the same drawable in two different ImageViews, you can get some very weird‐looking behavior. ImageViews do not modify bitmaps, so they are safe to reuse here.
Try running the app now, and you will see that the background changes as you select different tasks.
Creating More Filters
Currently, the only header you have on the left‐hand side of the app is the “All” filter. Let’s add some additional filters to make it easier for users to navigate their tasks. In this section, you add the following filters:
· All
· Today
· This Week
· This Month
· This Year
The way to do this is to add one HeaderItem for each new filter, then add a new CursorObjectAdapter for each filter’s row of data. You also need to create several more loaders to handle all the new CursorObjectAdapters.
First add the following to your MainFragment:
public static final Object[] CATEGORIES[] = {
new Object[]{ "All",new int[]{
Calendar.YEAR,
Calendar.DAY_OF_YEAR,
Calendar.HOUR_OF_DAY,
Calendar.MINUTE,
Calendar.SECOND
}
},
new Object[]{ "Today", new int[]{
Calendar.HOUR_OF_DAY,
Calendar.MINUTE,
Calendar.SECOND
}
},
new Object[]{"This Week", new int[]{
Calendar.DAY_OF_WEEK,
Calendar.HOUR_OF_DAY,
Calendar.MINUTE,
Calendar.SECOND
}
},
new Object[]{"This Month", new int[]{
Calendar.DAY_OF_MONTH,
Calendar.HOUR_OF_DAY,
Calendar.MINUTE,
Calendar.SECOND
}
},
new Object[]{ "This Year",new int[]{
Calendar.DAY_OF_YEAR,
Calendar.HOUR_OF_DAY,
Calendar.MINUTE,
Calendar.SECOND
}
},
};
This static field defines all the categories you’re going to use as headers on the left‐hand side of the app. It also defines the fields that you need to zero‐out if you want to take a timestamp and create a filter for it.
Let’s say that you want to find all the tasks that have a reminder set for today. To do that, you would take a timestamp that represents your time right now, and zero‐out the hour, minutes, and seconds to get the time at midnight this morning. Anything with a reminder after midnight would be selected by your filter.
Similarly, if you want to find all of the reminders that are set this week, you would still zero‐out the hours, minutes, and seconds, but you would ALSO zero‐out the day of the week. This would tell you the time that the current week started, so any reminder after that time would be for this week.
Now that you’ve defined your filters, you just need to use them. Edit MainFragment again and change the following lines in onActivityCreated:
HeaderItem header = new HeaderItem(0,"All", null);
CursorObjectAdapter cursorObjectAdapter
= new CursorObjectAdapter(cardPresenter);
cursorObjectAdapter.setMapper(simpleMapper);
adapter.add(new ListRow(header, cursorObjectAdapter));
for( int i=0; i< CATEGORIES.length; ++i ) { →9
HeaderItem header = new HeaderItem(i,
(String)CATEGORIES[i][0], null); →11
CursorObjectAdapter cursorObjectAdapter
= new CursorObjectAdapter(cardPresenter); →13
cursorObjectAdapter.setMapper(simpleMapper); →14
adapter.add(new ListRow(header, cursorObjectAdapter)); →16
}
setAdapter(adapter);
LoaderManager loaderManager = getLoaderManager();
loaderManager.initLoader(0, null, this); →22
for( int i=0; i<CATEGORIES.length; ++i) →24
loaderManager.initLoader(i, null, this); →25
You just took the previous code that created a single row in the grid, and replaced it with code that created one row for each item in the CATEGORIES variable. Here’s how the code works:
→ 9 Loops over each item in the CATEGORIES array. Each of these categories will become a row in your grid.
→ 11 Creates a HeaderItem for each category in the for loop. The ID of the HeaderItem will be the current position in the category array (i), and the name of the HeaderItem will be set to "All", "Today", "This Week", and so on as appropriate. The last parameter is an optional image URL which you won’t use for the category headers.
→ 13 Because each category corresponds to a row in the grid, this line creates a new CursorObjectAdapter to load the data for that row. As before, you create the CursorObjectAdapter and pass in a cardPresenter and a simpleMapper (lines 13 and 14).
→ 16 Just like before, this line creates a new ListRow using the HeaderItem and the CursorObjectAdapter, and adds it to the ArrayObjectAdapter.
→ 22 Make sure you delete this line.
→ 24–25 Instead of initializing just one loader, this line calls initLoader() once for each row in the grid. You pass in the index of the row as the ID of the loader to initialize. You will use that ID later to find the correct loader in onCreateLoader.
The last step is to update the loader callbacks to know that they need to work with multiple loaders rather than just one. Replace your existing loader callbacks with the following:
@Override
public Loader<Cursor> onCreateLoader(int id, final Bundle args) {
long filterTimestamp = getFilterTimeForSelectedFilter(id); →3
return new CursorLoader(getActivity(), →4
TaskProvider.CONTENT_URI,
null,
TaskProvider.COLUMN_DATE_TIME + "> ?", →7
new String[]{Long.toString(filterTimestamp)}, →8
null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
int id = loader.getId(); →14
ObjectAdapter adapter = getAdapter(); →15
ListRow row = (ListRow) adapter.get(id); →16
CursorObjectAdapter rowAdapter = (CursorObjectAdapter) row
.getAdapter(); →18
rowAdapter.swapCursor(cursor); →19
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
int id = loader.getId(); →24
ObjectAdapter adapter = getAdapter(); →25
ListRow row = (ListRow) adapter.get(id); →26
CursorObjectAdapter rowAdapter = (CursorObjectAdapter) row
.getAdapter();
rowAdapter.swapCursor(null);
}
The last two methods are virtually identical to their predecessors, only now they use the ID of the loader to pick the correct adapter to update. The first method requires a little more explanation:
→ 3 Calls getFilterTimeForSelectedFilter() to get the timestamp for the selected filter. For example, if the filter was "Today", then the timestamp will correspond to midnight this morning as described previously in this section. You’ll add thegetFilterTimeForSelectedFilter() shortly.
→ 4 As before, this line creates the CursorLoader using the CONTENT_URI for our task ContentProvider. The only difference is on lines 7 and 8. Line 7 specifies a filter criteria for the query. It’s beyond the scope of this book to explain SQL, but the idea is that you create a query that says "task_date_time > ?", and the ? will be replaced by the time you specify on line 8. This way, each CursorLoader gets its own unique query (one for Today, one for This Week, one for This Month, and so on).
→ 14–16 The functions onLoadFinished() and onLoaderReset() are nearly identical to what they were before, except instead of assuming that they should use the CursorObjectAdapter in index 0 of the ArrayObjectAdapter like they did before, they now get the ID of the loader from the loader object, and use that as the index into the ArrayObjectAdapter to find the correct CursorObjectLoader. You’re using the loader’s ID to pass around the index for the correct adapter.
→ 24–26 The same as lines 14–16.
Finally, add the getFilterTimeForSelectedFilter method:
private long getFilterTimeForSelectedFilter(int id) { →1
Calendar calendar = Calendar.getInstance(); →2
int[] calendarFieldsToZero = (int[])CATEGORIES[id][1]; →3
for( int fieldToZero : calendarFieldsToZero ) →5
calendar.set( →6
fieldToZero,
calendar.getActualMinimum(fieldToZero)); →8
return calendar.getTimeInMillis(); →10
}
This method isn’t really related to Android at all, but what it does is this:
→ 1 getFilterTimeForSelectedFilter takes an index into the CATEGORIES array that indicates which filter to use. Index 0 is "All", index 1 is "Today", and so on.
→ 2 Gets a new calendar instance that corresponds to “now.”
→ 3 Gets the list of fields to zero‐out from the array. For example, if the selected filter is "Today", then according to the CATEGORIES array, the fields will be HOUR_OF_DAY, MINUTE, SECOND.
→ 5–8 For each field that needs to be zeroed, these lines call calendar.set() on the field and set it to its minimum. In most cases, this will be zero or one, but in some cases it may be other values. For example, the beginning of the week is considered to be SUNDAY (1) in the U.S., but in Europe it is considered to be MONDAY (2). Calendar.getActualMinimum() will tell us the appropriate value for each field, given the user’s current locale.
Programming dates and times in Java can get quite complicated. For more information about Java’s date and time classes, visit https://docs.oracle.com/javase/tutorial/datetime/iso. You may also be interested in checking out ThreeTenBackport athttp://www.threeten.org/threetenbp.
→ 10 Returns the timestamp for the result, in UNIX time (a long representing milliseconds since Jan 1, 1970 UTC).
Run the app again and create a few tasks with reminder times today, yesterday, and earlier this month, and you should see something similar to Figure 19-5.
Figure 19‐5 The completed Tasks app on Android TV.