Android Ranch Programming
Android Ranch Programming
Learning Android
In your first app, you will explore the fundamentals of Android projects,
activities, layouts, and explicit intents.
CriminalIntent
The largest app in the book, CriminalIntent lets you keep a record of your
colleagues lapses around the office. You will learn to use fragments, masterdetail interfaces, list-backed interfaces, menus, the camera, implicit intents,
and more.
BeatBox
Intimidate your foes with this app while you learn more about fragments,
media playback, themes, and drawables.
NerdLauncher
Building this custom launcher will give you insight into the intent system
and tasks.
xviii
Challenges
PhotoGallery
A Flickr client that downloads and displays photos from Flickrs public
feed, this app will take you through services, multithreading, accessing web
services, and more.
DragAndDraw
In this simple drawing app, you will learn about handling touch events and
creating custom views.
Sunset
In this toy app, you will create a beautiful representation of a sunset over open
water while learning about animations.
Locatr
This app lets you query Flickr for pictures around your current location and
display them on a map. In it, you will learn how to use location services and
maps.
Challenges
Most chapters have a section at the end with exercises for you to work through. This is your
opportunity to use what you have learned, explore the documentation, and do some problem solving on
your own.
We strongly recommend that you do the challenges. Going off the beaten path and finding your way
will solidify your learning and give you confidence with your own projects.
If you get lost, you can always visit http://forums.bignerdranch.com for some assistance.
Code Style
There are two areas where our choices differ from what you might see elsewhere in the Android
community:
We use anonymous inner classes for listeners.
This is mostly a matter of opinion. We find it makes for cleaner code in the applications in this
book because it puts the listeners method implementations right where you want to see them. In
high-performance contexts or large applications, anonymous inner classes may cause problems,
but for most circumstances they work fine.
After we introduce fragments in Chapter 7, we use them for all user interfaces.
Fragments are not an absolutely necessary tool but we find that, when used correctly, they are
a valuable tool in any Android developers toolkit. Once you get comfortable with fragments,
they are not that difficult to work with. Fragments have clear advantages over activities that
make them worth the effort, including flexibility in building and presenting your user interfaces.
xix
Click Next.
Activity
Leave Generate Layout File checked. The layout name will automatically update to activity_quiz
to reflect the activitys new name. The layout name reverses the order of the activity name, is all
lowercase, and has underscores between words. This naming style is recommended for layouts as well
as other resources that you will learn about later.
If your version of Android Studio has other options on this screen, leave them as is. Click Finish.
Android Studio will create and open your new project.
The template has already added a few string resources for you. Remove the unused string named
hello_world and add the three new strings that your layout requires.
(Depending on your version of Android Studio, you may have additional strings. Do not delete them.
Deleting them could cause cascading errors in other files.)
Now, whenever you refer to @string/false_button in any XML file in the GeoQuiz project, you will
get the literal string False at runtime.
Save strings.xml. If you had errors in activity_quiz.xml about the missing string resources, they
should now be gone. (If you still have errors, check both files for typos.)
Although the default strings file is named strings.xml, you can name a strings file anything you want.
You can also have multiple strings files in a project. As long as the file is located in res/values/, has
a resources root element, and contains child string elements, your strings will be found and used
appropriately.
15
android.support.v7.app.AppCompatActivity;
android.os.Bundle;
android.view.Menu;
android.view.MenuItem;
This method inflates a layout and puts it on screen. When a layout is inflated, each widget in the layout
file is instantiated as defined by its attributes. You specify which layout to inflate by passing in the
layouts resource ID.
17
Notice that there is a + sign in the values for android:id but not in the values for android:text. This
is because you are creating the IDs and only referencing the strings.
Wiring Up Widgets
Now that the buttons have resource IDs, you can access them in QuizActivity. The first step is to add
two member variables.
Type the following code into QuizActivity.java. (Do not use code completion; type it in yourself.)
After you save the file, it will report two errors.
20
QuizActivity.java.
import android.widget.Button;
Or you can do it the easy way and let Android Studio do it for you. Just press Option+Return (or Alt
+Enter) to let the IntelliJ magic under the hood amaze you. The new import statement now appears
with the others at the top of the file. This shortcut is generally useful when something is not correct
with your code. Try it often!
This should get rid of the errors. (If you still have errors, check for typos in your code and XML.)
Now you can wire up your button widgets. This is a two-step process:
get references to the inflated View objects
set listeners on those objects to respond to user actions
21
First, return to strings.xml and add the string resources that your toasts will display.
To create a toast, you call the following method from the Toast class:
public static Toast makeText(Context context, int resId, int duration)
The Context parameter is typically an instance of Activity (Activity is a subclass of Context). The
second parameter is the resource ID of the string that the toast should display. The Context is needed
by the Toast class to be able to find and use the strings resource ID. The third parameter is one of two
Toast constants that specify how long the toast should be visible.
After you have created a toast, you call Toast.show() on it to get it on screen.
In QuizActivity, you are going to call makeText() in each buttons listener (Listing 1.12). Instead of
typing everything in, try using Android Studios code completion feature to add these calls.
24
29
Notice that you use the escape sequence \' in the last value to get an apostrophe in your string. You
can use all the usual escape sequences in your string resources, such as \n for a new line.
Save your files. Then return to activity_quiz.xml and preview your layout changes in the graphical
layout tool.
That is all for now for GeoQuizs view layer. Time to wire everything up in your controller class,
QuizActivity.
Button mTrueButton;
Button mFalseButton;
Button mNextButton;
TextView mQuestionTextView;
private
new
new
new
new
new
};
41
hdpi
xhdpi
xxhdpi
(There are a few other density categories that are omitted from the solutions, including ldpi and
xxxhdpi.)
Within each directory, you will find two image files arrow_right.png and arrow_left.png. These
files have been customized for the screen pixel density specified in the directorys name.
You are going to include all the image files from the solutions in GeoQuiz. When it runs, the OS will
choose the best image file for the specific device running the app. Note that by duplicating the images
multiple times, you increase the size of your application. In this case, this is not a problem because
GeoQuiz is a simple app.
If an app runs on a device that has a screen density not included in any of the applications screen
density qualifiers, Android will automatically scale the available image to the appropriate size for the
device. Thanks to this feature, it is not necessary to provide images for all of the pixel density buckets.
To reduce the size of your application, you can focus on one or a few of the higher resolution buckets
and selectively optimize for lower resolutions when Androids automatic scaling provides an image
with artifacts on those lower resolution devices.
(For alternatives to duplicating images at different densities and an explanation of your mipmap
directory, see Chapter 21.)
49
...
Now override five more methods in QuizActivity by adding the following after onCreate(Bundle):
...
Notice that you call the superclass implementations before you log your messages. These superclass
calls are required. Calling the superclass implementation before you do anything else is critical in
onCreate(); the order is less important in the other methods.
59
Now run your app and press the Home button. Pressing Home causes the activity to be paused and
stopped. Then the stopped activity will be destroyed just as if the Android OS had reclaimed it for its
memory. Then you can restore the app to see if your state was saved as you expected. Be sure to turn
this setting off when you are done testing, as it will cause a performance decrease and some apps will
perform poorly.
Pressing the Back button instead of the Home button will always destroy the activity, regardless of
whether you have this development setting on. Pressing the Back button tells the OS that the user is
done with the activity.
73
You should then see a dialog like Figure 5.4. Set Activity Name to CheatActivity. This is the name of
your Activity subclass. Layout Name should be automatically set to activity_cheat. This will be the
base name of the layout file the wizard creates.
89
Now it is time to make the user interface look good. The screenshot at the beginning of the chapter
shows you what CheatActivitys view should look like. Figure 5.5 shows the widget definitions.
90
The android:name attribute is required, and the dot at the start of this attributes value tells the OS that
this activitys class is in the package specified in the package attribute in the manifest element at the
top of the file.
You will sometimes see a fully qualified android:name attribute:
android:name="com.bignerdranch.android.geoquiz.CheatActivity".
93
Starting an Activity
updateQuestion();
...
}
Starting an Activity
The simplest way one activity can start another is with the startActivity method:
public void startActivity(Intent intent)
You might guess that startActivity() is a static method that you call on the Activity subclass that
you want to start. But it is not. When an activity calls startActivity(), this call is sent to the OS.
In particular, it is sent to a part of the OS called the ActivityManager. The ActivityManager then
creates the Activity instance and calls its onCreate() method, as shown in Figure 5.7.
95
This static method allows us to create an Intent properly configured with the extras CheatActivity
will need. The answerIsTrue argument, a boolean, is put into the intent with a private name using
the EXTRA_ANSWER_IS_TRUE constant. You will extract this value momentarily. Using a newIntent()
method like this for your activity subclasses will make it easy for other code to properly configure their
launching intents.
Speaking of other code, use this new method in QuizActivitys cheat button listener now.
updateQuestion();
99
After the instance of QuizActivity is on screen, the user can press the Cheat! button. When this
happens, an instance of CheatActivity is started on top of the QuizActivity. These activities exist
in a stack (Figure 5.12).
Pressing the Back button in CheatActivity pops this instance off the stack, and the QuizActivity
resumes its position at the top, as shown in Figure 5.12.
A call to Activity.finish() in CheatActivity would also pop the CheatActivity off the stack.
107
});
int cx = mShowAnswer.getWidth() / 2;
int cy = mShowAnswer.getHeight() / 2;
float radius = mShowAnswer.getWidth();
Animator anim = ViewAnimationUtils
.createCircularReveal(mShowAnswer, cx, cy, radius, 0);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mShowAnswer.setVisibility(View.INVISIBLE);
}
});
anim.start();
The createCircularReveal method creates an Animator from a few parameters. First, you specify the
View that will be hidden or shown based on the animation. Next, a center position for the animation as
well as the start radius and end radius of the animation. You are hiding the Show Answer button, so the
radius moves from the width of the button to 0.
Before the newly created animation is started, you set a listener which allows you to know when the
animation is complete. Once complete, you will show the answer and hide the button.
Finally, the animation is started and the circular reveal animation will begin. You will learn much more
about animation in Chapter 30.
The ViewAnimationUtils and its createCircularReveal method were both added to the Android
SDK in API level 21, so this code would crash on a device running a lower version than that.
115
});
The Build.VERSION.SDK_INT constant is the devices version of Android. You then compare that
version with the constant that stands for the Lollipop release. (Version codes are listed at http://
developer.android.com/reference/android/os/Build.VERSION_CODES.html.)
116
Click Next and specify a minimum SDK of API 16: Android 4.1. Also ensure that only the Phone and
Tablet application type is checked.
Click Next again to select the type of Activity to add. Choose Empty Activity and continue along in the
wizard.
In the final step of the New Project wizard, name the activity CrimeActivity and click Finish
(Figure 7.8).
127
You may have additional dependencies specified here, such as the AppCompat dependency. If
you have other dependencies, do not remove them. You will learn about the AppCompat library in
Chapter 13.
Use the + button and choose Library dependency to add a new dependency (Figure 7.11). Choose the
support-v4 library from the list and click OK.
130
Before proceeding further with CrimeActivity, lets create the model layer for CriminalIntent by
writing the Crime class.
132
public Crime() {
// Generate unique identifier
mId = UUID.randomUUID();
}
Notice that you did not give the Button an android:text attribute. This button will display the date of
the Crime being displayed, and its text will be set in code.
Why display the date on a Button? You are preparing for the future. For now, a crimes date defaults
to the current date and cannot be changed. In Chapter 12, you will wire up the button so that a press
presents a DatePicker widget from which the user can set the date.
There are some new things in this layout to discuss, such as the style attribute and the margin
attributes. But first lets get CriminalIntent up and running with the new widgets.
Open res/values/strings.xml and add the necessary string resources.
152
Wiring Widgets
Wiring Widgets
Next, you are going to make the CheckBox display whether a Crime has been solved. You also need to
update the Crimes mSolved field when a user toggles the CheckBox.
For now, all the new Button needs to do is display the date in the Crimes mDate field.
In CrimeFragment.java, add two new instance variables.
Next, in onCreateView(), get a reference to the new button, set its text as the date of the crime, and
disable it for now.
return v;
153
}
}
...
In CrimeFragment, remove the call to the DatePickerFragment constructor and replace it with a call to
DatePickerFragment.newInstance(Date).
return v;
225
This method accepts the fragment that will be the target and a request code just like the one you send
in startActivityForResult(). The target fragment can use the request code later to identify which
fragment is reporting back.
The FragmentManager keeps track of the target fragment and request code. You can retrieve them by
calling getTargetFragment() and getTargetRequestCode() on the fragment that has set the target.
In CrimeFragment.java, create a constant for the request code and then make CrimeFragment the
target fragment of the DatePickerFragment instance.
return v;
...
227
tells Android to look for an existing instance of the activity in the stack,
and if there is one, pop every other activity off the stack so that the activity being started will be topmost (Figure 13.13).
The maxSdkVersion attribute makes it so that your app only asks for this permission on versions of
Android that are older than API 19, Android KitKat.
Note that you are only asking to read external storage. There is also a WRITE_EXTERNAL_STORAGE
permission, but you do not need it. You will not be writing anything to external storage: The camera
app will do that for you.
299
Chapter 18Assets
Creating BeatBox
Time to get started. The first step is to create your BeatBox app. In Android Studio, select File
New Project... to create a new project. Call it BeatBox, and give it a company domain of
android.bignerdranch.com. Use API 16 for your minimum SDK, and start with one Empty Activity
called BeatBoxActivity. Leave the defaults as they are.
You will be using RecyclerView again, so open your project
com.android.support:recyclerview-v7 dependency.
Now, lets build out the basics of the app. The main screen will show a grid of buttons, each of which
plays a sound. So, you will need two layout files: one for the grid and one for the buttons.
Create your layout file for the RecyclerView first. You will not need res/layout/
activity_beat_box.xml, so go ahead and rename it fragment_beat_box.xml. Then fill it up like so:
326
19
can load a large set of sounds into memory and control the maximum number of sounds
that are playing back at any one time. So if your apps user gets a bit too excited and mashes all the
buttons at the same time, it will not break your app or overtax your phone.
Ready? Time to get started.
Creating a SoundPool
The first step is to create a SoundPool object.
...
Lollipop introduced a new way of creating a SoundPool using a SoundPool.Builder. However, since
SoundPool.Builder is not available on your minimum-supported API 16, you are using the older
SoundPool(int, int, int) constructor instead.
339
Playing Sounds
Calling mSoundPool.load(AssetFileDescriptor, int) loads a file into your SoundPool for later
playback. To keep track of the sound and play it back again (or unload it), mSoundPool.load()
returns an int ID, which you stash in the mSoundId field you just defined. And since calling
openFd(String) throws IOException, load(Sound) throws IOException, too.
Now load up all your sounds by calling load(Sound) inside BeatBox.loadSounds().
Run BeatBox to make sure that all the sounds loaded correctly. If they did not, you will see red
exception logs in LogCat.
Playing Sounds
One last step: playing the sounds back. Add the play(Sound) method to BeatBox.
341
Before playing your soundId, you check to make sure it is not null. This might happen if the Sound
failed to load.
Once you are sure you have a non-null value, play the sound by calling SoundPool.play(int,
float, float, int, int, float). Those parameters are, respectively: the sound ID, volume on
the left, volume on the right, priority (which is ignored), whether the audio should loop, and playback
rate. For full volume and normal playback rate, you pass in 1.0. Passing in 0 for the looping value
says do not loop. (You can pass in -1 if you want it to loop forever. We speculate that this would be
incredibly annoying.)
With that method written, you can play the sound each time one of the buttons is pressed.
mButton = (Button)itemView.findViewById(R.id.list_item_sound_button);
mButton.setOnClickListener(this);
@Override
public void onClick(View v) {
mBeatBox.play(mSound);
}
Press a button, as shown in Figure 19.1, and you should hear a sound played.
342
Color resources are a convenient way to specify color values in one place that you reference
throughout your application.
Styles
Now, update the buttons in BeatBox with a style. A style is a set of attributes that you can apply to a
widget.
Navigate to res/values/styles.xml and add a style named BeatBoxButton. (When you created
BeatBox, your new project should have come with a built-in styles.xml file. If your project did not,
create the file.)
Here, you create a style called BeatBoxButton. This style defines a single attribute,
and sets it to a dark blue color. You can apply this style to as many widgets as
you like and then update the attributes of all of those widgets in this one place.
android:background,
Now that the style is defined, apply BeatBoxButton to your buttons in BeatBox.
Run BeatBox and you will see that all of your buttons now have a dark blue background color
(Figure 20.2).
354
Run BeatBox and verify that your button text is indeed bold, as in Figure 20.3.
356
Here, you define three theme attributes. These theme attributes look similar to the style attributes
that you set up earlier, but they specify different properties. Style attributes specify properties for an
individual widget, such as the textStyle that you used to bold the button text. Theme attributes have a
larger scope: they are properties that are set on the theme that any widget can access. For example, the
toolbar will look at the colorPrimary attribute on the theme to set its background color.
359
list_item_sound.xml
Run BeatBox and verify that your buttons are back to the old, bland look.
Navigate back to the Theme.Holo definition and look for a group of button attributes.
<style name="Theme.Holo">
...
<!-- Button styles -->
<item name="buttonStyle">@style/Widget.Holo.Button</item>
<item name="buttonStyleSmall">@style/Widget.Holo.Button.Small</item>
<item name="buttonStyleInset">@style/Widget.Holo.Button.Inset</item>
...
</style>
Notice the attribute named buttonStyle. This is the style of any normal button within your app.
The buttonStyle attribute points to a style resource rather than a value. When you updated the
colorBackground attribute, you passed in a value: the color. In this case, buttonStyle should point to
a style. Navigate to Widget.Holo.Button to see the button style.
<style name="Widget.Holo.Button" parent="Widget.Button">
<item name="background">@drawable/btn_default_holo_dark</item>
<item name="textAppearance">?attr/textAppearanceMedium</item>
<item name="textColor">@color/primary_text_holo_dark</item>
<item name="minHeight">48dip</item>
<item name="minWidth">64dip</item>
</style>
You specified a parent of android:style/Widget.Holo.Button. You want your button to inherit all of
the properties that a normal button would receive and then selectively modify attributes.
If you do not specify a parent theme for BeatBoxButton, you will notice that your buttons devolve into
something that does not look like a button at all. Properties you expect to see, such as the text centered
in the button, will be lost.
Now that you have fully defined BeatBoxButton, it is time to use it. Look back at the buttonStyle
attribute that you found earlier when digging through Androids themes. Duplicate this attribute in
your own theme.
You are now overriding the buttonStyle attribute from Androids themes and substituting your own
style: BeatBoxButton.
366
The ? notation says to use the resource that the colorAccent attribute on your theme points to. In your
case, this is the gray color that you defined in your colors.xml file.
You can also use theme attributes in code, although it is much more verbose.
Resources.Theme theme = getActivity().getTheme();
int[] attrsToFetch = { R.attr.colorAccent };
TypedArray a = theme.obtainStyledAttributes(R.style.AppTheme, attrsToFetch);
int accentColor = a.getInt(0, 0);
a.recycle();
On the Theme object, you ask to resolve the attribute R.attr.colorAccent that is defined in your
AppTheme: R.style.AppTheme. This call returns a TypedArray, which holds your data. On the
TypedArray, you ask for an int value to pull out the accent color. From here, you can use that color to
change the background of a button, for example.
The toolbar and buttons in BeatBox are doing exactly this to style themselves based on your theme
attributes.
Setting Up NerdLauncher
Create a new Android application project named NerdLauncher. Select Phone and Tablet as the form
factor and API 16: Android 4.1 (Jelly Bean) as the minimum SDK. Create an empty activity named
NerdLauncherActivity.
NerdLauncherActivity will host a single fragment and in turn should be a subclass of
SingleFragmentActivity. Copy SingleFragmentActivity.java and activity_fragment.xml
into
@Override
protected void onCreate(Bundle savedInstanceState) {
/* Auto-generated template code... */
}
NerdLauncherFragment
384
Creating PhotoGallery
Click Next. When prompted, check Phone and Tablet as the target form factor and choose API 16:
Android 4.1 (Jelly Bean) from the Minimum SDK dropdown.
Then have the wizard create an empty activity named PhotoGalleryActivity.
PhotoGallery will follow the same architecture you have been using so far. PhotoGalleryActivity
will be a SingleFragmentActivity subclass and its view will be the container view defined
in activity_fragment.xml. This activity will host a fragment in particular, an instance of
PhotoGalleryFragment, which you will create shortly.
Copy SingleFragmentActivity.java and activity_fragment.xml into your project from a previous
project.
In PhotoGalleryActivity.java, set up PhotoGalleryActivity as a SingleFragmentActivity
by deleting the code that the template generated and replacing it with an implementation of
createFragment(). Have createFragment() return an instance of PhotoGalleryFragment.
(Bear with the error that this code will cause for the moment. It will go away after you create the
PhotoGalleryFragment class.)
407
PhotoGallery will display its results in a RecyclerView, using the built-in GridLayoutManager to
arrange the items in a grid.
First, add the RecyclerView library as a dependency, as you did in Chapter 9. Open the Project
Structure window and select the app module on the left. Select the Dependencies tab and click the
+ button. Select Library dependency from the drop-down menu that appears. Find and select the
recyclerview-v7 library and click OK.
Rename layout/activity_photo_gallery.xml to layout/fragment_photo_gallery.xml to create a
layout for the fragment. Then replace its contents with the RecyclerView shown in Figure 23.4.
Finally, create the PhotoGalleryFragment class. Retain the fragment, inflate the layout you just
created, and initialize a member variable referencing the RecyclerView (Listing 23.2).
408
...
...
Run PhotoGallery, and you should see an array of close-up Bills, as in Figure 24.2.
431
A message loop consists of a thread and a looper. The Looper is the object that manages a threads
message queue. The main thread is a message loop and has a looper. Everything your main thread does
is performed by its looper, which grabs messages off of its message queue and performs the task they
specify.
You are going to create a background thread that is also a message loop. You will use a class called
HandlerThread that prepares a Looper for you.
Notice you gave the class a single generic argument, <T>. Your ThumbnailDownloaders user,
PhotoGalleryFragment in this case, will need to use some object to identify each download and to
433
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
...
}
@Override
public void onDestroy() {
super.onDestroy();
mThumbnailDownloader.quit();
Log.i(TAG, "Background thread destroyed");
}
...
You can specify any type for ThumbnailDownloaders generic argument. However, recall that this
argument specifies the type of the object that will be used as the identifier for your download. In this
case, the PhotoHolder makes for a convenient identifier as it is also the target where the downloaded
images will eventually go.
A couple of safety notes. One: notice that you call getLooper() after calling start() on your
(you will learn more about the Looper in a moment). This is a way to ensure
ThumbnailDownloader
434
In this case, your implementation of handleMessage() will use FlickrFetchr to download bytes
from the URL and then turn these bytes into a bitmap.
First, add the constant and member variables as shown in Listing 24.7.
...
The newly added mRequestHandler will store a reference to the Handler responsible for queueing
download requests as messages onto the ThumbnailDownloader background thread. This handler will
also be in charge of processing download request messages when they are pulled off the queue.
The mRequestMap variable is a ConcurrentHashMap. A ConcurrentHashMap is a thread-safe version
of HashMap. Here, using a download requests identifying object of type T as a key, you can store
and retrieve the URL associated with a particular request. (In this case, the identifying object is
a PhotoHolder, so the request response can be easily routed back to the UI element where the
downloaded image should be placed.)
Next, add code to queueThumbnail() to update mRequestMap and to post a new message to the
background threads message queue.
438
Using handlers
if (url == null) {
mRequestMap.remove(target);
} else {
mRequestMap.put(target, url);
mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD, target)
.sendToTarget();
}
You obtain a message directly from mRequestHandler, which automatically sets the new Message
objects target field to mRequestHandler. This means mRequestHandler will be in charge of
processing the message when it is pulled off the message queue. The messages what field is set to
MESSAGE_DOWNLOAD. Its obj field is set to the T target value (a PhotoHolder in this case) that is
passed to queueThumbnail().
The new message represents a download request for the specified T target (a PhotoHolder from the
RecyclerView). Recall that PhotoGalleryFragments RecyclerViews adapter implementation calls
queueThumbnail() from onBindViewHolder(), passing along the PhotoHolder the image is being
downloaded for and the URL location of the image to download.
Notice that the message itself does not include the URL. Instead you update mRequestMap with
a mapping between the request identifier (PhotoHolder) and the URL for the request. Later you
will pull the URL from mRequestMap to ensure that you are always downloading the most recently
requested URL for a given PhotoHolder instance. (This is important because ViewHolder objects in
RecyclerViews are recycled and reused.)
Finally, initialize mRequestHandler and define what that Handler will do when downloaded messages
are pulled off the queue and passed to it.
439
440
Passing handlers
You implemented Handler.handleMessage() in your Handler subclass within onLooperPrepared().
HandlerThread.onLooperPrepared() is called before the Looper checks the queue for the first time.
This makes it a good place to create your Handler implementation.
Within Handler.handleMessage(), you check the message type, retrieve the obj value (which will
be of type T and serves as the identifier for the request), and then pass it to handleRequest(). (Recall
that Handler.handleMessage() will get called when a download message is pulled off the queue and
ready to be processed.)
The handleRequest() method is a helper method where the downloading happens. Here you check for
the existence of a URL. Then you pass the URL to a new instance of your old friend FlickrFetchr. In
particular, you use the FlickrFetchr.getUrlBytes() method that you created with such foresight in
the last chapter.
Finally, you use BitmapFactory to construct a bitmap with the array of bytes returned from
getUrlBytes().
Run PhotoGallery and check LogCat for your confirming log statements.
Of course, the request will not be completely handled until you set the bitmap on the PhotoHolder that
originally came from PhotoAdapter. However, this is UI work, so it must be done on the main thread.
Everything you have seen so far uses handlers and messages on a single thread
ThumbnailDownloader putting messages in ThumbnailDownloaders own inbox. In the next section,
you will see how ThumbnailDownloader can use a Handler to post requests to a separate thread
(namely, the main thread).
Passing handlers
So far you are able to schedule work on the background thread from the main thread using
ThumbnailDownloaders mRequestHandler. This flow is shown in Figure 24.7.
You can also schedule work on the main thread from the background thread using a Handler attached
to the main thread. This flow looks like Figure 24.8.
441
Passing handlers
...
image
once it is complete.
443
...
Remember that by default, the Handler will attach itself to the Looper for the current thread. Because
this Handler is created in onCreate(), it will be attached to the main threads Looper.
Now ThumbnailDownloader has access via mResponseHandler to a Handler that is tied to the main
threads Looper. It also has your ThumbnailDownloadListener to do the UI work with the returning
Bitmaps. Specifically, the onThumbnailDownloaded implementation sets the Drawable of the originally
requested PhotoHolder to the newly downloaded Bitmap.
You could send a custom Message back to the main thread requesting to add the image to the UI,
similar to how you queued a request on the background thread to download the image. However, this
would require another subclass of Handler, with an override of handleMessage().
Instead, lets use another handy Handler method post(Runnable).
Handler.post(Runnable)
When a Message has its callback field set, it is not routed to its target Handler when pulled off the
message queue. Instead, the run() method of the Runnable stored in callback is executed directly.
In ThumbnailDownloader.handleRequest(), add the following code.
444
Passing handlers
});
mRequestMap.remove(target);
mThumbnailDownloadListener.onThumbnailDownloaded(target, bitmap);
Because mResponseHandler is associated with the main threads Looper, all of the code inside of
will be executed on the main thread.
run()
So what does this code do? First, you double-check the requestMap. This is necessary because the
recycles its views. By the time ThumbnailDownloader finishes downloading the Bitmap,
may have recycled the PhotoHolder and requested a different URL for it. This check
ensures that each PhotoHolder gets the correct image, even if another request has been made in the
meantime.
RecyclerView
RecyclerView
Next, you check mHasQuit. If ThumbnailDownloader has already quit, it may be unsafe to run any
callbacks.
Finally, you remove the PhotoHolder-URL mapping from the requestMap and set the bitmap on the
target PhotoHolder.
445
Then clean out your downloader in PhotoGalleryFragment when your view is destroyed.
...
With that, your work for this chapter is complete. Run PhotoGallery. Scroll around to see images
dynamically loading.
PhotoGallery has achieved its basic goal of displaying images from Flickr. In the next few chapters,
you will add more functionality, like searching for photos and opening each photos Flickr page in a
web view.
446
The advantage of RxJavas solution is that your eventBus is now also an Observable, RxJavas
representation of a stream of events. That means that you get to use all of RxJavas various event
manipulation tools. If that piques your interest, check out the wiki on RxJavas project page: https://
github.com/ReactiveX/RxJava/wiki.
29
527
Setting up DragAndDrawActivity
will be a subclass of SingleFragmentActivity that inflates the usual singlefragment-containing layout. Copy SingleFragmentActivity.java and its activity_fragment.xml
layout file into the DragAndDraw project.
DragAndDrawActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
...
}
Setting up DragAndDrawFragment
To prepare a layout for DragAndDrawFragment, rename the activity_drag_and_draw.xml layout file
to fragment_drag_and_draw.xml.
DragAndDrawFragments
528
30
Property Animation
For an app to be functional, all you need to do is write your code correctly so that it does not crash. For
an app to be a joy to use, though, you need to give it more love than that. You need to make it feel like
a real, physical phenomenon playing out on your phone or tablets screen.
Real things move. To make your user interface move, you animate its elements into new positions.
In this chapter, you will write an app that shows a scene of the sun in the sky. When you press on the
scene, it will animate the sun down below the horizon, and the sky will change colors like a sunset.
Rectangular views will make for a fine impression of the sky and the sea. But people will not buy a
rectangular sun, no matter how much you argue in favor of its technical simplicity. So, in the res/
drawable/ folder, add an oval shape drawable for a circular sun called sun.xml.
When you display this oval in a square view, you will get a circle. People will nod their heads in
approval, and then think about the real sun up in the sky.
539
Take a moment to run Sunset to make sure everything is hooked up correctly before moving on. It
should look like Figure 30.1. Ahhh.
541
heightAnimator.start();
Then hook up startAnimation() so that it is called every time the user presses anywhere in the scene.
return view;
Run Sunset and press anywhere on the scene to run the animation (Figure 30.2).
543
Creating Locatr
Now to get started. In Android Studio, create a new project called Locatr. Create an empty activity and
name it LocatrActivity. As you have for your other apps, set your minSdkVersion to 16 and copy in
SingleFragmentActivity and activity_fragment.xml.
You will also want some additional code from PhotoGallery. You will be querying Flickr again,
so having your old query code will be handy. Open up your PhotoGallery solution (anything after
Chapter 24 will do), select FlickrFetchr.java and GalleryItem.java, and right-click to copy them.
Then paste them into your Java code area in Locatr.
In a minute, you will get started on building out your user interface. If you are using an emulator,
though, read this next section so that you can test all the code you are about to write. If you are not,
feel free to skip on ahead to the section called Building out Locatr.
552
Run MockWalker and press Start. Its service will keep running after you exit the app. (Do not exit the
emulator, however. Leave the emulator running while you work on Locatr.) When you no longer need
those mock locations, open MockWalker again and press the Stop button.
If you would like to know how MockWalker works, you can find its source code in the solutions folder
for this chapter (see the section called Adding an Icon in Chapter 2 for more on the solutions). It
uses a few interesting things: RxJava and a sticky foreground service to manage the ongoing location
updates. If those sound interesting to you, check it out.
556
You also need a button to trigger the search. You can use your toolbar for that. Create res/menu/
fragment_locatr.xml and add a menu item to display a location icon. (Yes, this is the same filename
as res/layout/fragment_locatr.xml. This is no problem at all: menu resources live in a different
namespace.)
The button is disabled in XML by default. Later on, you will enable it once you are connected to Play
Services.
Now create a Fragment subclass called LocatrFragment that hooks up your layout and pulls out that
ImageView.
557
Flickr Geosearch
}
})
.build();
If you are curious, you can hook up an OnConnectionFailedListener and see what it reports. But it is
not necessary.
With that, your Google Play Services hookup is ready.
Flickr Geosearch
The next step is to add the ability to search for geographic locations on Flickr. To do this, you perform
a regular search, but you also provide a latitude and longitude.
In Android, the location APIs pass around these location fixes in Location objects. So write a new
override that takes in one of these Location objects and builds an appropriate search
query.
buildUrl()
563
Index
Symbols
9-patch images, 376
@+id, 20, 187
@Override, 60
A
aapt (Android Asset Packing tool), 30
action bar, tool bar vs., 254
action view, 455
ACTION_IMAGE_CAPTURE, 299
activities
(see also Activity, fragments)
about, 2
abstract fragment-hosting activity, 172
adding to project, 87-109
as controller, 37
back stack of, 107, 108, 393
base, 393
child, 88, 101
creating new, 89
fragment transactions and, 314
handling configuration changes in, 522
hosting fragments, 125, 133-136
label (display name), 388
launcher, 106
lifecycle and fragments, 146
lifecycle diagram, 70
lifecycle of, 57, 63, 64, 70, 71
managing fragments, 314-323
overriding methods, 58
passing data between, 97-106
record, 70
rotation and, 63-68
starting from fragment, 193
starting in current task, 393
starting in new task, 396
states of, 57, 70
tasks and, 393
UI flexibility and, 123
Activity
as Context subclass, 24
FragmentActivity, 128
getIntent(), 100, 196
onActivityResult(), 103
onCreate(), 17, 57, 59
onDestroy(), 57
onOptionsItemSelected(MenuItem), 17
onPause(), 57
onResume(), 57, 200
onSaveInstanceState(), 68-70, 349, 351
onStart(), 57
onStop(), 57
setContentView(), 17
setResult(), 103
SingleFragmentActivity, 172, 173, 175, 309
startActivity(), 95
startActivityForResult(),
101
activity record, 70
ActivityInfo, 391
ActivityManager
Index
components, 96
concurrent documents, 401-403
configuration changes, 64, 68, 346
configuration qualifiers
defined, 66
for screen density, 49
for screen orientation, 66
for screen size, 313, 323
ConnectivityManager, 470
contacts
getting data from, 285
permissions for, 287
container views, 135, 143
ContentProvider, 285
ContentResolver, 285
ContentValues, 263
Context, 272
AssetManager from, 332
basic file and directory methods in, 294
explicit intents and, 96
external file and directory methods in, 295
for opening database file, 258
getSharedPreferences(), 461
resource IDs and, 24
Context.getExternalFilesDir(String), 298
Context.MODE_WORLD_READABLE, 295
controller objects, 37
conventions
class naming, 7
extra naming, 99
package naming, 4
variable naming, 21, 34
create() (AlertDialog.Builder), 219
createChooser() (Intent), 282
Creative Commons, 331
Cursor, 267, 269
CursorWrapper, 267
/data/data
directory, 257
database schema, 258
databases, SQLite, 257-272
Date, 226
DatePicker, 221
debugging
(see also Android Lint)
build errors, 85
608
crash, 76
crash on unconnected device, 77
database issues, 261
misbehaviors, 77
online help for, 86
R, 85
running app with debugger, 80
stopping debugger, 81
when working with teams, 584
DEFAULT (Intent), 397
delayed execution, 473
density-independent pixel, 156
dependencies, adding, 129-132
dependency injector, 192
detach() (FragmentTransaction), 211
Dev Tools, 73
developer documentation, 117, 118
devices
configuration changes and, 64
hardware, 26
virtual, 26, 307
Devices view, 47
Dialog, 215
DialogFragment, 217
onCreateDialog(), 219
show(), 220
dialogs, 215-224
diamond notation, 170
dip (density-independent pixel), 156
documentation, 117, 118
doInBackground() (AsyncTask), 411
dp (density-independent pixel), 156
draw() (View), 536
draw9patch tool, 379
drawables, 369
9-patch images, 376
for uniform buttons, 369
layer list, 374
referencing, 52
shape, 371
state list, 372
drawing
Canvas, 536
in onDraw(), 536
Paint, 536