488
CHAPTER 9: Super Jumper: A 2D OpenGL ES Game
For your convenience Apress has placed some of the front matter mate...
40 downloads
1064 Views
19MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
488
CHAPTER 9: Super Jumper: A 2D OpenGL ES Game
For your convenience Apress has placed some of the front matter material after the index. Please use the Bookmarks and Contents at a Glance links to access them.
Contents at a Glance Contents .............................................................................................................. v About the Author...............................................................................................xvi About the Technical Reviewer .........................................................................xvii Acknowledgments ..........................................................................................xviii Preface .............................................................................................................xix Part I: Core Concept ............................................................................................ 1 ■Chapter 1: The Big Picture............................................................................... 3 ■Chapter 2: How to Get Started ......................................................................... 7 ■Chapter 3: Your First Android Project ........................................................... 23 ■Chapter 4: Examining Your First Project ....................................................... 31 ■Chapter 5: A Bit About Eclipse....................................................................... 37 ■Chapter 6: Enhancing Your First Project ....................................................... 47 Part II: Activities ............................................................................................... 49 ■Chapter 7: Rewriting Your First Project ........................................................ 51 ■Chapter 8: Using XML-Based Layouts ........................................................... 55 ■Chapter 9: Employing Basic Widgets............................................................. 61 ■Chapter 10: Working with Containers ........................................................... 73 ■Chapter 11: The Input Method Framework .................................................... 93 ■Chapter 12: Using Selection Widgets .......................................................... 103 ■Chapter 13: Getting Fancy with Lists .......................................................... 119 ■Chapter 14: Still More Widgets and Containers........................................... 135 ■Chapter 15: Embedding the WebKit Browser .............................................. 159 ■Chapter 16: Applying Menus ....................................................................... 167 ■Chapter 17: Showing Pop-Up Messages...................................................... 179 ■Chapter 18: Handling Activity Lifecycle Events ........................................... 183 ■Chapter 19: Handling Rotation .................................................................... 187 ■Chapter 20: Dealing with Threads ............................................................... 203 iii
■ CONTENTS AT A GLANCE
■Chapter 21: Creating Intent Filters .............................................................. 221 ■Chapter 22: Launching Activities and Subactivities ................................... 227 ■Chapter 23: Working with Resources .......................................................... 235 ■Chapter 24: Defining and Using Styles ........................................................ 251 ■Chapter 25: Handling Multiple Screen Sizes ............................................... 257 Part III: Honeycomb and Tablets..................................................................... 279 ■Chapter 26: ntroducing the Honeycomb UI .................................................. 281 ■Chapter 27: Using the Action Bar ................................................................ 289 ■Chapter 28: Fragments ................................................................................ 297 ■Chapter 29: Handling Platform Changes ..................................................... 313 ■Chapter 30: Accessing Files ........................................................................ 323 Part IV: Data Stores, Network Services, and APIs .......................................... 337 ■Chapter 31: Using Preferences .................................................................... 339 ■Chapter 32: Managing and Accessing Local Databases .............................. 357 ■Chapter 33: Leveraging Java Libraries ....................................................... 369 ■Chapter 34: Communicating via the Internet .............................................. 377 Part V: Services .............................................................................................. 393 ■Chapter 35: Services: The Theory ................................................................ 395 ■Chapter 36: Basic Service Patterns ............................................................. 403 ■Chapter 37: Alerting Users via Notifications ............................................... 423 Part VI: Other Android Capabilities ................................................................. 435 ■Chapter 38: Requesting and Requiring Permissions ................................... 437 ■Chapter 39: Accessing Location-Based Services ........................................ 443 ■Chapter 40: Mapping with MapView and MapActivity ................................ 449 ■Chapter 41: Handling Telephone Calls ......................................................... 463 ■Chapter 42: Fonts ........................................................................................ 467 ■Chapter 43: More Development Tools ......................................................... 473 Part VII: Alternative Application Environments .............................................. 489 ■Chapter 44: The Role of Alternative Environments ...................................... 491 ■Chapter 45: HTML5 ...................................................................................... 495 ■Chapter 46: PhoneGap ................................................................................. 507 ■Chapter 47: Other Alternative Environments ............................................... 523 Part VIII: The Ever-Evolving Android .............................................................. 529 ■Chapter 48: Dealing with Devices................................................................ 531 ■Chapter 49: Where Do We Go From Here? ................................................... 537 Index ............................................................................................................... 541 iv
Part
Core Concept
I
Chapter
1
The Big Picture Android is everywhere. Phones. Tablets. TVs and set-top boxes powered by Google TV. Soon, Android will be in cars and all sort of other places as well. However, the general theme of Android devices will be smaller screens and/or no hardware keyboard. And, by the numbers, Android will probably be associated mostly with smartphones for the foreseeable future. For developers, this has both benefits and drawbacks, as described next. This chapter also describes the main components in an Android application and the Android features that you can exploit when developing your applications.
Benefits and Drawbacks of Smartphone Programming On the plus side, Android-style smartphones are sexy. Offering Internet services over mobile devices dates back to the mid-1990s and the Handheld Device Markup Language (HDML). However, only in recent years have phones capable of Internet access taken off. Now, thanks to trends like text messaging and products like Apple’s iPhone, phones that can serve as Internet-access devices are rapidly gaining popularity. So, working on Android applications gives you experience with an interesting technology (Android) in a fast-moving market segment (Internet-enabled phones), which is always a good thing. The problem comes when you actually have to program the darn things. Anyone with experience in programming for PDAs or phones has felt the pain of phones simply being small in all sorts of dimensions:
Screens are small (you will not get comments like, “Is that a 24-inch LCD in your pocket, or...?”).
Keyboards, if they exist, are small.
3
4
CHAPTER 1: The Big Picture
Pointing devices, if they exist, are annoying (as anyone who has lost their stylus will tell you) or inexact (large fingers and “multitouch” LCDs can sometimes be...problematic).
CPU speed and memory are limited compared to what’s available on desktops and servers.
Moreover, applications running on a phone have to deal with the fact that they’re on a phone. People with mobile phones tend to get very irritated when those phones do not work. Similarly, those same people will get irritated if your program “breaks” their phones by
Tying up the CPU such that calls can’t be received.
Not quietly fading into the background when a call comes in or needs to be placed, because the program doesn’t work properly with the rest of the phone’s operating system.
Crashing the phone’s operating system, such as by leaking memory like a sieve.
Hence, developing programs for a phone is a different experience than developing desktop applications, web sites, or back-end server processes. The tools look different, the frameworks behave differently, and you have more limitations on what you can do with your programs. What Android tries to do is meet you halfway:
You get a commonly used programming language (Java) with some commonly used libraries (e.g., some Apache Commons APIs), with support for tools you may be used to using (Eclipse).
You get a fairly rigid and uncommon framework in which your programs need to run so they can be “good citizens” on the phone and not interfere with other programs or the operation of the phone itself.
As you might expect, much of this book deals with that framework and how you write programs that work within its confines and take advantage of its capabilities.
What Androids Are Made Of When you write a desktop application, you are “master of your own domain.” You launch your main window and any child windows—like dialog boxes—that are needed. From your standpoint, you are your own world, leveraging features supported by the operating system, but largely ignorant of any other program that may be running on the computer at the same time. If you do interact with other programs, it is typically through an application programming interface (API), such as Java Database Connectivity (JDBC), or frameworks atop it, to communicate with MySQL or another database.
CHAPTER 1: The Big Picture
Android has similar concepts, but they are packaged differently and structured to make phones more crash-resistant:
Activities: The building block of the user interface is the activity. You can think of an activity as being the Android analogue for the window or dialog box in a desktop application or the page in a classic web application. Android is designed to support lots of cheap activities, so you can allow users to keep tapping to open new activities and tapping the Back button to back up, just like they do in a web browser.
Services: Activities are short-lived and can be shut down at any time. Services, on the other hand, are designed to keep running, if needed, independent of any activity. You might use a service to check for updates to an RSS feed or to play back music even if the controlling activity is no longer operating. You will also use services for scheduled tasks (“cron jobs”) and for exposing custom APIs to other applications on the device, though those are relatively advanced capabilities.
Content providers: Content providers provide a level of abstraction for any data stored on the device that is accessible by multiple applications. The Android development model encourages you to make your own data available to other applications, as well as your own applications. Building a content provider lets you do that, while maintaining complete control over how your data gets accessed.
Intents: Intents are system messages that run around the inside of the device and notify applications of various events, from hardware state changes (e.g., an SD card was inserted), to incoming data (e.g., a Short Message Service [SMS] message arrived), to application events (e.g., your activity was launched from the device’s main menu). Not only can you respond to an Intent, but you can create your own to launch other activities or to let you know when specific situations arise (e.g., raise such-and-so Intent when the user gets within 100 meters of this-and-such location).
Stuff at Your Disposal
Storage: You can package data files with your application for things that do not change, such as icons or help files. You also can carve out a small bit of space on the device itself, for databases or files containing user-entered or retrieved data needed by your application. And, if the user supplies bulk storage, like an SD card, you can read and write files on there as needed.
5
6
CHAPTER 1: The Big Picture
Network: Android devices generally are Internet-ready, through one communications medium or another. You can take advantage of the Internet access at any level you wish, from raw Java sockets all the way up to a built-in WebKit-based web browser widget you can embed in your application.
Multimedia: Android devices have the ability to play back and record audio and video. While the specifics may vary from device to device, you can query the device to learn its capabilities and then take advantage of the multimedia capabilities as you see fit, whether that is to play back music, take pictures with the camera, or use the microphone for audio note-taking.
Global Positioning System (GPS): Android devices frequently have access to location providers, such as GPS, that can tell your applications where the device is on the face of the Earth. In turn, you can display maps or otherwise take advantage of the location data, such as to track a device’s movements if the device has been stolen.
Phone services: Because Android devices are typically phones, your software can initiate calls, send and receive SMS messages, and do everything else you expect from a modern bit of telephony technology.
The Big Picture...of This Book Now that you have the Android big picture, here is what’s coming in the rest of this book:
The next two chapters are designed to get you going quickly with the Android environment, through a series of step-by-step, tutorial-style instructions for setting up the tools you need, creating your first project, and getting that first project running on the Android emulator.
The three chapters that follow explain a bit more about what just happened in Chapters 2 and 3. We examine the Android project that we created, talk a bit more about Eclipse, and discuss some things we could add to the project to help it run on more devices and enhance its capabilities.
The bulk of the book explores the various capabilities of the Android APIs—how to create components like activities, how to access the Internet and local databases, how to get your location and show it on a map, and so forth.
Chapter
2
How to Get Started Without further ado, let’s get you set up with the pieces and parts necessary to build an Android app. NOTE: The instructions presented here are accurate as of the time of this writing. However, the tools change rapidly, so these instructions may be out of date by the time you read this. Please refer to the Android Developers web site for current instructions, using this as a base guideline of what to expect.
Step 1: Set Up Java When you write Android applications, you typically write them in Java source code. That Java source code is then turned into the stuff that Android actually runs (Dalvik bytecode in an Android package [APK] file). Hence, the first thing you need to do is get set up with a Java development environment so that you are prepared to start writing Java classes.
Install the JDK You need to obtain and install the official Sun/Oracle Java SE SDK (JDK). You can obtain this from the Oracle Java web site for Windows and Linux, and presumably from Apple for Mac OS X. The plain JDK (sans any “bundles”) should suffice. Follow the instructions supplied by Oracle or Apple for installing it on your machine. At the time of this writing, Android supports Java 5 and Java 6, the latter being the now-current edition.
Alternative Java Compilers In principle, you are supposed to use the official Sun/Oracle Java SE Development Kit (JDK) In practice, it appears that OpenJDK also works, at least on Ubuntu. However, the
7
8
CHAPTER 2: How to Get Started
further removed you get from the official Sun/Oracle implementation, the less likely it is that it will work. For example, the GNU Compiler for Java (GCJ) may not work with Android.
Learn Java This book, like most books and documentation on Android, assumes that you have basic Java programming experience. If you lack this, you really should consider spending a bit of time on Java fundamentals, before you dive into Android. Otherwise, you may find the experience to be frustrating. If you are in need of a crash course in Java to get involved in Android development, here are the concepts you need to learn, presented in no particular order:
Language fundamentals (flow control, etc.)
Classes and objects
Methods and data members
Public, private, and protected
Static and instance scope
Exceptions
Threads and concurrency control
Collections
Generics
File I/O
Reflection
Interfaces
One of the easiest ways of acquiring this knowledge is to read Learn Java for Android Development by Jeff Friesen (Apress, 2010).
Step 2: Install the Android SDK The Android SDK gives you all the tools you need to create and test Android applications. It comes in two parts: the base tools, and version-specific SDKs and related add-ons.
Install the Base Tools You can find the Android developer tools on the Android Developers web site. Download the ZIP file that is appropriate for your platform and unzip it in a logical location on your
CHAPTER 2: How to Get Started
machine—no specific path is required. Windows users also have the option of running a self-installing EXE file.
Install the SDKs and Add-ons Inside the tools/ directory of your Android SDK installation from the previous step, you will see an android batch file or shell script. If you run that, you will be presented with the Android SDK and AVD Manager, shown in Figure 2–1.
Figure 2–1. Android SDK and AVD Manager
At this point, you have some of the build tools, but you lack the Java files necessary to compile an Android application. You also lack a few additional build tools, and the files necessary to run an Android emulator. To address this, click the Available packages option on the left to open the screen shown in Figure 2–2.
9
10
CHAPTER 2: How to Get Started
Figure 2–2. Android SDK and AVD Manager available packages
Open the Android Repository branch of the tree. After a short pause, you will see a screen similar to Figure 2–3.
Figure 2–3. Android SDK and AVD Manager available Android packages
Check the boxes for the following items:
“SDK Platform” for all Android SDK releases you want to test against
“Documentation for Android SDK” for the latest Android SDK release
“Samples for SDK” for the latest Android SDK release, and perhaps for older releases if you wish
Then, open the Third party Add-ons branch of the tree. After a short pause, you will see a screen similar to Figure 2–4.
CHAPTER 2: How to Get Started
Figure 2–4. Android SDK and AVD Manager available third-party add-ons
Click the “Google Inc. add-ons” branch to open it, as shown in Figure 2–5.
Figure 2–5. Android SDK and AVD Manager available Google add-ons
Most likely, you will want to check the boxes for the “Google APIs by Google Inc.” items that match up with the SDK versions you selected in the Android Repository branch. The Google APIs include support for Google Maps, both from your code and in the Android emulator. After you have checked all the items you want to download, click the Install Selected button, which brings up a license confirmation dialog box, shown in Figure 2–6.
11
12
CHAPTER 2: How to Get Started
Figure 2–6. Android SDK and AVD Manger license agreement screen
Review and accept the licenses if you agree with the terms, and then click the Install button. At this point, this is a fine time to go get lunch or dinner. Unless you have a substantial Internet connection, downloading all of this data and unpacking it will take a fair bit of time. When the download is complete, you can close the SDK and AVD Manager if you wish, though you will use it to set up the emulator in Step 5 of this chapter.
Step 3: Install the ADT for Eclipse If you will not be using Eclipse for your Android development, you can skip to the next section. If you will be using Eclipse but have not yet installed it, you will need to do that first. Eclipse can be downloaded from the Eclipse web site, www.eclipse.org/. The Eclipse IDE for Java Developers package will work fine. Next, you need to install the Android Developer Tools (ADT) plug-in. To do this, open Eclipse and choose Help ➤ Install New Software. Then, in the Install dialog box, click the Add button to add a new source of plug-ins. Give it a name (e.g., Android) and supply the following URL: https://dl-ssl.google.com/android/eclipse/. That should trigger Eclipse to download the roster of plug-ins available from that site (see Figure 2–7).
CHAPTER 2: How to Get Started
Figure 2–7. Eclipse ADT plug-in installation
Check the Developer Tools check box and click the Next button. Follow the rest of the wizard steps to review the tools to be downloaded and review and accept their respective license agreements. When the Finish button is enabled, click it, and Eclipse will download and install the plug-ins. When it’s done, Eclipse will ask to restart; let it do so. Then, you need to show ADT where to locate your Android SDK installation from the preceding section. To do this, choose Window ➤ Preferences from the Eclipse main menu (or the equivalent Preferences option for Mac OS X). Click the Android entry in the list pane of the Preferences dialog box, as shown in Figure 2–8.
13
14
CHAPTER 2: How to Get Started
Figure 2–8. Eclipse ADT configuration
Then, click the Browse button to find the directory where you installed the SDK. After choosing it, click Apply in the Preferences dialog box, and you should see the Android SDK versions you installed previously. Then, click OK, and the ADT will be ready for use.
Step 4: Install Apache Ant If you will be doing all of your development from Eclipse, you can skip to the next section. If you wish to develop using command-line build tools, you need to install Apache Ant. You may have this installed already from previous Java development work, as it is fairly common in Java projects. However, you need Ant version 1.8.1 or later, so check your current copy (e.g., ant -version). If you do not have Ant or do not have the correct version, you can obtain it from the Apache Ant web site, at http://ant.apache.org/. Full installation instructions are available in the Ant manual, but the basic steps are as follows: 1.
Unpack the ZIP archive in a logical place on your machine.
2.
Add a JAVA_HOME environment variable, pointing to where your JDK is installed, if you do not have one already.
3.
Add an ANT_HOME environment variable, pointing to the directory where you unpacked Ant in step 1.
CHAPTER 2: How to Get Started
4.
Add $JAVA_HOME/bin and $ANT_HOME/bin to your PATH.
5.
Run ant -version to confirm that Ant is installed properly.
Step 5: Set Up the Emulator The Android tools include an emulator, a piece of software that pretends to be an Android device. This is very useful for development—it not only enables you to get started on your Android development without a device, but also enables you to test device configurations for devices that you do not own. The Android emulator can emulate one or several Android devices. Each configuration you want is stored in an Android Virtual Device (AVD). The Android SDK and AVD Manager, which you used to download the SDK components earlier in this chapter, is where you create these AVDs. If you do not have the SDK and AVD Manager running, you can run it via the android command from your SDK’s tools/ directory, or via Window ➤ SDK and AVD Manager from Eclipse. It opens with a screen listing the AVDs you have available; initially, the list will be empty, as shown in Figure 2–9.
Figure 2–9. Android SDK and AVD Manager Android Virtual Devices list
Click the New button to create a new AVD file. This opens the dialog box shown in Figure 2–10, where you can configure how this AVD should look work.
15
16
CHAPTER 2: How to Get Started
Figure 2–10. Adding a new AVD
You need to provide the following:
A name for the AVD: Since the name goes into files on your development machine, you are limited by the file name conventions for your operating system (e.g., no backslashes on Windows).
The Android version (target) you want the emulator to run: Choose one of the SDKs you installed via the Target drop-down list. Note that in addition to “pure” Android environments, you will have options based on the third-party add-ons you selected. For example, you probably have some options for setting up AVDs containing the Google APIs, and you will need such an AVD for testing an application that uses Google Maps.
Details about the SD card the emulator should emulate: Since Android devices invariably have some form of external storage, you probably want to set up an SD card, by supplying a size in the associated field. However, since a file will be created on your development machine of whatever size you specify for the card, you probably do not want to create a 2GB emulated SD card. 32MB is a nice starting point, though you can go larger if needed.
CHAPTER 2: How to Get Started
The “skin” or resolution the emulator should run in: The skin options you have available depend upon what target you chose. The skins let you choose a typical Android screen resolution (e.g., WVGA800 for 800480). You can also manually specify a resolution when you want to test a nonstandard configuration.
You can skip the Hardware section of the dialog box for now, as changing those settings is usually only required for advanced configurations. The resulting dialog box might look something like Figure 2–11.
Figure 2–11. Adding a new AVD (continued)
Click the Create AVD button, and your AVD stub will be created. To start the emulator, select it in the Android Virtual Devices list and click Start. You can skip the launch options for now and just click Launch. The first time you launch a new AVD, it will take a long time to start up. The second and subsequent times you start the AVD, it will come up a bit faster, and usually you need to start it only once per day (e.g., when you start development). You do not need to stop and restart the emulator every time you want to test your application, in most cases. The emulator will go through a few startup phases, the first of which displays a plain-text ANDROID label, as shown in Figure 2–12.
17
18
CHAPTER 2: How to Get Started
Figure 2–12. Android emulator, initial startup segment
The second phase displays a graphical Android logo, as shown in Figure 2–13.
CHAPTER 2: How to Get Started
Figure 2–13. Android emulator, secondary startup segment
Finally, the emulator reaches the home screen (the first time you run the AVD; see Figure 2–14) or the keyguard (see Figure 2–15).
19
20
CHAPTER 2: How to Get Started
Figure 2–14. Android home screen
If you get the keyguard, press the Menu button or slide the green lock on the screen to the right, to get to the emulator’s home screen.
Figure 2–15. Android keyguard
CHAPTER 2: How to Get Started
Step 6: Set Up the Device With an emulator set up, you do not need an Android device to get started in Android application development. Having one is a good idea before you try to ship an application (e.g., upload it to the Android Market). But perhaps you already have a device—maybe that is what is spurring your interest in developing for Android. The first step to make your device ready for use with development is to go into the Settings application on the device. From there, choose Applications, then Development. That should give you a set of check boxes for choosing development-related options, similar to what’s shown in Figure 2–16.
Figure 2–16. Android device development settings
Generally, you will want to enable USB debugging so that you can use your device with the Android build tools. You can leave the other settings alone for now if you wish, though you may find the Stay awake option to be handy, as it saves you from having to unlock your phone repeatedly while it is plugged into USB. Next, you need to set up your development machine to talk to your device. That process varies by the operating system of your development machine, as covered in the following sections.
Windows When you first plug in your Android device, Windows attempts to find a driver for it. It is possible that, by virtue of other software you have installed, the driver is ready for use. If Windows finds a driver, you are probably ready to go. If Windows doesn’t find the driver, here are some options for getting one:
Windows Update: Some versions of Windows (e.g., Vista) prompt you to search Windows Update for drivers. This is certainly worth a shot, though not every device will have supplied its driver to Microsoft.
21
22
CHAPTER 2: How to Get Started
Standard Android driver: In your Android SDK installation, you will find a google-usb_driver directory, containing a generic Windows driver for Android devices. You can try pointing the driver wizard at this directory to see if it thinks this driver is suitable for your device.
Manufacturer-supplied driver: If you still do not have a driver, search the CD that came with the device (if any) or search the web site of the device manufacturer. Motorola, for example, has drivers available for all of its devices in one spot for download.
Mac OS X and Linux Odds are decent that simply plugging in your device will “just work.” You can see if Android recognizes your device by running adb devices in a shell (e.g., OS X Terminal), where adb is in your platform-tools/ directory of your SDK. If you get output similar to the following, Android detected your device: List of devices attached HT9CPP809576 device
If you are running Ubuntu (or perhaps another Linux variant) and this command did not work, you may need to add some udev rules. For example, here is a 51-android.rules file that will handle the devices from a handful of manufacturers: SUBSYSTEM=="usb", SYSFS{idVendor}=="0bb4", MODE="0666" SUBSYSTEM=="usb", SYSFS{idVendor}=="22b8", MODE="0666" SUBSYSTEM=="usb", SYSFS{idVendor}=="18d1", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="0c01", MODE="0666", OWNER="[me]" SUBSYSTEM=="usb", SYSFS{idVendor}=="19d2", SYSFS{idProduct}=="1354", MODE="0666" SUBSYSTEM=="usb", SYSFS{idVendor}=="04e8", SYSFS{idProduct}=="681c", MODE="0666"
Drop that in your /etc/udev/rules.d directory on Ubuntu, and then either reboot the computer or otherwise reload the udev rules (e.g., sudo service udev reload). Then, unplug the device, plug it in again, and see if it is detected.
Chapter
3
Your First Android Project Now that you have the Android SDK, it is time to make your first Android project. The good news is that this requires zero lines of code—Android’s tools create a “Hello, world!” application for you as part of creating a new project. All you need to do is build it, install it, and watch it open on your emulator or device.
Step 1: Create the New Project Android’s tools can create a complete skeleton project for you, with everything you need for a complete (albeit very trivial) Android application. The process differs depending on whether you are using Eclipse or the command line.
Eclipse From the Eclipse main menu, choose File ➤ New ➤ Project to open the New Project dialog box, which gives you a list of project type wizards to choose from. Expand the Android option and click Android Project, as shown in Figure 3–1.
23
24
CHAPTER 3: Your First Android Project
Figure 3–1. Selecting a wizard in the Eclipse New Project dialog box
Click Next to advance to the first page of the New Android Project wizard, shown in Figure 3–2.
CHAPTER 3: Your First Android Project
Figure 3–2. Eclipse New Android Project wizard, ready to fill in
Fill in the following and leave the default settings otherwise (the completed example for this project is shown in Figure 3–3):
Project name: The name of the project (e.g., Now)
Build Target: The Android SDK you wish to compile against (e.g., Google APIs for Android 2.3.3)
Application name: The display name of your application, which will be used for the caption under your icon in the launcher (e.g., Now)
Package name: The name of the Java package in which this project belongs (e.g., com.commonsware.android.skeleton)
Create Activity: The name of the initial activity to create (e.g., Now)
25
26
CHAPTER 3: Your First Android Project
Figure 3–3. Eclipse New Android Project wizard, completed
At this point, click Finish to create your Eclipse project.
Command Line Here is a sample command that creates an Android project from the command line: android create project --target "Google Inc.:Google APIs:7" --path Skeleton/Now --activity Now --package com.commonsware.android.skeleton
This creates an application skeleton for you, complete with everything you need to build your first Android application: Java source code, build instructions, and so forth.
CHAPTER 3: Your First Android Project
However, you’ll probably need to customize this somewhat. Here are what those command-line switches mean:
--target: Indicates which version of Android you are targeting in terms of your build process. You need to supply the ID of a target that is installed on your development machine, one you downloaded via the Android SDK and AVD Manager. You can find out which targets are available via the android list targets command. Typically, your build process will target the newest version of Android that you have available.
--path: Indicates where you want the project files to be generated. Android will create a directory if the one you name does not exist. For example, in the preceding command, a Skeleton/Now/ directory will be created (or used if it exists) under the current working directory, and the project files will be stored there.
--activity: Indicates the Java class name of your first activity for this project. Do not include a package name, and make sure the name meets Java class-naming conventions.
--package: Indicates the Java package in which your first activity will be located. This package name also uniquely identifies your project on any device on which you install it, and it must be unique on the Android Market if you plan on distributing your application there. Hence, typically, you should construct your package based on a domain name you own (e.g., com.commonsware.android.skeleton), to reduce the odds of an accidental package name collision with somebody else.
For your development machine, you need to pick a suitable target, and you may wish to change the path. You can ignore the activity and package for now.
Step 2: Build, Install, and Run the Application in Your Emulator or Device Having a project is nice and all, but it would be even better if you could build and run it, whether on the Android emulator or on your Android device. Once again, the process differs somewhat depending on whether you are using Eclipse or the command line.
Eclipse With your project selected on the Package Explorer panel of Eclipse, click the green play button in the Eclipse toolbar to run your project. The first time you do this, you have to go through a few steps to set up a run configuration, so Eclipse knows what you want to do. First, in the Run As dialog box, choose Android Application, as shown in Figure 3–4.
27
28
CHAPTER 3: Your First Android Project
Figure 3–4. Choosing to run as an Android application in the Eclipse Run As dialog box
Click OK. If you have more than one emulator AVD or device available, you will then get an option to choose which you wish to run the application on. Otherwise, if you do not have a device plugged in, the emulator will start up with the AVD you created earlier. Then, Eclipse will install the application on your device or emulator and start it.
Command Line For developers who are not using Eclipse, in your terminal, change into the Skeleton/Now directory, then run the following command: ant clean install
The Ant-based build should emit a list of steps involved in the installation process, which looks like this: Buildfile: /home/some-balding-guy/projects/Skeleton/Now/build.xml [setup] Android SDK Tools Revision 10 [setup] Project Target: Android 1.6 [setup] API level: 4 [setup] [setup] -----------------[setup] Resolving library dependencies: [setup] No library dependencies. [setup] [setup] -----------------[setup] [setup] WARNING: No minSdkVersion value set. Application will install on all Android versions. [setup] [setup] Importing rules file: tools/ant/main_rules.xml clean:
CHAPTER 3: Your First Android Project
[delete] Deleting directory /home/some-balding-guy/projects/Skeleton/Now/bin [delete] Deleting directory /home/some-balding-guy/projects/Skeleton/Now/gen -debug-obfuscation-check: -set-debug-mode: -compile-tested-if-test: -pre-build: -dirs: [echo] [mkdir] [mkdir] [mkdir]
Creating output directories if needed... Created dir: /home/some-balding-guy/projects/Skeleton/Now/bin Created dir: /home/some-balding-guy/projects/Skeleton/Now/gen Created dir: /home/some-balding-guy/projects/Skeleton/Now/bin/classes
-aidl: [echo] Compiling aidl files into Java classes... -renderscript: [echo] Compiling RenderScript files into Java classes and RenderScript bytecode... -resource-src: [echo] Generating R.java / Manifest.java from the resources... -pre-compile: compile: [javac] /opt/android-sdk-linux/tools/ant/main_rules.xml:384: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds [javac] Compiling 2 source files to /home/some-baldingguy/projects/Skeleton/Now/bin/classes -post-compile: -obfuscate: -dex: [echo] Converting compiled files and external libraries into /home/some-baldingguy/projects/Skeleton/Now/bin/classes.dex... -package-resources: [echo] Packaging resources [aapt] Creating full resource package... -package-debug-sign: [apkbuilder] Creating Now-debug-unaligned.apk and signing it with a debug key... debug: [echo] Running zip align on final apk... [echo] Debug Package: /home/some-balding-guy/projects/Skeleton/Now/bin/Nowdebug.apk install:
29
30
CHAPTER 3: Your First Android Project
[echo] Installing /home/some-balding-guy/projects/Skeleton/Now/bin/Now-debug.apk onto default emulator or device... [exec] 98 KB/s (4626 bytes in 0.045s) [exec] pkg: /data/local/tmp/Now-debug.apk [exec] Success BUILD SUCCESSFUL Total time: 10 seconds
Note the BUILD SUCCESSFUL at the bottom—that is how you know the application compiled successfully. When you have a clean build, in your emulator or device, open the application launcher, shown in Figure 3–5, which typically is found at the bottom of the home screen.
Figure 3–5. Android emulator application launcher
Notice there is an icon for your Now application. Click it to open it and see your first activity in action. To leave the application and return to the launcher, press the Back button, which is located to the right of the Menu button and looks like an arrow pointing to the left.
Chapter
4
Examining Your First Project The previous chapter stepped you through creating a stub project. This chapter describes what is inside of this project, so you understand what Android gives you at the outset and what the roles are for the various directories and files.
Project Structure The Android build system is organized around a specific directory tree structure for your Android project, much like any other Java project. The specifics, though, are fairly unique to Android—the Android build tools do a few extra things to prepare the actual application that will run on the device or emulator. Here’s a quick primer on the project structure, to help you make sense of it all, particularly for the sample code referenced in this book.
Root Contents When you create a new Android project (e.g., via android create project), you get several items in the project’s root directory, including the following:
AndroidManifest.xml: An XML file that describes the application being built and what components (activities, services, etc.) are being supplied by that application
bin/: The directory that holds the application once it is compiled
libs/: The directory that holds any third-party JARs your application requires
res/: The directory that holds resources, such as icons, GUI layouts, and the like, that are packaged with the compiled Java in the application
src/: The directory that holds the Java source code for the application
31 3
32
CHAPTER 4: Examining Your First Project
In addition to the preceding file and directories, you may find any of the following in Android projects:
assets/: The directory that holds other static files that you want packaged with the application for deployment onto the device
gen/: The directory in which Android’s build tools place source code that they generate
build.xml and *.properties: Files that are used as part of the Antbased command-line build process, if you are not using Eclipse
proguard.cfg: A file that is used for integration with ProGuard to obfuscate your Android code
The Sweat Off Your Brow When you create an Android project (e.g., via android create project), you supply the fully qualified class name of the main activity for the application (e.g., com.commonsware.android.SomeDemo). You will then find that your project’s src/ tree already has the namespace directory tree in place, plus a stub Activity subclass representing your main activity (e.g., src/com/commonsware/android/SomeDemo.java). You are welcome to modify this file and add others to the src/ tree as needed to implement your application. The first time you compile the project (e.g., via ant), out in the main activity’s namespace directory, the Android build chain will create R.java. This contains a number of constants tied to the various resources you placed in the res/ directory tree. You should not modify R.java yourself, but instead let the Android tools handle it for you. You will see throughout this book that many of the examples reference things in R.java (e.g., referring to a layout’s identifier via R.layout.main).
And Now, the Rest of the Story The res/ directory tree in your project holds resources—static files that are packaged along with your application, either in their original form or, occasionally, in a preprocessed form. Following are some of the subdirectories you will find or create under res/:
res/drawable/: For images (PNG, JPEG, etc.)
res/layout/: For XML-based UI layout specifications
res/menu/: For XML-based menu specifications
res/raw/: For general-purpose files (e.g., an audio clip or a CSV file of account information)
res/values/: For strings, dimensions, and the like
res/xml/: For other general-purpose XML files you wish to ship
CHAPTER 4: Examining Your First Project
Some of the directory names may have suffixes, like res/drawable-hdpi/. This indicates that the directory of resources should be used only in certain circumstances—in this case, the drawable resources should be used only on devices with high-density screens. We will cover all of these resources, and more, in later chapters of this book. In your initial project, you will find the following:
res/drawable-hdpi/icon.png, res/drawable-ldpi/icon.png, and res/drawable-mdpi/icon.png: Three renditions of a placeholder icon for your application for high-, low-, and medium-density screens, respectively
res/layout/main.xml: An XML file that describes the very simple layout of your user interface
res/values/strings.xml: An XML file that contains externalized strings, notably the placeholder name of your application
What You Get Out of It When you compile your project (via ant or the IDE), the results go into the bin/ directory under your project root, as follows:
bin/classes/: Holds the compiled Java classes
bin/classes.dex: Holds the executable created from those compiled Java classes
bin/yourapp.ap_: Holds your application’s resources, packaged as a ZIP file (where yourapp is the name of your application)
bin/yourapp-*.apk: The actual Android application (where * varies)
The .apk file is a ZIP archive containing the .dex file, the compiled edition of your resources (resources.arsc), any uncompiled resources (such as what you put in res/raw/), and the AndroidManifest.xml file. If you build a debug version of the application (which is the default), you will have yourapp-debug.apk and yourapp-debugaligned.apk as two versions of your APK. The latter has been optimized with the zipalign utility to make it run faster.
Inside Your Manifest The foundation for any Android application is the manifest file, AndroidManifest.xml, in the root of your project. This is where you declare what is inside your application—the activities, the services, and so on. You also indicate how these pieces attach themselves to the overall Android system; for example, you indicate which activity (or activities) should appear on the device’s main menu (a.k.a., the launcher).
33
34
CHAPTER 4: Examining Your First Project
When you create your application, a starter manifest is generated for you automatically. For a simple application, offering a single activity and nothing else, the autogenerated manifest will probably work out fine, or perhaps require a few minor modifications. On the other end of the spectrum, the manifest file for the Android API demo suite is over 1,000 lines long. Your production Android applications will probably fall somewhere in the middle.
In the Beginning, There Was the Root, and It Was Good The root of all manifest files is, not surprisingly, a manifest element: ...
Note the namespace declaration. Curiously, the generated manifests apply it only on the attributes, not the elements (e.g., manifest, not android:manifest). This pattern works, so, unless Android changes, you should stick with it. The biggest piece of information you need to supply on the manifest element is the package attribute (also curiously not namespaced). Here, you can provide the name of the Java package that will be considered the “base” of your application. Then, everywhere else in the manifest file that needs a class name, you can just substitute a leading dot as shorthand for the package. For example, if you needed to refer to com.commonsware.android.search.Snicklefritz in the preceding manifest, you could just use .Snicklefritz, since com.commonsware.android.search is defined as the application’s package. As noted in the previous chapter, your package also is a unique identifier for your application. A device can have only one application installed with a given package, and the Android Market will list only one project with a given package. Your manifest also specifies android:versionName and android:versionCode attributes. These represent the versions of your application. The android:versionName value is what the user will see in the Applications list in their Settings application. Also, the version name is used by the Android Market listing, if you are distributing your application that way. The version name can be any string value you want. The android:versionCode, on the other hand, must be an integer, and newer versions must have higher version codes than do older versions. Android and the Android Market will compare the version code of a new APK to the version code of an installed application to determine if the new APK is indeed an update. The typical approach is to start the version code at 1 and increment it with each production release of your application, though you can choose another convention if you wish.
CHAPTER 4: Examining Your First Project
An Application for Your Application In your initial project’s manifest, the only child of the element is an element. The children of the element represent the core of the manifest file. One attribute of the element that you may need in select circumstances is the android:debuggable attribute. This needs to be set to true if you are installing the application on an actual device, you are using Eclipse (or another debugger), and your device precludes debugging without this flag. For example, the Google/HTC Nexus One requires android:debuggable = "true", according to some reports. By default, when you create a new Android project, you get a single element inside the element:
This element supplies android:name for the class implementing the activity, android:label for the display name of the activity, and (frequently) an child element describing under what conditions this activity will be displayed. The stock element sets up your activity to appear in the launcher, so users can choose to run it. As you’ll see later in this book, you can have several activities in one project, if you so choose.
35
Chapter
5
A Bit About Eclipse Eclipse is an extremely popular integrated development environment (IDE), particularly for Java development. It is also designed to be extensible via an add-in system. To top it off, Eclipse is open source. That combination made it an ideal choice of IDE for the core Android developer team. Specifically, to go alongside the Android SDK, Google has published some add-ins for the Eclipse environment. Primary among these is the Android Developer Tools (ADT) add-in, which gives the core of Eclipse awareness of Android.
What the ADT Gives You The ADT add-in, in essence, takes regular Eclipse operations and extends them to work with Android projects. For example, with Eclipse, you get the following features (among others):
New project wizards to create regular Android projects, Android test projects, and so forth
The ability to run an Android project just like you might run a regular Java application—via the green Run button in the toolbar—despite the fact that this really involves pushing the Android application over to an emulator or device, possibly even starting up the emulator if it is not running
Tooltip support for Android classes and methods
In addition, the latest version of the ADT provides you with preliminary support for dragand-drop GUI editing. While this book will focus on the XML files that Eclipse generates, Eclipse now lets you assemble those XML files by dragging GUI components around on the screen, adjusting properties as you go. Drag-and-drop GUI editing is fairly new, so there may be a few rough edges for a while as the community and Google identify the problems and limitations with the current implementation.
37
38
CHAPTER 5: A Bit About Eclipse
Coping with Eclipse Eclipse is a powerful tool. Like many powerful tools, Eclipse is sometimes confounding. Determining how to solve some specific development problem can be a challenge, exacerbated by the newness of Android itself. This section offers some tips for handling some common issues in using Eclipse with Android.
How to Import a Non-Eclipse Project Not all Android projects ship with Eclipse project files, such as the sample projects associated with this book. However, you can easily add them to your Eclipse workspace, if you wish. Here’s how to do it! First, choose File ➤ New ➤ Project from the Eclipse main menu, as shown in Figure 5–1.
Figure 5–1. File menu in Eclipse
CHAPTER 5: A Bit About Eclipse
Then, choose Android ➤ Android Project from the tree of available project types, as shown in Figure 5–2, and click Next.
Figure 5–2. New Project wizard in Eclipse
NOTE: If you do not see this option, you have not installed Android Developer Tools. Then, on the first page of the New Android Project wizard, choose the “Create project from existing source” radio button, click the Browse button, and open the directory containing your project’s AndroidManifest.xml file. This will populate most of the rest of the wizard page, though you may need to also specify a build target from the table, as shown in Figure 5–3.
39
40
CHAPTER 5: A Bit About Eclipse
Figure 5–3. New Android Project wizard in Eclipse
Then, click Finish. This will return you to Eclipse, with the imported project in your workspace, as shown in Figure 5–4.
Figure 5–4. Android project tree in Eclipse
CHAPTER 5: A Bit About Eclipse
Next, right-click the project name and choose Build Path ➤ Configure Build Path from the context menu, as shown in Figure 5–5.
Figure 5–5. Project context menu in Eclipse
This brings up the Java Build Path portion of the project Properties window, as shown in Figure 5–6.
Figure 5–6. Project Properties window in Eclipse
41
42
CHAPTER 5: A Bit About Eclipse
If the Android JAR is not checked (the Android 2.2 entry in Figure 5–6), check it, and then click OK to close the Properties window. At this point, your project should be ready for use.
How to Get to DDMS Many times, you will be told to take a look at something in DDMS, such as the LogCat tab to examine Java stack traces. In Eclipse, DDMS is a perspective. To open this perspective in your workspace, choose Window ➤ Open Perspective ➤ Other from the main menu, as shown in Figure 5–7.
Figure 5–7. Perspective menu in Eclipse
Then, in the list of perspectives, shown in Figure 5–8, choose DDMS.
CHAPTER 5: A Bit About Eclipse
Figure 5–8. Perspective roster in Eclipse
This will add the DDMS perspective to your workspace and open it in your Eclipse IDE. DDMS is covered in greater detail in a later chapter of this book.
How to Create an Emulator By default, your Eclipse environment has no Android emulators set up. You will need one before you can run your project successfully. To do this, first choose Window ➤ Android SDK and AVD Manager from the main menu, as shown in Figure 5–9.
43
44
CHAPTER 5: A Bit About Eclipse
Figure 5–9. Android SDK and AVD Manager menu option in Eclipse
That brings up the same window as you get by running android from the command line. You can now define an Android Virtual Device (AVD) by following the instructions given in Chapter 2, in the section “Step 5: Set Up the Emulator.”
How to Run a Project Given that you have an AVD defined, or that you have a device set up for debugging and connected to your development machine, you can run your project in the emulator. First, click the Run toolbar button, or choose Project ➤ Run from the main menu. This brings up the Run As dialog box the first time you run the project, as shown in Figure 5–10.
CHAPTER 5: A Bit About Eclipse
Figure 5–10. The Run As dialog box in Eclipse
Choose Android Application and click OK. If you have more than one AVD or device available, you will be presented with a window in which you choose the desired target environment. Then, the emulator will start up to run your application. Note that you will need to unlock the lock screen on the emulator (or device) if it is locked.
How Not to Run Your Project When you go to run your project, be sure that an XML file is not the active tab in the editor. Attempting to “run” this will result in a .out file being created in whatever directory the XML file lives in (e.g., res/layout/main.xml.out). To recover, simply delete the offending .out file and try running again, this time with a Java file as the active tab.
Alternative IDEs If you really like Eclipse and the ADT, you may want to consider MOTODEV Studio for Android. This is another set of add-ins for Eclipse, augmenting the ADT and offering a number of other Android-related development features, including the following (among many others):
More wizards for helping you create Android classes
Integrated SQLite browsing, so you can manipulate a SQLite database in your emulator right from your IDE
45
46
CHAPTER 5: A Bit About Eclipse
More validators to check for common bugs, and a library of code snippets to have fewer bugs at the outset
Assistance with translating your application to multiple languages
While MOTODEV Studio for Android is published by Motorola, you can use it to build applications for all Android devices, not only those manufactured by Motorola themselves. Other IDEs are slowly getting their equivalents of the ADT, albeit with minimal assistance from Google. For example, IntelliJ’s IDEA has a module for Android. It was originally commercial, but now it is part of the open source community edition of IDEA as of version 10. And, of course, you do not need to use an IDE at all. While this may sound sacrilegious to some, IDEs are not the only way to build applications. Much of what is accomplished via the ADT can be accomplished through command-line equivalents, meaning a shell and an editor is all you truly need. For example, the author of this book does not presently use an IDE and has no intention of adopting Eclipse any time soon.
IDEs and This Book You are welcome to use Eclipse as you work through this book. You are welcome to use another IDE if you wish. You are even welcome to skip the IDE outright and just use an editor. This book is focused on demonstrating Android capabilities and the APIs for exploiting those capabilities. It is not aimed at teaching the use of any one IDE. As such, the sample code shown should work in any IDE, particularly if you follow the instructions in this chapter for importing non-Eclipse projects into Eclipse.
Chapter
6
Enhancing Your First Project The AndroidManifest.xml file that Android generated for your first project gets the job done. However, for a production application, you may wish to consider adding a few attributes and elements, such as those described in this chapter.
Supporting Multiple Screen Sizes Android devices come with a wide range of screen sizes, from 2.8-inch tiny smartphones to 46-inch Google TVs. Android divides these into four categories, based on physical screen size and the distance at which they are usually viewed:
Small (under 3 inches)
Normal (3 inches to around 4.5 inches)
Large (4.5 inches to around 10 inches)
Extra-large (over 10 inches)
By default, your application will not support small screens, will support normal screens, and may support large and extra-large screens via some automated conversion code built into Android. To truly support all the screen sizes you want to target, you should consider adding a element. This enumerates the screen sizes you have explicit support for. For example, if you want to support small screens, you need to include the element. Similarly, if you are providing custom UI support for large or extra-large screens, you will want to have the element. So, while the default settings in the starting manifest file work, you should consider adding support for handling multiple screen sizes. Much more information about providing solid support for all screen sizes can be found in Chapter 25.
47
48
CHAPTER 6: Enhancing Your First Project
Specifying Versions As noted in the previous chapter, your manifest already contains some version information about your application’s version. However, you probably want to add to your AndroidManifest.xml file a element as a child of the element, to specify what versions of Android your application supports. By default, your application is assumed to support every Android version from 1.0 to the current 3.0 and onward to any version in the future. Most likely, that is not what you want. The most important attribute for your element is android:minSdkVersion. This indicates what is the oldest version of Android you are testing with your application. The value of the attribute is an integer representing the Android SDK version:
1: Android 1.0
2: Android 1.1
3: Android 1.5
4: Android 1.6
5: Android 2.0
6: Android 2.0.1
7: Android 2.1
8: Android 2.2
9: Android 2.3
10: Android 2.3.3
11: Android 3.0
So, if you are testing your application only on Android 2.1 and newer versions of Android, you would set the android:minSdkVersion attribute to 7. You may also wish to specify an android:targetSdkVersion attribute. This indicates what version of Android you are targeting as you are writing your code. If your application is run on a newer version of Android, Android may do some things to try to improve compatibility of your code with respect to changes made in the newer Android. So, for example, you might specify android:targetSdkVersion="10", indicating you are writing your application with Android 2.3.3 in mind; if your app someday is run on an Android 3.0 device, Android may take some extra steps to make sure your 2.3.3-centric code runs correctly on the 3.0 device. In particular, to get the new Honeycomb look and feel when running on an Android 3.0 (or higher) tablet, you need to specify a target SDK version of 11. This topic will be covered in more detail in Chapters 26 and 27.
Part
Activities
II
Chapter
7
Rewriting Your First Project The project you created in Chapter 3 is composed of just the default files generated by the Android build tools—you did not write any Java code yourself. In this chapter, you will modify that project to make it somewhat more interactive. Along the way, you will examine the basic Java code that comprises an Android activity. NOTE: The instructions in this chapter assume you followed the original instructions in Chapter 3 in terms of the names of packages and files. If you used different names, you will need to adjust the names in the following steps to match yours.
The Activity Your project’s src/ directory contains the standard Java-style tree of directories based on the Java package you used when you created the project (e.g., com.commonsware.android results in src/com/commonsware/android/). Inside the innermost directory you should find a pregenerated source file named Now.java, which is where your first activity will go. Open Now.java in your editor and paste in the following code (or, if you downloaded the source files from the Apress web site, you can just use the Skeleton/Now project directly): package com.commonsware.android.skeleton; import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.widget.Button; java.util.Date;
public class Now extends Activity implements View.OnClickListener {
51
52
CHAPTER 7: Rewriting Your First Project
Button btn; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); btn=new Button(this); btn.setOnClickListener(this); updateTime(); setContentView(btn); } public void onClick(View view) { updateTime(); } private void updateTime() { btn.setText(new Date().toString()); } }
Dissecting the Activity Let’s examine this Java code piece by piece, starting with the package declaration and imported classes: package com.commonsware.android.skeleton; import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.widget.Button; java.util.Date;
The package declaration needs to be the same as the one you used when creating the project. And, as with any other Java project, you need to import any classes you reference. Most of the Android-specific classes are in the android package. NOTE: Not every Java SE class is available to Android programs. Visit the Android class reference to see what is and is not available. Activities are public classes, inheriting from the android.app.Activity base class. In this case, the activity holds a button (btn): public class Now extends Activity implements View.OnClickListener { Button btn;
Since, for simplicity, we want to trap all button clicks just within the activity itself, we also have the activity class implement OnClickListener.
CHAPTER 7: Rewriting Your First Project
The onCreate() method is invoked when the activity is started. The first thing you should do is chain upward to the superclass, so the stock Android activity initialization can be done: @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); btn=new Button(this); btn.setOnClickListener(this); updateTime(); setContentView(btn); }
In our implementation, we then create the button instance (new Button(this)), tell it to send all button clicks to the activity instance itself (via setOnClickListener()), call a private updateTime() method, and then set the activity’s content view to be the button itself (via setContentView()). We will take a look at that magical Bundle icicle in a later chapter. For the moment, consider it an opaque handle that all activities receive upon creation. public void onClick(View view) { updateTime(); }
In Swing, a JButton click raises an ActionEvent, which is passed to the ActionListener configured for the button. In Android, a button click causes onClick() to be invoked in the OnClickListener instance configured for the button. The listener is provided the view that triggered the click (in this case, the button). All we do here is call that private updateTime() method: private void updateTime() { btn.setText(new Date().toString()); }
When we open the activity (onCreate()) or when the button is clicked (onClick()), we update the button’s label to be the current time via setText(), which functions much the same as the JButton equivalent.
Building and Running the Activity To build the activity, use your IDE’s built-in Android packaging tool, or run ant clean install in the base directory of your project (as described in Chapter 3). Then, run the activity. It should be launched for you automatically if you are using Eclipse; otherwise, find the activity in the home screen launcher. You should see an activity akin to what’s shown in Figure 7–1.
53
54
CHAPTER 7: Rewriting Your First Project
Figure 7–1. The Now demonstration activity
Clicking the button—in other words, clicking pretty much anywhere on the device’s screen—will update the time shown in the button’s label. Note that the label is centered horizontally and vertically, as those are the default styles applied to button captions. We can control that formatting, which will be covered in a later chapter. After you are finished gazing at the awesomeness of Advanced Push-Button Technology, you can click the Back button on the emulator to return to the launcher.
Chapter
8
Using XML-Based Layouts While it is technically possible to create and attach widgets to your activity purely through Java code, as we did in the preceding chapter, the more common approach is to use an XML-based layout file. Dynamic instantiation of widgets is reserved for more complicated scenarios, where the widgets are not known at compile time (e.g., populating a column of radio buttons based on data retrieved from the Internet). With that in mind, it’s time to break out the XML and learn how to lay out Android activity views that way.
What Is an XML-Based Layout? As the name suggests, an XML-based layout is a specification of widgets’ relationships to each other—and to containers—encoded in XML format. Specifically, Android considers XML-based layouts to be resources, and as such, layout files are stored in the res/layout directory inside your Android project. Each XML file contains a tree of elements specifying a layout of widgets and containers that make up one View. The attributes of the XML elements are properties, describing how a widget should look or how a container should behave. For example, if a Button element has an attribute value of android:textStyle = "bold", that means that the text appearing on the face of the button should be rendered in a boldface font style. Android’s SDK ships with a tool (aapt) that uses the layouts. This tool should be automatically invoked by your Android tool chain (e.g., Eclipse or Ant’s build.xml). Of particular importance to you as a developer is that aapt generates the R.java source file within your project’s gen/ directory, allowing you to access layouts and widgets within those layouts directly from your Java code, as will be demonstrated later in this chapter.
Why Use XML-Based Layouts? Most everything you do using XML layout files can be achieved through Java code. For example, you could use setTypeface() to have a button render its text in bold, instead
55
56
CHAPTER 8: Using XML-Based Layouts
of using a property in an XML layout. Since XML layouts are yet another file for you to keep track of, we need good reasons for using such files. Perhaps the biggest reason is to assist in the creation of tools for view definition, such as a GUI builder in an IDE like Eclipse or a dedicated Android GUI designer like DroidDraw. Such GUI builders could, in principle, generate Java code instead of XML. The challenge is rereading the definition in to support edits, which is far simpler when the data is in a structured format like XML rather than in a programming language. Moreover, keeping the generated bits separated from handwritten code makes it less likely that somebody’s custom-crafted source will get clobbered by accident when the generated bits get regenerated. XML forms a nice middle ground between something that is easy for tool writers to use and something that is easy for programmers to work with by hand as needed. Also, XML as a GUI definition format is becoming more commonplace. Microsoft’s Extensible Application Markup Language (XAML), Adobe’s Flex, Google’s Google Web Toolkit (GWT), and Mozilla’s XML User Interface Language (XUL) all take a similar approach to that of Android: put layout details in an XML file and put programming smarts in source files (e.g., JavaScript for XUL). Many less-well-known GUI frameworks, such as ZK, also use XML for view definition. While “following the herd” is not necessarily the best policy, it does have the advantage of helping to ease the transition to Android from any other XML-centered view description language.
OK, So What Does It Look Like? Here is the Button from the previous chapter’s sample application, converted into an XML layout file, found in the Layouts/NowRedux sample project:
The class name of the widget, Button, forms the name of the XML element. Since Button is an Android-supplied widget, we can just use the bare class name. If you create your own widgets as subclasses of android.view.View, you will need to provide a full package declaration as well (e.g., com.commonsware.android.MyWidget). The root element needs to declare the Android XML namespace: xmlns:android="http://schemas.android.com/apk/res/android"
All other elements will be children of the root and will inherit that namespace declaration. Because we want to reference this button from our Java code, we need to give it an identifier via the android:id attribute. We will cover this concept in greater detail in the next section. The remaining attributes are properties of this Button instance:
CHAPTER 8: Using XML-Based Layouts
android:text: Indicates the initial text to be displayed on the button face (in this case, an empty string)
android:layout_width and android:layout_height: Tell Android to have the button’s width and height fill the parent, which in this case is the entire screen
These attributes will be covered in greater detail in Chapter 10. Since this single widget is the only content in our activity’s view, we need only this single element. Complex views will require a whole tree of elements, representing the widgets and containers that control their positioning. All the remaining chapters of this book will use the XML layout form whenever practical, so there are dozens of other examples of more complex layouts for you to peruse.
What’s with the @ Signs? Many widgets and containers need to appear only in the XML layout file and do not need to be referenced in your Java code. For example, a static label (TextView) frequently needs to be in the layout file only to indicate where it should appear. These sorts of elements in the XML file do not need to have the android:id attribute to give them a name. Anything you do want to use in your Java source, though, needs an android:id. The convention is to use @+id/... as the id value, where the ... represents your locally unique name for the widget in question, for the first occurrence of a given id value in your layout file. In the XML layout example in the preceding section, @+id/button is the identifier for the Button widget. The second and subsequent occurrences in the same layout file should drop the + sign—a feature we will use in Chapter 10. Android provides a few special android:id values, of the form @android:id/.... You will see some of these values in various examples throughout this book.
And How Do We Attach These to the Java? Given that you have painstakingly set up the widgets and containers for your view in an XML layout file named main.xml stored in res/layout, all you need is one statement in your activity’s onCreate() callback to use that layout: setContentView(R.layout.main);
This is the same setContentView() we used earlier, passing it an instance of a View subclass (in that case, a Button). The Android-built View, constructed from our layout, is accessed from that code-generated R class. All of the layouts are accessible under R.layout, keyed by the base name of the layout file; for example, res/layout/main.xml results in R.layout.main. To access your identified widgets, use findViewById(), passing it the numeric identifier of the widget in question. That numeric identifier was generated by Android in the R
57
58
CHAPTER 8: Using XML-Based Layouts
class as R.id.something (where something is the specific widget you are seeking). Those widgets are simply subclasses of View, just like the Button instance we created in the previous chapter.
The Rest of the Story In the original Now demo, the button’s face would show the current time, which would reflect when the button was last pushed (or when the activity was first shown, if the button had not yet been pushed). Most of that logic still works, even in this revised demo (NowRedux). However, rather than instantiating the Button in our activity’s onCreate() callback, we can reference the one from the XML layout: package com.commonsware.android.layouts; import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.widget.Button; java.util.Date;
public class NowRedux extends Activity implements View.OnClickListener { Button btn; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); btn=(Button)findViewById(R.id.button); btn.setOnClickListener(this); updateTime(); } public void onClick(View view) { updateTime(); } private void updateTime() { btn.setText(new Date().toString()); } }
The first difference is that, rather than setting the content view to be a view we created in Java code, we set it to reference the XML layout (setContentView(R.layout.main)). The R.java source file will be updated when we rebuild this project to include a reference to our layout file (stored as main.xml in our project’s res/layout directory). The other difference is that we need to get our hands on our Button instance, for which we use the findViewById() call. Since we identified our button as @+id/button, we can reference the button’s identifier as R.id.button. Now, with the Button instance in hand, we can set the callback and set the label as needed.
CHAPTER 8: Using XML-Based Layouts
The results look the same as with the original Now demo, as shown in Figure 8–1.
Figure 8–1. The NowRedux sample activity
59
Chapter
9
Employing Basic Widgets Every GUI toolkit has some basic widgets: fields, labels, buttons, and so forth. Android’s toolkit is no different in scope, and the basic widgets provide a good introduction to how widgets work in Android activities.
Assigning Labels The simplest widget is the label, referred to in Android as a TextView. As in most GUI toolkits, labels are bits of text that can’t be edited directly by users. Typically, labels are used to identify adjacent widgets (e.g., a “Name:” label next to a field where the user fills in a name). In Java, you can create a label by creating a TextView instance. More commonly, though, you will create labels in XML layout files by adding a TextView element to the layout, with an android:text property to set the value of the label itself. If you need to swap labels based on certain criteria, such as internationalization, you may wish to use a string resource reference in the XML instead, as will be described later in this book. TextView has numerous other properties of relevance for labels, such as the following:
android:typeface: Sets the typeface to use for the label (e.g., monospace)
android:textStyle: Indicates that the typeface should be made bold (bold), italic (italic), or bold and italic (bold_italic)
android:textColor: Sets the color of the label’s text, in RGB hex format (e.g., #FF0000 for red)
For example, in the Basic/Label project, you will find the following layout file:
61
62
CHAPTER 9: Employing Basic Widgets
Just that layout alone, with the stub Java source provided by Android’s project builder (e.g., android create project), gives you the result shown in Figure 9–1.
Figure 9–1. The LabelDemo sample application
Button, Button, Who’s Got the Button? You’ve already seen the use of the Button widget in the previous two chapters. As it turns out, Button is a subclass of TextView, so everything discussed in the preceding section also applies to formatting the face of the button. However, Android 1.6 added a new feature for the declaration of the “on-click” listener for a Button. In addition to the classic approach of defining some object (such as the activity) as implementing the View.OnClickListener interface, you can now take a somewhat simpler approach:
Define some method on your Activity that holds the button that takes a single View parameter, has a void return value, and is public.
In your layout XML, on the Button element, include the android:onClick attribute with the name of the method you defined in the previous step.
For example, we might have a method on our Activity that looks like this: public void someMethod(View theButton) { // do something useful here }
CHAPTER 9: Employing Basic Widgets
Then, we could use this XML declaration for the Button itself, including android:onClick:
This is enough for Android to wire together the Button with the click handler.
Fleeting Images Android has two widgets to help you embed images in your activities: ImageView and ImageButton. As the names suggest, they are image-based analogues to TextView and Button, respectively. Each widget takes an android:src attribute (in an XML layout) to specify which picture to use. These attributes usually reference a drawable resource, described in greater detail in the chapter on resources. ImageButton, a subclass of ImageView, mixes in the standard Button behaviors, for responding to clicks and whatnot. For example, take a peek at the main.xml layout from the Basic/ImageView sample project:
The result, just using the code-generated activity, is simply the image, as shown in Figure 9–2.
63
64
CHAPTER 9: Employing Basic Widgets
Figure 9–2. The ImageViewDemo sample application
Fields of Green…or Other Colors Along with buttons and labels, fields are the third anchor of most GUI toolkits. In Android, they are implemented via the EditText widget, which is a subclass of the TextView used for labels. Along with the standard TextView properties (e.g., android:textStyle), EditText has many other properties that will be useful to you in constructing fields, including the following:
android:autoText: Controls if the field should provide automatic spelling assistance
android:capitalize: Controls if the field should automatically capitalize the first letter of entered text (e.g., in name and city fields)
android:digits: Configures the field to accept only certain digits
android:singleLine: Controls if the field is for single-line input or multiple-line input (e.g., does pressing Enter move you to the next widget or add a newline?)
Most of the preceding properties are also available from the new android:inputType attribute, added in Android 1.5 as part of adding “soft keyboards” to Android (discussed in Chapter 11). For example, from the Basic/Field project, here is an XML layout file showing an EditText widget:
CHAPTER 9: Employing Basic Widgets
Note that android:singleLine is set to "false", so users will be able to enter several lines of text. For this project, the FieldDemo.java file populates the input field with some prose: package com.commonsware.android.field; import android.app.Activity; import android.os.Bundle; import android.widget.EditText; public class FieldDemo extends Activity { @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); EditText fld=(EditText)findViewById(R.id.field); fld.setText("Licensed under the Apache License, Version 2.0 " + "(the \"License\"); you may not use this file " + "except in compliance with the License. You may " + "obtain a copy of the License at " + "http://www.apache.org/licenses/LICENSE-2.0"); } }
The result, once built and installed into the emulator, is shown in Figure 9–3. Another flavor of field is one that offers autocompletion, to help users supply a value without typing in the whole text. That is provided in Android as the AutoCompleteTextView widget, discussed in greater detail later in this book.
65
66
CHAPTER 9: Employing Basic Widgets
Figure 9–3. The FieldDemo sample application
Just Another Box to Check The classic check box has two states: checked and unchecked. Clicking the check box toggles between those states to indicate a choice (e.g., “Add rush delivery to my order”). In Android, there is a CheckBox widget to meet this need. It has TextView as an ancestor, so you can use TextView properties like android:textColor to format the widget. Within Java, you can invoke the following:
isChecked(): Determines if the check box has been checked
setChecked(): Forces the check box into a checked or unchecked state
toggle(): Toggles the check box as if the user checked it
Also, you can register a listener object (in this case, an instance of OnCheckedChangeListener) to be notified when the state of the check box changes. For example, from the Basic/CheckBox project, here is a simple check box layout:
The corresponding CheckBoxDemo.java retrieves and configures the behavior of the check box:
CHAPTER 9: Employing Basic Widgets
public class CheckBoxDemo extends Activity implements CompoundButton.OnCheckedChangeListener { CheckBox cb; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); cb=(CheckBox)findViewById(R.id.check); cb.setOnCheckedChangeListener(this); } public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { cb.setText("This checkbox is: checked"); } else { cb.setText("This checkbox is: unchecked"); } } }
Note that the activity serves as its own listener for check box state changes, since it implements the OnCheckedChangeListener interface (via cb.setOnCheckedChangeListener(this)). The callback for the listener is onCheckedChanged(), which receives the check box whose state has changed and the new state. In this case, we update the text of the check box to reflect what the actual box contains. The result? Clicking the check box immediately updates its text, as shown in Figures 9– 4 and 9–5.
67
68
CHAPTER 9: Employing Basic Widgets
Figure 9–4. The CheckBoxDemo sample application, with the check box unchecked
Figure 9–5. The same application, now with the check box checked
Turn the Radio Up As with other implementations of radio buttons in other toolkits, Android’s radio buttons are two-state, like check boxes, but can be grouped such that only one radio button in the group can be checked at any time.
CHAPTER 9: Employing Basic Widgets
Like CheckBox, RadioButton inherits from CompoundButton, which in turn inherits from TextView. Hence, all the standard TextView properties for font face, style, color, and so forth are available for controlling the look of radio buttons. Similarly, you can call isChecked() on a RadioButton to see if it is selected, toggle() to select it, and so on, as you can with a CheckBox. Most times, you will want to put your RadioButton widgets inside a RadioGroup. The RadioGroup indicates a set of radio buttons whose state is tied, meaning only one button in the group can be selected at any time. If you assign an android:id to your RadioGroup in your XML layout, you can access the group from your Java code and invoke the following:
check(): Checks a specific radio button via its ID (e.g., group.check(R.id.radio1))
clearCheck(): Clears all radio buttons, so none in the group are checked
getCheckedRadioButtonId(): Gets the ID of the currently checked radio button (or -1 if none are checked)
Note that the mutual-exclusion feature of RadioGroup applies only to RadioButton widgets that are immediate children of the RadioGroup. You cannot have other containers—discussed in the next chapter—between the RadioGroup and its RadioButton widgets. For example, from the Basic/RadioButton sample application, here is an XML layout showing a RadioGroup wrapping a set of RadioButton widgets:
Using the stock Android-generated Java for the project and this layout, you get the result shown in Figure 9–6.
69
70
CHAPTER 9: Employing Basic Widgets
Figure 9–6. The RadioButtonDemo sample application
Note that the radio button group is initially set to be completely unchecked at the outset. To preset one of the radio buttons to be checked, use either setChecked() on the RadioButton or check() on the RadioGroup from within your onCreate() callback in your activity.
It’s Quite a View All widgets, including the ones shown in the previous sections, extend View, which gives all widgets an array of useful properties and methods beyond those already described.
Padding Widgets have a minimum size, which may be influenced by what is inside of them. So, for example, a Button will expand to accommodate the size of its caption. You can control this size by using padding. Adding padding will increase the space between the contents (e.g., the caption of a Button) and the edges of the widget. Padding can be set once in XML for all four sides (android:padding) or on a per-side basis (android:paddingLeft, etc.). Padding can also be set in Java via the setPadding() method. The value of any of these is a dimension, a combination of a unit of measure and a count. So, 5px is 5 pixels, 10dip is 10 density-independent pixels, and 2mm is 2 millimeters. We will examine dimension in greater detail in an upcoming chapter.
CHAPTER 9: Employing Basic Widgets
Other Useful Properties In addition to the properties presented in this chapter and in the next chapter, some of the other properties on View that are most likely to be used include the following:
android: visibility: Controls whether the widget is initially visible
android:nextFocusDown, android:nextFocusLeft, android:nextFocusRight, and android:nextFocusUp: Control the focus order if the user uses the D-pad, trackball, or similar pointing device
android:contentDescription: Roughly equivalent to the alt attribute on an HTML tag, used by accessibility tools to help people who cannot see the screen navigate the application
Useful Methods You can toggle whether or not a widget is enabled via setEnabled() and see if it is enabled via isEnabled(). One common use pattern for this is to disable some widgets based on a CheckBox or RadioButton selection. You can give a widget focus via requestFocus() and see if it is focused via isFocused(). You might use this in concert with disabling widgets to ensure the proper widget has the focus once your disabling operation is complete. To help navigate the tree of widgets and containers that make up an activity’s overall view, you can use:
getParent(): Finds the parent widget or container
findViewById(): Finds a child widget with a certain ID
getRootView(): Gets the root of the tree (e.g., what you provided to the activity via setContentView())
Colors There are two types of color attributes in Android widgets. Some, like android:background, take a single color (or a graphic image to serve as the background). Others, like android:textColor on TextView (and subclasses), can take a ColorStateList, including via the Java setter (in this case, setTextColor()). A ColorStateList allows you to specify different colors for different conditions. For example, a TextView can have one text color when it is the selected item in a list and another color when it is not selected (Chapter 12 covers selection widgets). This is handled via the default ColorStateList associated with TextView. If you wish to change the color of a TextView widget in Java code, you have two main choices:
71
72
CHAPTER 9: Employing Basic Widgets
Use ColorStateList.valueOf(), which returns a ColorStateList in which all states are considered to have the same color, which you supply as the parameter to the valueOf() method. This is the Java equivalent of the android:textColor approach, to make the TextView always a specific color regardless of circumstances.
Create a ColorStateList with different values for different states, either via the constructor or via an XML drawable resource, a concept discussed in a later chapter.
10
Chapter
Working with Containers Containers pour a collection of widgets (and possibly child containers) into specific structures you like. If you want a form with labels on the left and fields on the right, you need a container. If you want OK and Cancel buttons to be beneath the rest of the form, next to one another, and flush to right side of the screen, you need a container. Just from a pure XML perspective, if you have multiple widgets (beyond RadioButton widgets in a RadioGroup), you need a container just to have a root element in which to place the widgets. Most GUI toolkits have some notion of layout management, frequently organized into containers. In Java/Swing, for example, you have layout managers like BoxLayout and containers that use them (e.g., Box). Some toolkits, such as XUL and Flex, stick strictly to the box model, figuring that any desired layout can be achieved through the right combination of nested boxes. Android, through LinearLayout, also offers a box model, but in addition supports a range of containers that provide different layout rules. In this chapter, we will look at three commonly used containers, LinearLayout (the box model), RelativeLayout (a rule-based model), and TableLayout (the grid model), along with ScrollView, a container designed to assist with implementing scrolling containers.
Thinking Linearly As just noted, LinearLayout is a box model—widgets or child containers are lined up in a column or row, one after the next. This works similarly to FlowLayout in Java/Swing, vbox and hbox in Flex and XUL, and so forth. Flex and XUL use the box as their primary unit of layout. If you want, you can use LinearLayout in much the same way, eschewing some of the other containers. Getting the visual representation you want is mostly a matter of identifying where boxes should nest and which properties those boxes should have, such as their alignment relative to other boxes.
73
74
CHAPTER 10: Working with Containers
LinearLayout Concepts and Properties To configure a LinearLayout, you have five main areas of control besides the container’s contents: the orientation, the fill model, the weight, the gravity, and the padding.
Orientation Orientation indicates whether the LinearLayout represents a row or a column. Just add the android:orientation property to your LinearLayout element in your XML layout, and set the value to be horizontal for a row or vertical for a column. The orientation can be modified at runtime by invoking setOrientation() on the LinearLayout, supplying it either HORIZONTAL or VERTICAL.
Fill Model Let’s imagine a row of widgets, such as a pair of radio buttons. These widgets have a “natural” size based on their text. Their combined size probably does not exactly match the width of the Android device’s screen—particularly since screens come in various sizes. We then have the issue of what to do with the remaining space. All widgets inside a LinearLayout must supply android:layout_width and android:layout_height properties to help address this issue. These properties’ values have three flavors:
You can provide a specific dimension, such as 125dip to indicate the widget should take up exactly a certain size.
You can provide wrap_content, which means the widget should fill up its natural space, unless that is too big, in which case Android can use word-wrap as needed to make it fit.
You can provide fill_parent, which means the widget should fill up all available space in its enclosing container, after all other widgets are taken care of.
The latter two flavors are the most common, as they are independent of screen size, allowing Android to adjust your view to fit the available space. NOTE: In API level 8 (Android 2.2), fill_parent was renamed to match_parent, for unknown reasons. You can still use fill_parent, as it will be supported for the foreseeable future. However, at such point in time as you are supporting only API level 8 or higher (e.g., android:minSdkVersion="8" in your manifest), you should probably switch over to match_parent.
CHAPTER 10: Working with Containers
Weight But what happens if we have two widgets that should split the available free space? For example, suppose we have two multiline fields in a column, and we want them to take up the remaining space in the column after all other widgets have been allocated their space. To make this work, in addition to setting android:layout_width (for rows) or android:layout_height (for columns) to fill_parent, you must also set android:layout_weight. This property indicates the proportion of the free space that should go to that widget. For example, if you set android:layout_weight to be the same nonzero value for a pair of widgets (e.g., 1), the free space will be split evenly between them. If you set it to be 1 for one widget and 2 for the other widget, the second widget will use up twice the free space that the first widget does. And so on. The weight for a widget is 0 by default. Another pattern for using weights is if you want to allocate sizes on a percentage basis. To use this technique for, say, a horizontal layout, do the following:
Set all the android:layout_width values to be 0 for the widgets in the layout.
Set the android:layout_weight values to be the desired percentage size for each widget in the layout.
Make sure all those weights add up to 100.
Gravity By default, everything in a LinearLayout is left- and top-aligned. So, if you create a row of widgets via a horizontal LinearLayout, the row will start flush on the left side of the screen. If that is not what you want, you need to specify a gravity value. Using android:layout_gravity on a widget (or calling setGravity() at runtime on the widget’s Java object), you can tell the widget and its container how to align it vis-à-vis the screen. For a column of widgets, common gravity values are left, center_horizontal, and right for left-aligned, centered, and right-aligned widgets, respectively. For a row of widgets, the default is for them to be aligned so their text is aligned on the baseline (the invisible line that letters seem to “sit on”). You can specify a gravity of center_vertical to center the widgets along the row’s vertical midpoint.
Margins By default, widgets are tightly packed next to each other. You can change this via the use of margins, a concept that is similar to that of padding, described in Chapter 9. The difference between padding and margins is apparent only for widgets with a nontransparent background. For widgets with a transparent background—like the
75
76
CHAPTER 10: Working with Containers
default look of a TextView—padding and margins have similar visual effect, increasing the space between the widget and adjacent widgets. For widgets with a nontransparent background—like a Button—padding is considered to be inside the background, while margins are considered to be outside the background. In other words, adding padding will increase the space between the contents (e.g., the caption of a Button) and the edges, while adding margins increases the empty space between the edges and adjacent widgets. Margins can be set in XML, either on a per-side basis (e.g., android:layout_marginTop) or on all sides via android:layout_margin. As with padding, the value of any of these is a dimension—a combination of a unit of measure and a count, such as 5px for 5 pixels.
LinearLayout Example Let’s look at an example (Containers/Linear) that shows LinearLayout properties set both in the XML layout file and at runtime. Here is the layout:
CHAPTER 10: Working with Containers
Note that we have a LinearLayout wrapping two RadioGroup sets. RadioGroup is a subclass of LinearLayout, so our example demonstrates nested boxes as if they were all LinearLayout containers. The top RadioGroup sets up a row (android:orientation = "horizontal") of RadioButton widgets. The RadioGroup has 5dip of padding on all sides, separating it from the other RadioGroup, where dip stands for density-independent pixels (think of them as ordinary pixels for now—we will get into the distinction later in the book). The width and height are both set to wrap_content, so the radio buttons will take up only the space that they need. The bottom RadioGroup is a column (android:orientation = "vertical") of three RadioButton widgets. Again, we have 5dip of padding on all sides and a natural height (android:layout_height = "wrap_content"). However, we have set android:layout_width to be fill_parent, meaning the column of radio buttons claims the entire width of the screen. To adjust these settings at runtime based on user input, we need some Java code: package com.commonsware.android.linear; import import import import import import import
android.app.Activity; android.os.Bundle; android.view.Gravity; android.text.TextWatcher; android.widget.LinearLayout; android.widget.RadioGroup; android.widget.EditText;
public class implements RadioGroup RadioGroup
LinearLayoutDemo extends Activity RadioGroup.OnCheckedChangeListener { orientation; gravity;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); orientation=(RadioGroup)findViewById(R.id.orientation); orientation.setOnCheckedChangeListener(this); gravity=(RadioGroup)findViewById(R.id.gravity); gravity.setOnCheckedChangeListener(this); } public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.horizontal: orientation.setOrientation(LinearLayout.HORIZONTAL); break; case R.id.vertical: orientation.setOrientation(LinearLayout.VERTICAL); break;
77
78
CHAPTER 10: Working with Containers
case R.id.left: gravity.setGravity(Gravity.LEFT); break; case R.id.center: gravity.setGravity(Gravity.CENTER_HORIZONTAL); break; case R.id.right: gravity.setGravity(Gravity.RIGHT); break; } } }
In onCreate(), we look up our two RadioGroup containers and register a listener on each, so we are notified when the radio buttons change state (setOnCheckedChangeListener(this)). Since the activity implements OnCheckedChangeListener, the activity itself is the listener. In onCheckedChanged() (the callback for the listener), we see which RadioButton had a state change. Based on the clicked-upon item, we adjust either the orientation of the first LinearLayout or the gravity of the second LinearLayout. Figure 10–1 shows the result when the demo is first launched inside the emulator.
Figure 10–1. The LinearLayoutDemo sample application, as initially launched
If we toggle on the “vertical” radio button, the top RadioGroup adjusts to match, as shown in Figure 10–2.
CHAPTER 10: Working with Containers
Figure 10–2. The same application, with the vertical radio button selected
If we toggle the “center” or “right” radio button, the bottom RadioGroup adjusts to match, as shown in Figures 10–3 and 10–4.
Figure 10–3. The same application, with the vertical and center radio buttons selected
79
80
CHAPTER 10: Working with Containers
Figure 10–4. The same application, with the vertical and right radio buttons selected
The Box Model As noted earlier in this chapter, some GUI frameworks treat everything as boxes—what Android calls LinearLayout containers. In Flex and XUL, for example, you create boxes and indicate how big they should be, as a percentage of the available space, and then you put widgets in the boxes. A similar pattern exists in Android for LinearLayout, as is demonstrated in the Containers\LinearPercent project. Here we have a layout XML file that contains a vertical LinearLayout wrapping three Button widgets:
CHAPTER 10: Working with Containers
Each of the three widgets will take up a certain percentage of the vertical space for the LinearLayout. Since the LinearLayout is set to fill the screen, this means that the three widgets will divide up the screen based on their requested percentages. To request a percentage, each Button does the following:
Sets its android:layout_height to be 0dip (note that we use height here because it is a vertical LinearLayout we are subdividing)
Sets its android:layout_weight to be the desired percentage (e.g., android:layout_weight="50")
So long as the weights sum to 100, as they do in this case, you will get your desired breakdown by percentage, as shown in Figure 10–5.
Figure 10–5. A LinearLayout split among three Buttons by percentage
All Things Are Relative RelativeLayout, as the name suggests, lays out widgets based on their relationship to other widgets in the container and the parent container. You can place widget X below and to the left of widget Y, have widget Z’s bottom edge align with the bottom of the
81
82
CHAPTER 10: Working with Containers
container, and so on. This is reminiscent of James Elliot’s RelativeLayout for use with Java/Swing.
RelativeLayout Concepts and Properties To make all this work, we need ways to reference other widgets within an XML layout file, plus ways to indicate the relative positions of those widgets.
Positions Relative to Container The easiest relationships to set up are those that tie a widget’s position to that of its container, using the following properties:
android:layout_alignParentTop: Aligns the widget’s top with the top of the container
android:layout_alignParentBottom: Aligns the widget’s bottom with the bottom of the container
android:layout_alignParentLeft: Aligns the widget’s left side with the left side of the container
android:layout_alignParentRight: Aligns the widget’s right side with the right side of the container
android:layout_centerHorizontal: Positions the widget horizontally at the center of the container
android:layout_centerVertical: Positions the widget vertically at the center of the container
android:layout_centerInParent: Positions the widget both horizontally and vertically at the center of the container
All of these properties take a simple Boolean value (true or false). Note that the padding of the widget is taken into account when performing these various alignments. The alignments are based on the widget’s overall cell (combination of its natural space plus the padding).
Relative Notation in Properties The remaining properties of relevance to RelativeLayout take as a value the identity of a widget in the container. To do this: 1.
Put identifiers (android:id attributes) on all elements that you will need to address.
2.
Reference other widgets using the same identifier value.
CHAPTER 10: Working with Containers
The first occurrence of an id value should include the plus sign (@+id/widget_a); the second and subsequent times that id value is used in the layout file, the plus sign should be omitted (@id/widget_a). This allows the build tools to better help you catch typos in your widget id values—if you do not have a plus sign for a widget id value that has not been seen before, that will be caught at compile time. For example, if widget A is identified as @+id/widget_a, widget B can refer to widget A in one of its own properties via the identifier @id/widget_a.
Positions Relative to Other Widgets The following four properties control the position of a widget relative to other widgets:
android:layout_above: Indicates that the widget should be placed above the widget referenced in the property
android:layout_below: Indicates that the widget should be placed below the widget referenced in the property
android:layout_toLeftOf: Indicates that the widget should be placed to the left of the widget referenced in the property
android:layout_toRightOf: Indicates that the widget should be placed to the right of the widget referenced in the property
Beyond those four properties, five additional properties can be used to control one widget’s alignment relative to another:
android:layout_alignTop: Indicates that the widget’s top should be aligned with the top of the widget referenced in the property
android:layout_alignBottom: Indicates that the widget’s bottom should be aligned with the bottom of the widget referenced in the property
android:layout_alignLeft: Indicates that the widget’s left should be aligned with the left of the widget referenced in the property
android:layout_alignRight: Indicates that the widget’s right should be aligned with the right of the widget referenced in the property
android:layout_alignBaseline: Indicates that the baseline of the two widgets should be aligned (where the baseline is the invisible line that text appears to sit on)
The android:layout_alignBaseline property is useful for aligning labels and fields so that the text appears natural. Since fields have a box around them and labels do not, android:layout_alignTop would align the top of the field’s box with the top of the label, causing the text of the label to be higher on the screen than the text entered into the field.
83
84
CHAPTER 10: Working with Containers
So, if we want widget B to be positioned to the right of widget A, in the XML element for widget B, we need to include android:layout_toRightOf = "@id/widget_a" (assuming @id/widget_a is the identity of widget A).
Order of Evaluation Android formerly used a single pass to process RelativeLayout-defined rules. That meant you could not reference a widget (e.g., via android:layout_above) until it had been declared in the XML. This made defining some layouts a bit complicated. Starting in Android 1.6, Android uses two passes to process the rules, so you can now safely have forward references to as-yet-undefined widgets.
RelativeLayout Example With all that in mind, let’s examine a typical form with a field, a label, and a pair of buttons labeled OK and Cancel. Here is the XML layout, pulled from the Containers/Relative sample project:
CHAPTER 10: Working with Containers
First, we open the RelativeLayout. In this case, we want to use the full width of the screen (android:layout_width = "fill_parent") and only as much height as we need (android:layout_height = "wrap_content"). Next, we define the label as a TextView. We indicate that we want its left edge aligned with the left edge of the RelativeLayout (android:layout_alignParentLeft="true") and its baseline aligned with the baseline of the yet-to-be-defined EditText. Since the EditText has not been declared yet, we use the + sign in the ID (android:layout_alignBaseline="@+id/entry"). After that, we add in the field as an EditText. We want the field to be to the right of the label, have the field be aligned with the top of the RelativeLayout, and have the field take up the rest of this “row” in the layout. These requirements are handled by the following three properties, respectively:
android:layout_toRightOf = "@id/label"
android:layout_alignParentTop = "true"
android:layout_width = "fill_parent"
Then, the OK button is set to be below the field (android:layout_below = "@id/entry") and have its right side align with the right side of the field (android:layout_alignRight = "@id/entry"). The Cancel button is set to be to the left of the OK button (android:layout_toLeft = "@id/ok") and have its top aligned with the OK button (android:layout_alignTop = "@id/ok"). With no changes to the autogenerated Java code, the emulator gives us the result shown in Figure 10–6.
Figure 10–6. The RelativeLayoutDemo sample application
85
86
CHAPTER 10: Working with Containers
Overlap RelativeLayout also has a feature that LinearLayout lacks—the ability to have widgets overlap one another. Later children of a RelativeLayout are “higher in the Z axis” than are earlier children, meaning that later children will overlap earlier children if they are set up to occupy the same space in the layout. This will be clearer with an example. Here is a layout, from Containers/RelativeOverlap, with a RelativeLayout holding two Button widgets:
The first Button is set to fill the screen. The second Button is set to be centered inside the parent and to take up only as much space as is needed for its caption. Hence, the second Button will appear to float over the first Button, as shown in Figure 10–7.
CHAPTER 10: Working with Containers
Figure 10–7. The RelativeOverlap sample application
Both Button widgets can still be clicked, though clicking the smaller Button does not also click the bigger Button. Your clicks will be handled by the widget on top in the case of an overlap like this.
Tabula Rasa If you like HTML tables, spreadsheet grids, and similar layout options, you will like Android’s TableLayout, which allows you to position your widgets in a grid to your specifications. You control the number of rows and columns, which columns might shrink or stretch to accommodate their contents, and so on. TableLayout works in conjunction with TableRow. TableLayout controls the overall behavior of the container, with the widgets themselves poured into one or more TableRow containers, one per row in the grid.
TableLayout Concepts and Properties For your table layout to work as you intend, you need to understand how widgets work with rows and columns, and how to handle widgets that live outside of rows.
87
88
CHAPTER 10: Working with Containers
Putting Cells in Rows Rows are declared by you, the developer, by putting widgets as children of a TableRow inside the overall TableLayout. You, therefore, control directly how many rows appear in the table. The number of columns is determined by Android; you control the number of columns in an indirect fashion. First, there will be at least one column per widget in your longest row. So if you have three rows—one with two widgets, one with three widgets, and one with four widgets—there will be at least four columns. However, you can have a widget take up more than one column by including the android:layout_span property, indicating the number of columns the widget spans. This is akin to the colspan attribute one finds in table cells in HTML. In this XML layout fragment, the field spans three columns:
Ordinarily, widgets are put into the first available column. In the preceding fragment, the label would go in the first column (column 0, as columns are counted starting from 0), and the field would go into a spanned set of three columns (columns 1 through 3). However, you can put a widget into a different column via the android:layout_column property, specifying the 0-based column the widget belongs to:
In the preceding XML layout fragment, the Cancel button goes in the third column (column 2). The OK button then goes into the next available column, which is the fourth column.
Non-Row Children of TableLayout Normally, TableLayout contains only TableRow elements as immediate children. However, it is possible to put other widgets in between rows. For those widgets, TableLayout behaves a bit like LinearLayout with vertical orientation. The widgets automatically have their width set to fill_parent, so they will fill the same space that the longest row does. One pattern for this is to use a plain View as a divider. For example, you could use as a two-pixelhigh blue bar across the width of the table.
CHAPTER 10: Working with Containers
Stretch, Shrink, and Collapse By default, each column will be sized according to the natural size of the widest widget in that column (taking spanned columns into account). Sometimes, though, that does not work out very well, and you need more control over column behavior. You can place an android:stretchColumns property on the TableLayout. The value should be a single column number (again, 0-based) or a comma-delimited list of column numbers. Those columns will be stretched to take up any available space on the row. This helps if your content is narrower than the available space. Conversely, you can place an android:shrinkColumns property on the TableLayout. Again, this should be a single column number or a comma-delimited list of column numbers. The columns listed in this property will try to word-wrap their contents to reduce the effective width of the column—by default, widgets are not word-wrapped. This helps if you have columns with potentially wordy content that might cause some columns to be pushed off the right side of the screen. You can also leverage an android:collapseColumns property on the TableLayout, again with a column number or comma-delimited list of column numbers. These columns will start out collapsed, meaning they will be part of the table information but will be invisible. Programmatically, you can collapse and uncollapse columns by calling setColumnCollapsed() on the TableLayout. You might use this to allow users to control which columns are of importance to them and should be shown versus which ones are less important and can be hidden. You can also control stretching and shrinking at runtime via setColumnStretchable() and setColumnShrinkable().
TableLayout Example The XML layout fragments previously shown, when combined, give us a TableLayout rendition of the form we created for RelativeLayout, with the addition of a divider line between the label/field and the two buttons (found in the Containers/Table demo):
89
90
CHAPTER 10: Working with Containers
When compiled against the generated Java code and run on the emulator, we get the result shown in Figure 10–8.
Figure 10–8. The TableLayoutDemo sample application
Scrollwork Phone screens tend to be small, which requires developers to use some tricks to present a lot of information in the limited available space. One trick for doing this is to use scrolling, so that only part of the information is visible at one time, and the rest is available via scrolling up or down. ScrollView is a container that provides scrolling for its contents. You can take a layout that might be too big for some screens, wrap it in a ScrollView, and still use your existing layout logic. The user can see only part of your layout at one time, and see the rest via scrolling. For example, here is a ScrollView used in an XML layout file (from the Containers/Scroll demo):
91
92
CHAPTER 10: Working with Containers
Without the ScrollView, the table would take up at least 560 pixels (seven rows at 80 pixels each, based on the View declarations). There may be some devices with screens capable of showing that much information, but many will be smaller. The ScrollView lets us keep the table as is, but present only part of it at a time. On the stock Android emulator, when the activity is first viewed, it appears as shown in Figure 10–9.
Figure 10–9. The ScrollViewDemo sample application
Notice how only five rows and part of the sixth are visible. By pressing the up/down buttons on the D-pad, you can scroll up and down to see the remaining rows. Also note how the right side of the content is clipped by the scrollbar—be sure to put some padding on that side or otherwise ensure your own content does not get clipped in that fashion. Android 1.5 introduced HorizontalScrollView, which works like ScrollView, but horizontally. This is useful for forms that might be too wide rather than too tall. Note that neither ScrollView nor HorizontalScrollView will give you bidirectional scrolling, so you have to choose vertical or horizontal. Also, note that you cannot put scrollable items into a ScrollView. For example, a ListView widget—which we will see in an upcoming chapter—already knows how to scroll. If you put a ListView in a ScrollView, it will not work very well.
11
Chapter
The Input Method Framework Android 1.5 introduced the input method framework (IMF), which is commonly referred to as soft keyboards. However, this term is not necessarily accurate, as IMF could be used for handwriting recognition or other means of accepting text input via the screen.
Keyboards, Hard and Soft Some Android devices have a hardware keyboard that is visible some of the time (when it is slid out). A few Android devices have a hardware keyboard that is always visible (socalled “bar” or “slab” phones). Most Android devices, though, have no hardware keyboard at all. The IMF handles all of these scenarios. In short, if there is no hardware keyboard, an input method editor (IME) will be available to the user when they tap an enabled EditText. If the default functionality of the IME is what you want to offer, you don’t need to make any code changes to your application. Fortunately, Android is fairly smart about guessing what you want, so you may simply need to test with the IME and make no specific code changes. But the IME may not quite behave how you would like it to for your application. For example, in the Basic/Field sample project, the FieldDemo activity has the IME overlaying the multiple-line EditText, as shown in Figure 11–1. It would be nice to have more control over how this appears, and to be able to control other behavior of the IME. Fortunately, the IMF as a whole gives you many options for this, as described in this chapter.
93
94
CHAPTER 11: The Input Method Framework
Figure 11–1. The input method editor, as seen in the FieldDemo sample application
Tailored to Your Needs Android 1.1 and earlier offered many attributes on EditText widgets to control their style of input, such as android:password to indicate a field should be for password entry (shrouding the password keystrokes from prying eyes). Starting in Android 1.5, with the IMF, many of these attributes have been combined into a single android:inputType attribute. The android:inputType attribute takes a class plus modifiers, in a pipe-delimited list (where | is the pipe character). The class generally describes what the user is allowed to input, and this determines the basic set of keys available on the soft keyboard. The available classes are as follows:
text (the default)
number
phone
datetime
date
time
Many of these classes offer one or more modifiers to further refine what the user will be allowed to enter. To get a better understanding of how these modifiers work, take a look at the res/layout/main.xml file from the InputMethod/IMEDemo1 project:
CHAPTER 11: The Input Method Framework
This shows a TableLayout containing five rows, each demonstrating a slightly different flavor of EditText:
95
96
CHAPTER 11: The Input Method Framework
The first row has no attributes at all on the EditText, meaning you get a plain text-entry field.
The second row has android:inputType = "text|textEmailAddress", meaning it is a text-entry field that specifically seeks an e-mail address.
The third row allows for signed decimal numeric input, via android:inputType = "number|numberSigned|numberDecimal".
The fourth row is set up to allow for data entry of a date (android:inputType = "date").
The last allows for multiline input with autocorrection of probable spelling errors (android:inputType = "text|textMultiLine|textAutoCorrect").
The class and modifiers tailor the keyboard. So, a plain text-entry field results in a plain soft keyboard, as shown in Figure 11–2.
Figure 11–2. A standard input method editor (a.k.a., soft keyboard)
An e-mail address field might put the @ symbol on the soft keyboard, at the cost of a smaller spacebar, as shown in Figure 11–3.
CHAPTER 11: The Input Method Framework
Figure 11–3. The input method editor for e-mail addresses
Note, though, that this behavior is specific to the IME. Some editors might put the @ symbol on the primary keyboard for an e-mail field. Some might put a .com button on the primary keyboard. Some might not react at all. It is up to the implementation of the IME—all you can do is supply the hint. Number and date fields restrict the keys to numeric keys, plus a set of symbols that may or may not be valid on a given field, as shown in Figure 11–4.
97
98
CHAPTER 11: The Input Method Framework
Figure 11–4. The input method editor for signed decimal numbers
These are just a few examples of the possible IMEs. By choosing the appropriate android:inputType, you can give users a soft keyboard that best suits the type of data they should be entering.
Tell Android Where It Can Go You may have noticed a subtle difference between the IME shown in Figure 11–2 and the IME shown in Figure 11–3, beyond the addition of the @ key. The lower-right corner of the soft keyboard in Figure 11–3 has a Next button, whereas the one in Figure 11–2 has a newline button. This points out two things:
EditText widgets are multiline by default if you do not specify android:inputType.
You can control what goes on with that lower-right button, called the accessory button.
By default, on an EditText where you have specified android:inputType, the accessory button will be Next, moving you to the next EditText in sequence, or Done, if you are on the last EditText on the screen. You can manually stipulate what the accessory button will be labeled via the android:imeOptions attribute. For example, in the res/layout/main.xml file from InputMethod/IMEDemo2, you will see an augmented version of the previous example, where two input fields specify what their accessory button should look like:
Here, we attach a Send action to the accessory button for the e-mail address (android:imeOptions = "actionSend"), and the Done action on the middle field (android:imeOptions = "actionDone").
99
100
CHAPTER 11: The Input Method Framework
By default, Next moves the focus to the next EditText and Done closes the IME. However, for those actions, or for any others like Send, you can use setOnEditorActionListener() on EditText (technically, on the TextView superclass) to get control when the accessory button is clicked or the user presses the Enter key. You are provided with a flag indicating the desired action (e.g., IME_ACTION_SEND), and you can then do something to handle that request (e.g., send an e-mail to the supplied email address).
Fitting In Notice that the IMEDemo2 layout shown in the preceding section has another difference from its IMEDemo1 predecessor: the use of a ScrollView container wrapping the TableLayout. This ties into another level of control you have over the IMEs: what happens to your activity’s own layout when the IME appears. There are three possibilities, depending on circumstances:
Android can “pan” your activity, effectively sliding the whole layout up to accommodate the IME, or overlaying your layout, depending on whether the EditText being edited is at the top or bottom. This has the effect of hiding some portion of your UI.
Android can resize your activity, effectively causing it to shrink to a smaller screen dimension, allowing the IME to sit below the activity itself. This is great when the layout can readily be shrunk (e.g., it is dominated by a list or multiline input field that does not need the whole screen to be functional).
In landscape mode, Android may display the IME full-screen, obscuring your entire activity. This allows for a bigger keyboard and generally easier data entry.
Android controls the full-screen option purely on its own. And, by default, Android will choose between pan and resize modes depending on what your layout looks like. If you want to specifically choose between pan and resize, you can do so via an android:windowSoftInputMode attribute on the element in your AndroidManifest.xml file. For example, here is the manifest from IMEDemo2:
CHAPTER 11: The Input Method Framework
Because we specified resize, Android will shrink our layout to accommodate the IME. With the ScrollView in place, this means the scroll bar will appear as needed, as shown in Figure 11–5.
Figure 11–5. The shrunken, scrollable layout
Jane, Stop This Crazy Thing! Sometimes, you need the IME to just go away. For example, if you make the accessory button a Search button, the IME won’t be hidden automatically when the user taps that button, whereas you may want it to be hidden. To hide the IME, you need to make a call to the InputMethodManager, a system service that controls these IMEs: InputMethodManager mgr=(InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); mgr.hideSoftInputFromWindow(fld.getWindowToken(), 0);
(In the preceding line, fld is the EditText whose IME you want to hide.) This will always close the designated IME. However, bear in mind that there are two ways a user can open the IME in the first place:
If the user’s device does not have a hardware keyboard exposed, and the user taps the EditText, the IME should appear.
If the user previously dismissed the IME or is using the IME for a widget that does not normally pop one up (e.g., ListView), and the user presses the Menu button, the IME should appear.
101
102
CHAPTER 11: The Input Method Framework
If you want to close the IME only for the first scenario, but not the second, use InputMethodManager.HIDE_IMPLICIT_ONLY as a flag for the second parameter to your call to hideSoftInputFromWindow(), instead of the 0 shown in the previous example.
12
Chapter
Using Selection Widgets In Chapter 11, you saw how fields could have constraints placed on them to limit possible input, such as numeric-only or phone-number-only. These sorts of constraints help users “get it right” when entering information, particularly on mobile devices with cramped keyboards. Of course, the ultimate in constrained input is to allow selection only from a set of items, such as a group of radio buttons. Classic UI toolkits have list boxes, combo boxes, drop-down lists, and the like for that very purpose. Android provides many of the same sorts of widgets, plus others of particular interest for mobile devices (e.g., the Gallery for examining saved photos). Moreover, Android offers a flexible framework for determining which choices are available in these widgets. Specifically, Android offers a framework of data adapters that provides a common interface for selection lists, ranging from static arrays to database contents. Selection views—widgets for presenting lists of choices—are handed an adapter to supply the actual choices.
Adapting to the Circumstances In the abstract, adapters provide a common interface to multiple disparate APIs. More specifically, in Android’s case, adapters provide a common interface to the data model behind a selection-style widget, such as a list box. This use of Java interfaces is fairly common (e.g., Java/Swing’s model adapters for JTable), and Java is far from the only environment offering this sort of abstraction (e.g., Flex’s XML data-binding framework accepts XML inlined as static data or retrieved from the Internet). Android’s adapters are responsible not only for providing the roster of data for a selection widget, but also for converting individual elements of data into specific views to be displayed inside the selection widget. The latter facet of the adapter system may sound a little odd, but in reality, it is not that different from other GUI toolkits’ ways of overriding default display behavior. For example, in Java/Swing, if you want a JListbacked list box to actually be a checklist (where individual rows are a check box plus label, and clicks adjust the state of the check box), you inevitably wind up calling
103
104
CHAPTER 12: Using Selection Widgets
setCellRenderer() to supply your own ListCellRenderer, which in turn converts strings for the list into JCheckBox-plus-JLabel composite widgets.
Using ArrayAdapter The easiest adapter to use is ArrayAdapter. You simply wrap one of these around a Java array or java.util.List instance, and you have a fully functioning adapter: String[] items={"this", "is", "a", "really", "silly", "list"}; new ArrayAdapter(this, android.R.layout.simple_list_item_1, items);
One flavor of the ArrayAdapter constructor takes three parameters:
The Context to use (typically this will be your activity instance)
The resource ID of a view to use (such as a built-in system resource ID, as shown in the preceding example)
The actual array or list of items to show
By default, the ArrayAdapter will invoke toString() on the objects in the list and wrap each of those strings in the view designated by the supplied resource. android.R.layout.simple_list_item_1 simply turns those strings into TextView objects. Those TextView widgets, in turn, will be shown in the list, spinner, or whatever widget uses this ArrayAdapter. If you want to see what android.R.layout.simple_list_item_1 looks like, you can find a copy of it in your SDK installation—just search for simple_list_item_1.xml. In Chapter 13, you’ll see how to subclass an adapter and override row creation, to give you greater control over how rows appear.
Lists of Naughty and Nice The classic list box widget in Android is known as ListView. Include one of these in your layout, invoke setAdapter() to supply your data and child views, and attach a listener via setOnItemSelectedListener() to find out when the selection has changed. With that, you have a fully functioning list box. However, if your activity is dominated by a single list, you might consider creating your activity as a subclass of ListActivity, rather than the regular Activity base class. If your main view is just the list, you do not even need to supply a layout—ListActivity will construct a full-screen list for you. If you do want to customize the layout, you can, as long as you identify your ListView as @android:id/list, so ListActivity knows which widget is the main list for the activity. For example, here is a layout pulled from the Selection/List sample project, a simple list with a label on top to show the current selection:
CHAPTER 12: Using Selection Widgets
The Java code to configure the list and connect the list with the label is as follows: public class ListViewDemo extends ListActivity { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, items)); selection=(TextView)findViewById(R.id.selection); } public void onListItemClick(ListView parent, View v, int position, long id) { selection.setText(items[position]); } }
With ListActivity, you can set the list adapter via setListAdapter()—in this case, providing an ArrayAdapter wrapping an array of nonsense strings. To find out when the list selection changes, override onListItemClick() and take appropriate steps based on the supplied child view and position—in this case, updating the label with the text for that position. The results are shown in Figure 12–1.
105
106
CHAPTER 12: Using Selection Widgets
Figure 12–1. The ListViewDemo sample application
The second parameter to our ArrayAdapter, android.R.layout.simple_list_item_1, controls the appearance of the rows. The value used in the preceding example provides the standard Android list row: big font, a lot of padding, and white text.
Selection Modes By default, ListView is set up to simply collect clicks on list entries. If you want a list that tracks a user’s selection, or possibly multiple selections, ListView can handle that as well, but it requires a few changes. First, you need to call setChoiceMode() on the ListView in Java code to set the choice mode, supplying either CHOICE_MODE_SINGLE or CHOICE_MODE_MULTIPLE as the value. You can get your ListView from a ListActivity via getListView(). You can also declare this via the android:choiceMode attribute in your layout XML. Then, instead of using android.R.layout.simple_list_item_1 as the layout for the list rows in your ArrayAdapter constructor, you need to use either android.R.layout.simple_list_item_single_choice or android.R.layout.simple_list_item_multiple_choice for single-choice or multiplechoice lists, respectively. For example, here is an activity layout from the Selection/Checklist sample project:
It is a full-screen ListView, with the android:choiceMode="multipleChoice" attribute to indicate that we want multiple-choice support. Our activity simply uses a standard ArrayAdapter on our list of nonsense words, but uses android.R.layout.simple_list_item_multiple_choice as the row layout: package com.commonsware.android.checklist; import import import import
android.os.Bundle; android.app.ListActivity; android.widget.ArrayAdapter; android.widget.ListView;
public class ChecklistDemo extends ListActivity { private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_multiple_choice, items)); } }
The user sees the list of words on the left with check boxes down the right edge, as shown in Figure 12–2.
107
108
CHAPTER 12: Using Selection Widgets
Figure 12–2. Multiple-select mode
If we wanted to, we could call getCheckedItemPositions() on our ListView to find out which items the user checked, or setItemChecked() to check (or uncheck) a specific entry ourselves.
Spin Control In Android, the Spinner is the equivalent of the drop-down selector you might find in other toolkits (e.g., JComboBox in Java/Swing). Pressing the center button on the D-pad pops up a selection dialog box from which the user can choose an item. The Spinner basically provides list selection capabilities without taking up all the screen space of a ListView, at the cost of an extra click or screen tap to make a change. As with ListView, you provide the adapter for data and child views via setAdapter(), and hook in a listener object for selections via setOnItemSelectedListener(). If you want to tailor the view used when displaying the drop-down perspective, you need to configure the adapter, not the Spinner widget. Use the setDropDownViewResource() method to supply the resource ID of the view to use. For example, culled from the Selection/Spinner sample project, here is an XML layout for a simple view with a Spinner:
This is the same view as shown in the previous section, but with a Spinner instead of a ListView. The Spinner property android:drawSelectorOnTop controls whether the arrow is drawn on the selector button on the right side of the Spinner UI. To populate and use the Spinner, we need some Java code: public class SpinnerDemo extends Activity implements AdapterView.OnItemSelectedListener { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); selection=(TextView)findViewById(R.id.selection); Spinner spin=(Spinner)findViewById(R.id.spinner); spin.setOnItemSelectedListener(this); ArrayAdapter aa=new ArrayAdapter(this, android.R.layout.simple_spinner_item, items); aa.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); spin.setAdapter(aa); } public void onItemSelected(AdapterView parent, View v, int position, long id) { selection.setText(items[position]); } public void onNothingSelected(AdapterView parent) { selection.setText(""); } }
109
110
CHAPTER 12: Using Selection Widgets
Here, we attach the activity itself as the selection listener (spin.setOnItemSelectedListener(this)). This works because the activity implements the OnItemSelectedListener interface. We configure the adapter not only with the list of fake words, but also with a specific resource to use for the drop-down view (via aa.setDropDownViewResource()). Also note the use of android.R.layout.simple_spinner_item as the built-in View for showing items in the spinner itself. Finally, we implement the callbacks required by OnItemSelectedListener to adjust the selection label based on user input. Figures 12–3 and 12–4 show the results.
Figure 12–3. The SpinnerDemo sample application, as initially launched
CHAPTER 12: Using Selection Widgets
Figure 12–4. The same application, with the spinner drop-down list displayed
Grid Your Lions (or Something Like That...) As the name suggests, GridView gives you a two-dimensional grid of items to choose from. You have moderate control over the number and size of the columns; the number of rows is dynamically determined based on the number of items the supplied adapter says are available for viewing. There are a few properties that, when combined, determine the number of columns and their sizes:
android:numColumns: Indicates how many columns there are, or, if you supply a value of auto_fit, Android will compute the number of columns based on the available space and the following properties in this list.
android:verticalSpacing and android:horizontalSpacing: Indicate how much whitespace should exist between items in the grid.
android:columnWidth: Indicates how many pixels wide each column should be.
android:stretchMode: Indicates, for grids with auto_fit for android:numColumns, what should happen for any available space not taken up by columns or spacing. This can be columnWidth, to have the columns take up available space, or spacingWidth, to have the whitespace between columns absorb extra space.
111
112
CHAPTER 12: Using Selection Widgets
Otherwise, the GridView works much like any other selection widget—use setAdapter() to provide the data and child views, invoke setOnItemSelectedListener() to register a selection listener, and so on. For example, here is an XML layout from the Selection/Grid sample project, showing a GridView configuration:
For this grid, we take up the entire screen except for what our selection label requires. The number of columns is computed by Android (android:numColumns = "auto_fit") based on our horizontal spacing (android:horizontalSpacing = "5dip") and column width (android:columnWidth = "100dip"), with the columns absorbing any “slop” width left over (android:stretchMode = "columnWidth"). The Java code to configure the GridView is as follows: package com.commonsware.android.grid; import import import import import import import import
android.app.Activity; android.content.Context; android.os.Bundle; android.view.View; android.widget.AdapterView; android.widget.ArrayAdapter; android.widget.GridView; android.widget.TextView;
public class GridDemo extends Activity implements AdapterView.OnItemSelectedListener { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet",
CHAPTER 12: Using Selection Widgets
"consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); selection=(TextView)findViewById(R.id.selection); GridView g=(GridView) findViewById(R.id.grid); g.setAdapter(new ArrayAdapter(this, R.layout.cell, items)); g.setOnItemSelectedListener(this); } public void onItemSelected(AdapterView parent, View v, int position, long id) { selection.setText(items[position]); } public void onNothingSelected(AdapterView parent) { selection.setText(""); } }
The grid cells are defined by a separate res/layout/cell.xml file, referenced in our ArrayAdapter as R.layout.cell:
With the vertical spacing from the XML layout (android:verticalSpacing = "40dip"), the grid overflows the boundaries of the emulator’s screen, as shown in Figures 12–5 and 12–6.
113
114
CHAPTER 12: Using Selection Widgets
Figure 12–5. The GridDemo sample application, as initially launched
Figure 12–6. The same application, scrolled to the bottom of the grid
CHAPTER 12: Using Selection Widgets
Fields: Now with 35% Less Typing! The AutoCompleteTextView is sort of a hybrid between the EditText (field) and the Spinner. With autocompletion, as the user types, the text is treated as a prefix filter, comparing the entered text as a prefix against a list of candidates. Matches are shown in a selection list that drops down from the field (as with Spinner). The user can either type the full entry (e.g., something not in the list) or choose an item from the list to be the value of the field. AutoCompleteTextView subclasses EditText, so you can configure all the standard lookand-feel aspects, such as font face and color. In addition, AutoCompleteTextView has a android:completionThreshold property, to indicate the minimum number of characters a user must enter before the list filtering begins. You can give AutoCompleteTextView an adapter containing the list of candidate values via setAdapter(). However, since the user could type something that is not in the list, AutoCompleteTextView does not support selection listeners. Instead, you can register a TextWatcher, as you can with any EditText widget, to be notified when the text changes. These events will occur either because of manual typing or from a selection from the drop-down list. The following is a familiar XML layout, this time containing an AutoCompleteTextView (pulled from the Selection/AutoComplete sample application):
The corresponding Java code is as follows: package com.commonsware.android.auto; import import import import import import import import
android.app.Activity; android.os.Bundle; android.text.Editable; android.text.TextWatcher; android.view.View; android.widget.AdapterView; android.widget.ArrayAdapter; android.widget.AutoCompleteTextView;
115
116
CHAPTER 12: Using Selection Widgets
import android.widget.TextView; public class AutoCompleteDemo extends Activity implements TextWatcher { private TextView selection; private AutoCompleteTextView edit; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); selection=(TextView)findViewById(R.id.selection); edit=(AutoCompleteTextView)findViewById(R.id.edit); edit.addTextChangedListener(this); edit.setAdapter(new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, items)); } public void onTextChanged(CharSequence s, int start, int before, int count) { selection.setText(edit.getText()); } public void beforeTextChanged(CharSequence s, int start, int count, int after) { // needed for interface, but not used } public void afterTextChanged(Editable s) { // needed for interface, but not used } }
This time, our activity implements TextWatcher, which means our callbacks are onTextChanged(),beforeTextChanged(), and afterTextChanged(). In this case, we are interested only in onTextChanged(), and we update the selection label to match the AutoCompleteTextView’s current contents. Figures 12–7, 12–8, and 12–9 show the results.
CHAPTER 12: Using Selection Widgets
Figure 12–7. The AutoCompleteDemo sample application, as initially launched
Figure 12–8. The same application, after a few matching letters were entered, showing the autocomplete dropdown
117
118
CHAPTER 12: Using Selection Widgets
Figure 12–9. The same application, after the autocomplete value was selected
Galleries, Give or Take the Art The Gallery widget is not one ordinarily found in GUI toolkits. It is, in effect, a list box that is laid out horizontally. One choice follows the next across the horizontal plane, with the currently selected item highlighted. On an Android device, one rotates through the options via the left and right D-pad buttons. Compared to the ListView, the Gallery takes up less screen space, while still showing multiple choices at one time (assuming they are short enough). Compared to the Spinner, the Gallery always shows more than one choice at a time. The quintessential example use for the Gallery is image preview. Given a collection of photos or icons, the Gallery lets people preview the pictures in the process of choosing one. Code-wise, the Gallery works much like a Spinner or GridView. In your XML layout, you have a few properties at your disposal:
android:spacing: Controls the number of pixels between entries in the list.
android:spinnerSelector: Controls what is used to indicate a selection. This can either be a reference to a Drawable (see the resources chapter) or an RGB value in #AARRGGBB or similar notation.
android:drawSelectorOnTop: Indicates if the selection bar (or Drawable) should be drawn before (false) or after (true) drawing the selected child. If you choose true, be sure that your selector has sufficient transparency to show the child through the selector; otherwise, users will not be able to read the selection.
13
Chapter
Getting Fancy with Lists The humble ListView is one of the most important widgets in all of Android, simply because it is used so frequently. Whether choosing a contact to call, an e-mail message to forward, or an e-book to read, ListView widgets are employed in a wide range of activities. Of course, it would be nice if they were more than just plain text. The good news is that Android lists can be as fancy as you want, within the limitations of a mobile device’s screen, of course. However, making them fancy takes some work, requiring the features of Android that are covered in this chapter.
Getting to First Base The classic Android ListView is a plain list of text—solid but uninspiring. Basically, we hand the ListView a bunch of words in an array and tell Android to use a simple built-in layout for pouring those words into a list. However, we can have a list whose rows are made up of icons, icons and text, check boxes and text, or whatever we want. It is merely a matter of supplying enough data to the adapter and helping the adapter to create a richer set of View objects for each row. For example, suppose we want a ListView whose entries are made up of an icon, followed by some text. We could construct a layout for the row that looks like this, found in res/layout/row.xml in the FancyLists/Static sample project:
This layout uses a LinearLayout to set up a row, with the icon on the left and the text (in a nice big font) on the right. However, by default, Android has no idea that we want to use this layout with our ListView. To make the connection, we need to supply our Adapter with the resource ID of the custom layout shown previously: public class StaticDemo extends ListActivity { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new ArrayAdapter(this, R.layout.row, R.id.label, items)); selection=(TextView)findViewById(R.id.selection); } public void onListItemClick(ListView parent, View v, int position, long id) { selection.setText(items[position]); } }
This follows the general structure for the previous ListView sample. The key difference here is that we have told ArrayAdapter that we want to use our custom layout (R.layout.row) and that the TextView where the word should go is known as R.id.label within that custom layout. NOTE: Remember that to reference a layout (row.xml), use R.layout as a prefix on the base name of the layout XML file (R.layout.row). The result is a ListView with icons down the left side; in this example, all the icons are the same, as shown in Figure 13-1.
CHAPTER 13: Getting Fancy with Lists
Figure 13-1. The StaticDemo application
A Dynamic Presentation As shown in the previous section, the technique of supplying an alternate layout to use for rows handles simple cases very nicely. However, what if we want the icon to change based on the row data? For example, suppose we want to use one icon for small words and a different icon for large words. In the case of ArrayAdapter, we will need to extend it, creating our own custom subclass (e.g., IconicAdapter) that incorporates our business logic. In particular, it will need to override getView(). The getView() method of an Adapter is what an AdapterView (like ListView or Spinner) calls when it needs the View associated with a given piece of data the Adapter is managing. In the case of an ArrayAdapter, getView() is called as needed for each position in the array—“get me the View for the first row,” “get me the View for the second row,” and so forth. As an example, let’s rework the code in the preceding section to use getView(), so we can show different icons for different rows—in this case, one icon for short words and one for long words (from the FancyLists/Dynamic sample project): public class DynamicDemo extends ListActivity { TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante",
121
122
CHAPTER 13: Getting Fancy with Lists
"porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new IconicAdapter()); selection=(TextView)findViewById(R.id.selection); } public void onListItemClick(ListView parent, View v, int position, long id) { selection.setText(items[position]); } class IconicAdapter extends ArrayAdapter { IconicAdapter() { super(DynamicDemo.this, R.layout.row, R.id.label, items); } public View getView(int position, View convertView, ViewGroup parent) { View row=super.getView(position, convertView, parent); ImageView icon=(ImageView)row.findViewById(R.id.icon); if (items[position].length()>4) { icon.setImageResource(R.drawable.delete); } else { icon.setImageResource(R.drawable.ok); } return(row); } } }
Our IconicAdapter—an inner class of the activity—has two methods. First, it has the constructor, which simply passes to ArrayAdapter the same data we used in the ArrayAdapter constructor in StaticDemo. Second, it has our getView() implementation, which does two things:
It chains to the superclass’s implementation of getView(), which returns to us an instance of our row View, as prepared by ArrayAdapter. In particular, our word has already been put into the TextView, since ArrayAdapter does that normally.
It finds our ImageView and applies a business rule to set which icon should be used, referencing one of two drawable resources (R.drawable.ok and R.drawable.delete).
The result of our revised example is shown in Figure 13-2.
CHAPTER 13: Getting Fancy with Lists
Figure 13-2. The DynamicDemo application
Inflating Rows Ourselves The preceding version of the DynamicDemo application works fine. However, sometimes ArrayAdapter cannot be used even to set up the basics of our row. For example, it is possible to have a ListView where the rows are materially different, such as category headers interspersed among regular rows. In that case, we may need to do all the work ourselves, starting with inflating our rows. We will do that after a brief introduction to inflation.
A Sidebar About Inflation “Inflation” means the act of converting an XML layout specification into the actual tree of View objects the XML represents. This is undoubtedly a tedious bit of code: take an element, create an instance of the specified View class, walk the attributes, convert those into property setter calls, iterate over all child elements, lather, rinse, and repeat. The good news is that the fine folks on the Android team wrapped up all that into a class called LayoutInflater, which we can use ourselves. When it comes to fancy lists, for example, we want to inflate a View for each row shown in the list, so we can use the convenient shorthand of the XML layout to describe what the rows are supposed to look like. For example, let’s look at a slightly different implementation of the DynamicDemo class, from the FancyLists/DynamicEx project:
123
124
CHAPTER 13: Getting Fancy with Lists
public class DynamicDemo extends ListActivity { TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new IconicAdapter()); selection=(TextView)findViewById(R.id.selection); } public void onListItemClick(ListView parent, View v, int position, long id) { selection.setText(items[position]); } class IconicAdapter extends ArrayAdapter { IconicAdapter() { super(DynamicDemo.this, R.layout.row, items); } public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater=getLayoutInflater(); View row=inflater.inflate(R.layout.row, parent, false); TextView label=(TextView)row.findViewById(R.id.label); label.setText(items[position]); ImageView icon=(ImageView)row.findViewById(R.id.icon); if (items[position].length()>4) { icon.setImageResource(R.drawable.delete); } else { icon.setImageResource(R.drawable.ok); } return(row); } } }
Here we inflate our R.layout.row layout by use of a LayoutInflater object, obtained from our Activity via getLayoutInflater(). This gives us a View object back, which, in reality, is our LinearLayout with an ImageView and a TextView, just as R.layout.row specifies. However, rather than having to create all those objects ourselves and wire them together, the XML and LayoutInflater handle the “heavy lifting” for us.
CHAPTER 13: Getting Fancy with Lists
And Now, Back to Our Story So we have used LayoutInflater to give us a View representing the row. This row is “empty,” since the static layout file has no idea what actual data goes into the row. It is our job to customize and populate the row as we see fit before returning it, as follows:
Fill in the text label for our label widget, using the word at the supplied position
See if the word is longer than four characters and, if so, find our ImageView icon widget and replace the stock resource with a different one
The user sees nothing different—we have simply changed how those rows are being created. Obviously, this was a fairly contrived example, but you can see that this technique could be used to customize rows based on any sort of criteria.
Better. Stronger. Faster. The getView() implementation shown in the FancyLists/DynamicEx project works, but it’s inefficient. Every time the user scrolls, we have to create a bunch of new View objects to accommodate the newly shown rows. This is bad. It might be bad for the immediate user experience, if the list appears to be sluggish. More likely, though, it will be bad due to battery usage—every bit of CPU that is used eats up the battery. This is compounded by the extra work the garbage collector needs to do to get rid of all those extra objects we create. So the less efficient our code, the more quickly the phone’s battery will be drained, and the less happy the user will be. And we want happy users, right? So, let’s take a look at a few tricks to make our fancy ListView widgets more efficient.
Using convertView The getView() method receives, as one of its parameters, a View named, by convention, convertView. Sometimes, convertView will be null. In those cases, we need to create a new row View from scratch (e.g., via inflation), just as we did in the previous example. However, if convertView is not null, then it is actually one of our previously created View objects! This will happen primarily when the user scrolls the ListView. As new rows appear, Android will attempt to recycle the views of the rows that scrolled off the other end of the list, to save us from having to rebuild them from scratch. Assuming that each of our rows has the same basic structure, we can use findViewById() to get at the individual widgets that make up our row and change their contents, and then return convertView from getView(), rather than create a whole new row. For example, here is the getView() implementation from the earlier example, now optimized via convertView (from the FancyLists/Recycling project):
125
126
CHAPTER 13: Getting Fancy with Lists
public class RecyclingDemo extends ListActivity { private TextView selection; private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new IconicAdapter()); selection=(TextView)findViewById(R.id.selection); } public void onListItemClick(ListView parent, View v, int position, long id) { selection.setText(items[position]); } class IconicAdapter extends ArrayAdapter { IconicAdapter() { super(RecyclingDemo.this, R.layout.row, items); } public View getView(int position, View convertView, ViewGroup parent) { View row=convertView; if (row==null) { LayoutInflater inflater=getLayoutInflater(); row=inflater.inflate(R.layout.row, parent, false); } TextView label=(TextView)row.findViewById(R.id.label); label.setText(items[position]); ImageView icon=(ImageView)row.findViewById(R.id.icon); if (items[position].length()>4) { icon.setImageResource(R.drawable.delete); } else { icon.setImageResource(R.drawable.ok); } return(row); } } }
CHAPTER 13: Getting Fancy with Lists
Here, we check to see if the convertView is null. If so, we inflate our row; otherwise, we just reuse it. The work to fill in the contents (icon image and text) is the same in either case. The advantage is that we avoid the potentially expensive inflation step. In fact, according to statistics cited by Google at the 2010 Google I|O conference, a ListView that uses a recycling ListAdapter will perform 150 percent faster than one that does not. For complex rows, that might even understate the benefit. Not only is this faster, but it uses much less memory. Each widget or container—in other words, each subclass of View—holds onto up to 2KB of data, not counting things like images in ImageView widgets. Each of our rows, therefore, might be as big as 6KB. For our list of 25 nonsense words, consuming as much as 150KB for a nonrecycling list (25 rows at 6KB each) would be inefficient but not a huge problem. A list of 1000 nonsense words, though, consuming as much as 6MB of RAM, would be a much bigger issue. Bear in mind that your application may have only 16MB of Java heap memory to work with. Recycling allows us to handle arbitrary list lengths with only as much View memory consumed as is needed for the rows visible onscreen. Note that row recycling is an issue only if we are creating the rows ourselves. If we let ArrayAdapter create the rows, by leveraging its implementation of getView(), as shown in the FancyLists/Dynamic project, then it deals with the recycling.
Using the Holder Pattern Another somewhat expensive operation commonly done with fancy views is calling findViewById(). This dives into our inflated row and pulls out widgets by their assigned identifiers, so we can customize the widget contents (e.g., to change the text of a TextView or change the icon in an ImageView). Since findViewById() can find widgets anywhere in the tree of children of the row’s root View, this could take a fair number of instructions to execute, particularly if we need to find the same widgets repeatedly. In some GUI toolkits, this problem is avoided by having the composite View objects, like rows, be declared totally in program code (in this case, Java). Then, accessing individual widgets is merely a matter of calling a getter or accessing a field. And we can certainly do that with Android, but the code gets rather verbose. What would be nice is a way that enables us still to use the layout XML, yet cache our row’s key child widgets so that we need to find them only once. That’s where the holder pattern comes into play, in a class we’ll call ViewHolder. All View objects have getTag() and setTag() methods. These allow us to associate an arbitrary object with the widget. The holder pattern uses that “tag” to hold an object that, in turn, holds each of the child widgets of interest. By attaching that holder to the row View, every time we use the row, we already have access to the child widgets we care about, without having to call findViewById() again. So, let’s take a look at one of these holder classes (taken from the FancyLists/ViewHolder sample project):
127
128
CHAPTER 13: Getting Fancy with Lists
package com.commonsware.android.fancylists.five; import android.view.View; import android.widget.ImageView; class ViewHolder { ImageView icon=null; ViewHolder(View base) { this.icon=(ImageView)base.findViewById(R.id.icon); } }
ViewHolder holds onto the child widgets, initialized via findViewById() in its constructor. The widgets are simply package-protected data members, accessible from other classes in this project, such as a ViewHolderDemo activity. In this case, we are holding onto only one widget—the icon—since we will let ArrayAdapter handle our label for us. Using ViewHolder is a matter of creating an instance whenever we inflate a row and attaching said instance to the row View via setTag(), as shown in this rewrite of getView(), found in ViewHolderDemo: public View getView(int position, View convertView, ViewGroup parent) { View row=super.getView(position, convertView, parent); ViewHolder holder=(ViewHolder)row.getTag(); if (holder==null) { holder=new ViewHolder(row); row.setTag(holder); } if (getModel(position).length()>4) { holder.icon.setImageResource(R.drawable.delete); } else { holder.icon.setImageResource(R.drawable.ok); } return(row); }
Here, we go back to allowing ArrayAdapter to handle our row inflation and recycling for us. If the call to getTag() on the row returns null, we know we need to create a new ViewHolder, which we then attach to the row via setTag() for later reuse. Then, accessing the child widgets is merely a matter of accessing the data members on the holder. The first time the ListView is displayed, all new rows need to be inflated, and we wind up creating a ViewHolder for each. As the user scrolls, rows get recycled, and we can reuse their corresponding ViewHolder widget caches. Using a holder helps performance, but the effect is not as dramatic. Whereas recycling can give you a 150 percent performance improvement, adding in a holder increases the improvement to 175 percent. Hence, while you may wish to implement recycling up front
CHAPTER 13: Getting Fancy with Lists
when you create your adapter, adding in a holder might be something you deal with later, when you are working specifically on performance tuning. In this particular case, we certainly could simplify all of this by skipping ViewHolder and using getTag() and setTag() with the ImageView directly. This example is written as it is to demonstrate how to handle a more complex scenario, where you might have several widgets that would need to be cached via the holder pattern.
Interactive Rows Lists with pretty icons next to them are all fine and well. But, can we create ListView widgets whose rows contain interactive child widgets instead of just passive widgets like TextView and ImageView? For example, there is a RatingBar widget that allows users to assign a rating by clicking on a set of star icons. Could we combine the RatingBar with text to allow people to scroll a list of, say, songs and rate them right inside the list? There is good news and bad news. The good news is that interactive widgets in rows work just fine. The bad news is that it is a little tricky, specifically when it comes to taking action when the interactive widget’s state changes (e.g., a value is typed into a field). We need to store that state somewhere, since our RatingBar widget will be recycled when the ListView is scrolled. We need to be able to set the RatingBar state based on the actual word being viewed as the RatingBar is recycled, and we need to save the state when it changes so it can be restored when this particular row is scrolled back into view. What makes this interesting is that, by default, the RatingBar has absolutely no idea which item in the ArrayAdapter it represents. After all, the RatingBar is just a widget, used in a row of a ListView. We need to teach the rows which item in the ArrayAdapter they are currently displaying, so when their RatingBar is checked, they know which item’s state to modify. So, let’s see how this is done, using the activity in the FancyLists/RateList sample project. We will use the same basic classes that we used in our previous example. We are displaying a list of nonsense words, which can then be rated. In addition, words given a top rating are put in all caps. package com.commonsware.android.fancylists.six; import import import import import import import import import import import import import
android.app.Activity; android.os.Bundle; android.app.ListActivity; android.view.View; android.view.ViewGroup; android.view.LayoutInflater; android.widget.AdapterView; android.widget.ArrayAdapter; android.widget.RatingBar; android.widget.LinearLayout; android.widget.ListView; android.widget.TextView; java.util.ArrayList;
129
130
CHAPTER 13: Getting Fancy with Lists
public class RateListDemo extends ListActivity { private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); ArrayList list=new ArrayList(); for (String s : items) { list.add(new RowModel(s)); } setListAdapter(new RatingAdapter(list)); } private RowModel getModel(int position) { return(((RatingAdapter)getListAdapter()).getItem(position)); } class RatingAdapter extends ArrayAdapter { RatingAdapter(ArrayList list) { super(RateListDemo.this, R.layout.row, R.id.label, list); } public View getView(int position, View convertView, ViewGroup parent) { View row=super.getView(position, convertView, parent); ViewHolder holder=(ViewHolder)row.getTag(); if (holder==null) { holder=new ViewHolder(row); row.setTag(holder); RatingBar.OnRatingBarChangeListener l= new RatingBar.OnRatingBarChangeListener() { public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromTouch) { Integer myPosition=(Integer)ratingBar.getTag(); RowModel model=getModel(myPosition); model.rating=rating; LinearLayout parent=(LinearLayout)ratingBar.getParent(); TextView label=(TextView)parent.findViewById(R.id.label); label.setText(model.toString()); } };
131
CHAPTER 13: Getting Fancy with Lists
holder.rate.setOnRatingBarChangeListener(l); } RowModel model=getModel(position); holder.rate.setTag(new Integer(position)); holder.rate.setRating(model.rating); return(row); } } class RowModel { String label; float rating=2.0f; RowModel(String label) { this.label=label; } public String toString() { if (rating>=3.0) { return(label.toUpperCase()); } return(label); } } }
The following explains what is different in this activity and getView() implementation from before:
We are still using String[] items as the list of nonsense words, but instead of pouring that String array straight into an ArrayAdapter, we turn it into a list of RowModel objects. RowModel is the mutable model: it holds the nonsense word plus the current checked state. In a real system, these might be objects populated from a database, and the properties would have more business meaning.
We updated utility methods such as onListItemClick()to reflect the change from a pure-String model to use a RowModel.
The ArrayAdapter subclass (RatingAdapter), in getView(), lets ArrayAdapter inflate and recycle the row, and then checks to see if we have a ViewHolder in the row’s tag. If not, we create a new ViewHolder and associate it with the row. For the row’s RatingBar, we add an anonymous onRatingChanged() listener that looks at the row’s tag (getTag()) and converts that into an Integer, representing the position within the ArrayAdapter that this row is displaying. Using that, the rating bar can get the actual RowModel for the row and update the model based on the new state of the rating bar. It also updates the text adjacent to the RatingBar when checked, to match the rating bar state.
v
132
CHAPTER 13: Getting Fancy with Lists
We always make sure that the RatingBar has the proper contents and has a tag (via setTag()) pointing to the position in the adapter the row is displaying.
The row layout is very simple, just a RatingBar and a TextView inside a LinearLayout:
The ViewHolder is similarly simple, just extracting the RatingBar out of the row View for caching purposes: package com.commonsware.android.fancylists.six; import android.view.View; import android.widget.RatingBar; class ViewHolder { RatingBar rate=null; ViewHolder(View base) { this.rate=(RatingBar)base.findViewById(R.id.rate); } }
And the result is what you would expect, visually, as shown in Figure 13-3.
CHAPTER 13: Getting Fancy with Lists
Figure 13-3. The RateListDemo application, as initially launched
Figure 13-4 shows a toggled rating bar turning its word into all caps.
Figure 13-4. The same application, showing a top-rated word
133
14
Chapter
Still More Widgets and Containers This book has covered a number of widgets and containers so far. This chapter is the last that focuses exclusively on widgets and containers, covering a number of popular options, from date and time widgets to tabs. After this chapter, we introduce new widgets occasionally, but in the context of some other topic, such as introducing the ProgressBar in Chapter 20 (covering threads).
Pick and Choose With limited-input devices like phones, having widgets and dialogs that are aware of the type of stuff a user is supposed to be entering is very helpful. They minimize keystrokes and screen taps and reduce the chance that a user will make some sort of error (e.g., entering a letter somewhere only numbers are expected). As shown in Chapter 9, EditText has content-aware flavors for entering numbers and text. Android also supports widgets (DatePicker and TimePicker) and dialogs (DatePickerDialog and TimePickerDialog) for helping users enter dates and times. DatePicker and DatePickerDialog allow you to set the starting date for the selection, in the form of a year, month, and day of month value. Note that the month runs from 0 for January through 11 for December. Most importantly, both let you provide a callback object (OnDateChangedListener or OnDateSetListener) where you are informed of a new date selected by the user. It is up to you to store that date someplace, particularly if you are using the dialog, since there is no other way for you to access the chosen date later. Similarly, TimePicker and TimePickerDialog let you do the following:
Set the initial time the user can adjust, in the form of an hour (0 through 23) and a minute (0 through 59)
135
136
CHAPTER 14: Still More Widgets and Containers
Indicate if the selection should be in 12-hour mode with an AM/PM toggle or in 24-hour mode (what is thought of in the United States as “military time” and in much of the rest of the world as “the way times are supposed to be”)
Provide a callback object (OnTimeChangedListener or OnTimeSetListener) to be notified of when the user has chosen a new time, which is supplied to you in the form of an hour and minute
As an example of using date and time pickers, from the Fancy/Chrono sample project, here’s a trivial layout containing a label and two buttons, which will pop up the dialog flavors of the date and time pickers:
The more interesting stuff comes in the Java source: package com.commonsware.android.chrono; import import import import import import import import import import
android.app.Activity; android.os.Bundle; android.app.DatePickerDialog; android.app.TimePickerDialog; android.view.View; android.widget.DatePicker; android.widget.TimePicker; android.widget.TextView; java.text.DateFormat; java.util.Calendar;
public class ChronoDemo extends Activity { DateFormat fmtDateAndTime=DateFormat.getDateTimeInstance(); TextView dateAndTimeLabel;
CHAPTER 14: Still More Widgets and Containers
Calendar dateAndTime=Calendar.getInstance(); @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); dateAndTimeLabel=(TextView)findViewById(R.id.dateAndTime); updateLabel(); } public void chooseDate(View v) { new DatePickerDialog(ChronoDemo.this, d, dateAndTime.get(Calendar.YEAR), dateAndTime.get(Calendar.MONTH), dateAndTime.get(Calendar.DAY_OF_MONTH)) .show(); } public void chooseTime(View v) { new TimePickerDialog(ChronoDemo.this, t, dateAndTime.get(Calendar.HOUR_OF_DAY), dateAndTime.get(Calendar.MINUTE), true) .show(); } private void updateLabel() { dateAndTimeLabel.setText(fmtDateAndTime .format(dateAndTime.getTime())); } DatePickerDialog.OnDateSetListener d=new DatePickerDialog.OnDateSetListener() { public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { dateAndTime.set(Calendar.YEAR, year); dateAndTime.set(Calendar.MONTH, monthOfYear); dateAndTime.set(Calendar.DAY_OF_MONTH, dayOfMonth); updateLabel(); } }; TimePickerDialog.OnTimeSetListener t=new TimePickerDialog.OnTimeSetListener() { public void onTimeSet(TimePicker view, int hourOfDay, int minute) { dateAndTime.set(Calendar.HOUR_OF_DAY, hourOfDay); dateAndTime.set(Calendar.MINUTE, minute); updateLabel(); } }; }
137
138
CHAPTER 14: Still More Widgets and Containers
The model for this activity is just a Calendar instance, initially set to be the current date and time. We pour it into the view via a DateFormat formatter. In the updateLabel() method, we take the current Calendar, format it, and put it in the TextView. Each button has a corresponding method that will get control when the user clicks it (chooseDate() and chooseTime()). When the button is clicked, either a DatePickerDialog or a TimePickerDialog is shown. In the case of the DatePickerDialog, we give it an OnDateSetListener callback that updates the Calendar with the new date (year, month, and day of month). We also give the dialog the last-selected date, getting the values from the Calendar. In the case of the TimePickerDialog, it gets an OnTimeSetListener callback to update the time portion of the Calendar, the last-selected time, and a true indicating we want 24-hour mode on the time selector. With all this wired together, the resulting activity looks like Figures 14–1, 14–2, and 14–3.
Figure 14–1. The ChronoDemo sample application, as initially launched
CHAPTER 14: Still More Widgets and Containers
Figure 14–2. The same application, showing the date picker dialog
Figure 14–3. The same application, showing the time picker dialog
139
140
CHAPTER 14: Still More Widgets and Containers
Time Keeps Flowing Like a River If you want to display the time, rather than have users enter the time, you may wish to use the DigitalClock widget or AnalogClock widget. These widgets are extremely easy to use, as they automatically update with the passage of time. All you need to do is put them in your layout and let them do their thing. For example, from the Fancy/Clocks sample application, here is an XML layout containing both DigitalClock and AnalogClock:
Without any Java code other than the generated stub, we can build this project and get the activity shown in Figure 14–4.
Figure 14–4. The ClocksDemo sample application
CHAPTER 14: Still More Widgets and Containers
If you are looking for more of a timer, Chronometer may be of interest. With a Chronometer, you can track elapsed time from a starting point, as shown in the example in Figure 14–5. You simply tell it when to start() and stop(), and possibly override the format string that displays the text.
Figure 14–5. The Views/Chronometer API Demo from the Android 2.0 SDK
Seeking Resolution The SeekBar is an input widget that allows the user to select a value along a range of possible values. Figure 14–6 shows an example.
141
142
CHAPTER 14: Still More Widgets and Containers
Figure 14–6. The Views/SeekBar API Demo from the Android 2.0 SDK
The user can either drag the thumb or click on either side of the thumb to reposition it. The thumb then points to a particular value along a range. That range will be 0 to some maximum value, 100 by default, which you control via a call to setMax(). You can find out what the current position is via getProgress(), or find out when the user makes a change to the thumb’s position by registering a listener via setOnSeekBarChangeListener(). We saw a variation on this theme with the RatingBar example in Chapter 13.
Putting It on My Tab The general Android philosophy is to keep activities short and sweet. If there is more information than can reasonably fit on one screen, albeit perhaps with scrolling, then it perhaps belongs in another activity kicked off via an Intent, as will be described in Chapter 22. However, that can be complicated to set up. Moreover, sometimes there legitimately is a lot of information that needs to be collected to be processed as an atomic operation. In a traditional UI, you might use tabs to collect and display information, such as a JTabbedPane in Java/Swing. In Android, you now have the option of using a TabHost container in much the same way. A portion of your activity’s screen is taken up with tabs, which, when clicked, swap out part of the view and replace it with something else. For example, you might have an activity with a tab for entering a location and a second tab for showing a map of that location. Some GUI toolkits refer to “tabs” as only the things that a user clicks to toggle from one view to another. Other GUI toolkits refer to “tabs” as the combination of the clickable
CHAPTER 14: Still More Widgets and Containers
button-like element and the content that appears when that element is chosen. Android treats the tab buttons and contents as discrete entities, so they are referred to as “tab buttons” and “tab contents” in this section.
The Pieces You use the following widgets and containers to set up a tabbed portion of a view:
TabHost: The overarching container for the tab buttons and tab contents.
TabWidget: Implements the row of tab buttons, which contain text labels and, optionally, icons.
FrameLayout: The container for the tab contents. Each tab content is a child of the FrameLayout.
This is similar to the approach that Mozilla’s XUL takes. In XUL’s case, the tabbox element corresponds to Android’s TabHost, the tabs element corresponds to TabWidget, and tabpanels corresponds to FrameLayout. For example, here is a layout definition for a tabbed activity, from Fancy/Tab:
Note that the TabWidget and FrameLayout are indirect children of the TabHost, and the FrameLayout itself has children representing the various tabs. In this case, there are two
143
144
CHAPTER 14: Still More Widgets and Containers
tabs: a clock and a button. In a more complicated scenario, the tabs could be some form of container (e.g., LinearLayout) with their own contents.
Wiring It Together You can put these widgets in a regular Activity or a TabActivity. TabActivity, like ListActivity, wraps a common UI pattern (an activity made up entirely of tabs) into a pattern-aware activity subclass. If you wish to use the TabActivity, you must give the TabHost an android:id of @android:id/tabhost. Conversely, if you do not wish to use TabActivity, you need to get your TabHost via findViewById(), and then call setup() on the TabHost, before you do anything else. The rest of the Java code needs to tell the TabHost which views represent the tab contents and what the tab buttons should look like. This is all wrapped up in TabSpec objects. You get a TabSpec instance from the host via newTabSpec(), fill it out, and then add it to the host in the proper sequence. TabSpec has two key methods:
setContent(): Indicates what goes in the tab content for this tab, typically the android:id of the view you want shown when this tab is selected
setIndicator(): Sets the caption for the tab button and, in some flavors of this method, supplies a Drawable to represent the icon for the tab
Note that tab “indicators” can actually be views in their own right, if you need more control than a simple label and optional icon. Also note that you must call setup() on the TabHost before configuring any of these TabSpec objects. The call to setup() is not needed if you are using the TabActivity base class for your activity. For example, here is the Java code to wire together the tabs from the preceding layout example: package com.commonsware.android.fancy; import android.app.Activity; import android.os.Bundle; import android.widget.TabHost; public class TabDemo extends Activity { @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); TabHost tabs=(TabHost)findViewById(R.id.tabhost); tabs.setup();
CHAPTER 14: Still More Widgets and Containers
TabHost.TabSpec spec=tabs.newTabSpec("tag1"); spec.setContent(R.id.tab1); spec.setIndicator("Clock"); tabs.addTab(spec); spec=tabs.newTabSpec("tag2"); spec.setContent(R.id.tab2); spec.setIndicator("Button"); tabs.addTab(spec); } }
We find our TabHost via the familiar findViewById() method, and then have it setup(). After that, we get a TabSpec via newTabSpec(), supplying a tag whose purpose is unknown at this time. Given the spec, we call setContent() and setIndicator(), and then call addTab() back on the TabHost to register the tab as available for use. Finally, we can choose which tab is the one to show via setCurrentTab(), providing the 0-based index of the tab. The results are shown in Figures 14–7 and 14–8.
Figure 14–7. The TabDemo sample application, showing the first tab
145
146
CHAPTER 14: Still More Widgets and Containers
Figure 14–8. The same application, showing the second tab
Adding Them Up TabWidget is set up to allow you to easily define tabs at compile time. However, sometimes you may want to add tabs to your activity during runtime. For example, imagine an e-mail client where individual e-mail messages are opened in their own tab, for easy toggling between messages. In this case, you do not know how many tabs you will need or what their contents will be until runtime, when the user chooses to open a message. Fortunately, Android also supports adding tabs dynamically at runtime. Adding tabs dynamically at runtime works much like the compile-time tabs previous described, except you use a different flavor of setContent(), one that takes a TabHost.TabContentFactory instance. This is just a callback that will be invoked. You provide an implementation of createTabContent() and use it to build and return the View that becomes the content of the tab. Let’s take a look at an example (Fancy/DynamicTab). First, here is some layout XML for an activity that sets up the tabs and defines one tab, containing a single button:
We want to add new tabs whenever the button is clicked, which we can accomplish with the following code: package com.commonsware.android.dynamictab; import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.widget.AnalogClock; android.widget.TabHost;
public class DynamicTabDemo extends Activity { private TabHost tabs=null; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); tabs=(TabHost)findViewById(R.id.tabhost); tabs.setup(); TabHost.TabSpec spec=tabs.newTabSpec("buttontab"); spec.setContent(R.id.buttontab); spec.setIndicator("Button"); tabs.addTab(spec); } public void addTab(View v) { TabHost.TabSpec spec=tabs.newTabSpec("tag1"); spec.setContent(new TabHost.TabContentFactory() { public View createTabContent(String tag) { return(new AnalogClock(DynamicTabDemo.this)); } }); spec.setIndicator("Clock"); tabs.addTab(spec); } }
147
148
CHAPTER 14: Still More Widgets and Containers
In our button’s addTab() callback, we create a TabHost.TabSpec object and give it an anonymous TabHost.TabContentFactory. The factory, in turn, returns the View to be used for the tab—in this case, just an AnalogClock. The logic for constructing the tab’s View could be much more elaborate, such as using LayoutInflater to construct a view from layout XML. Initially, when the activity is launched, we just have the one tab, as shown in Figure 14–9. Figure 14–10 shows the three dynamically created tabs.
Figure 14–9. The DynamicTab application, with the single initial tab
CHAPTER 14: Still More Widgets and Containers
Figure 14–10. The DynamicTab application, with three dynamically created tabs
Flipping Them Off Sometimes, you want the overall effect of tabs (only some Views visible at a time) but not the actual UI implementation of tabs. Maybe the tabs take up too much screen space. Maybe you want to switch between perspectives based on a gesture or a device shake. Or maybe you just like being different. The good news is that the guts of the view-flipping logic from tabs can be found in the ViewFlipper container, which can be used in other ways than the traditional tab. ViewFlipper inherits from FrameLayout, in the same way we use it to describe the innards of a TabWidget. However, initially, ViewFlipper just shows the first child view. It is up to you to arrange for the views to flip, either manually by user interaction or automatically via a timer. For example, here is a layout for a simple activity (Fancy/Flipper1) using a Button and a ViewFlipper:
149
150
CHAPTER 14: Still More Widgets and Containers
Notice that the layout defines three child views for the ViewFlipper, each a TextView with a simple message. Of course, you could have very complicated child views, if you so chose. To manually flip the views, we need to hook into the Button and flip them ourselves when the button is clicked: package com.commonsware.android.flipper1; import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.widget.ViewFlipper;
public class FlipperDemo extends Activity { ViewFlipper flipper; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); flipper=(ViewFlipper)findViewById(R.id.details); } public void flip(View v) { flipper.showNext(); } }
CHAPTER 14: Still More Widgets and Containers
This is just a matter of calling showNext() on the ViewFlipper, as you can on any ViewAnimator class. The result is a trivial activity: click the button, and the next TextView in sequence is displayed, wrapping around to the first after viewing the last, as shown in Figures 14–11 and 14–12.
Figure 14–11. The Flipper1 application, showing the first panel
Figure 14–12. The same application, after switching to the second panel
151
152
CHAPTER 14: Still More Widgets and Containers
Of course, this could be handled more simply by having a single TextView and changing the text and color on each click. However, you can imagine that the ViewFlipper contents could be much more complicated, like the contents you might put into a TabView. As with the TabWidget, sometimes your ViewFlipper contents may not be known at compile time. And as with TabWidget, you can add new contents on-the-fly with ease. For example, let’s look at another sample activity (Fancy/Flipper2), using this layout:
Notice that the ViewFlipper has no contents at compile time. Also notice that there is no Button for flipping between the contents—more on this in a moment. For the ViewFlipper contents, we will create large Button widgets, each containing one of the random words used in many chapters in this book. And, we will set up the ViewFlipper to automatically rotate between the Button widgets. package com.commonsware.android.flipper2; import import import import import import
android.app.Activity; android.os.Bundle; android.view.View; android.view.ViewGroup; android.widget.Button; android.widget.ViewFlipper;
public class FlipperDemo2 extends Activity { static String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; ViewFlipper flipper; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); flipper=(ViewFlipper)findViewById(R.id.details); for (String item : items) {
CHAPTER 14: Still More Widgets and Containers
Button btn=new Button(this); btn.setText(item); flipper.addView(btn, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); } flipper.setFlipInterval(2000); flipper.startFlipping(); } }
After iterating over the funky words, turning each into a Button, and adding the Button as a child of the ViewFlipper, we set up the flipper to automatically flip between children (flipper.setFlipInterval(2000);) and to start flipping (flipper.startFlipping();). The result is an endless series of buttons, each of which appears, as shown in Figure 14–13, and then is replaced by the next button in sequence after 2 seconds, wrapping around to the first after the last has been shown.
Figure 14–13. The Flipper2 application
The autoflipping ViewFlipper is useful for status panels or other situations where you have a lot of information to display, but not much room. However, since it automatically flips between views, expecting users to interact with individual views is dicey, because the view might switch away partway through their interaction.
153
154
CHAPTER 14: Still More Widgets and Containers
Getting in Somebody’s Drawer For a long time, Android developers yearned for a sliding-drawer container that worked like the one on the home screen, containing the icons for launching applications. The official implementation was in the open source code but was not part of the SDK, until Android 1.5, when the developers released SlidingDrawer for others to use. Unlike most other Android containers, SlidingDrawer moves, switching from a closed to an open position. This puts some restrictions on which container can hold the SlidingDrawer. It needs to be in a container that allows multiple widgets to sit atop each other. RelativeLayout and FrameLayout satisfy this requirement. FrameLayout is a container purely for stacking widgets atop one another. On the flip side, LinearLayout does not allow widgets to stack (they fall one after another in a row or column), and so you should not have a SlidingDrawer as an immediate child of a LinearLayout. Here is a layout showing a SlidingDrawer in a FrameLayout, from the Fancy/DrawerDemo project:
The SlidingDrawer should contain two things:
A handle, frequently an ImageView or something along those lines, such as the one used here, pulled from the Android open source project
The contents of the drawer itself, usually some sort of container, but a Button in this example
CHAPTER 14: Still More Widgets and Containers
Moreover, SlidingDrawer needs to know the android:id values of the handle and contents, via the android:handle and android:content attributes, respectively. These tell the drawer how to animate itself as it slides open and closed. Figure 14–14 shows what the SlidingDrawer looks like closed, using the supplied handle, and Figure 14–15 shows what it looks like open.
Figure 14–14. A SlidingDrawer, closed
Figure 14–15. A SlidingDrawer, open
155
156
CHAPTER 14: Still More Widgets and Containers
As you might expect, you can open and close the drawer from Java code, as well as via user touch events. However, you have two sets of these methods: ones that take place instantaneously (open(),close(), and toggle()) and ones that use the animation (animateOpen(),animateClose(), and animateToggle()). You can also lock() and unlock() the drawer; while locked, the drawer will not respond to touch events. You can also register three types of callbacks if you wish:
A listener to be invoked when the drawer is opened
A listener to be invoked when the drawer is closed
A listener to be invoked when the drawer is “scrolled” (i.e., the user drags or flings the handle)
For example, the Android launcher’s SlidingDrawer toggles the icon on the handle from open to closed to “delete” (if you long-tap something on the desktop). It accomplishes this, in part, through callbacks like these. SlidingDrawer can be vertical or horizontal. Note, though, that it keeps its orientation despite the screen orientation. In other words, if you rotate the Android device or emulator running DrawerDemo, the drawer always opens from the bottom—it does not always “stick” to the original side it opened from. This means that if you want the drawer to always open from the same side, like the launcher does, you will need separate layouts for portrait versus landscape, a topic discussed in Chapter 23.
Other Good Stuff Android offers AbsoluteLayout, where the contents are laid out based on specific coordinate positions. You tell AbsoluteLayout where to place a child in precise x and y coordinates, and Android puts it there, no questions asked. On the plus side, this gives you precise positioning. On the minus side, it means your views will look right only on screens of a certain dimension, or you will need to write a bunch of code to adjust the coordinates based on screen size. Since Android screens might run the gamut of sizes, with new sizes cropping up periodically, using AbsoluteLayout could get quite annoying. Also, note that AbsoluteLayout is officially deprecated, meaning that although it is available to you, its use is discouraged. Android also has the ExpandableListView. This provides a simplified tree representation, supporting two levels of depth: groups and children. Groups contain children; children are “leaves” of the tree. This requires a new set of adapters, since the ListAdapter family does not provide any sort of group information for the items in the list. Here are some other widgets available in Android beyond those covered so far in this book:
CheckedTextView: A TextView that can have either a check box or a radio button next to it, used with single- and multiple-choice lists
Chronometer: A stopwatch-style countdown timer
CHAPTER 14: Still More Widgets and Containers
Gallery: A horizontal scrolling selection widget, designed for thumbnail previews of images (e.g., camera photos and album covers)
MultiAutoCompleteTextView: Like an AutoCompleteTextView, except that the user can make multiple choices from the drop-down list, rather than just one
QuickContactBadge: Given the identity of a contact from the user’s contacts database, displays a roster of icons representing actions to be performed on that contact (place a call, send a text message, send an e-mail, etc.)
ToggleButton: A two-state button where the states are indicated by a “light” and prose ("ON", "OFF") instead of a check mark
ViewSwitcher (and the ImageSwitcher and TextSwitcher subclasses): Like a simplified ViewFlipper for toggling between two views
157
15
Chapter
Embedding the WebKit Browser Other GUI toolkits let you use HTML for presenting information, from limited HTML renderers (e.g., Java/Swing and wxWidgets) to embedding Internet Explorer into .NET applications. Android is much the same, in that you can embed the built-in web browser as a widget in your own activities, for displaying HTML or full-fledged browsing. The Android browser is based on WebKit, the same engine that powers Apple’s Safari web browser. The Android browser is sufficiently complex that it gets its own Java package (android.webkit). Using the WebView widget itself can be simple or powerful, based on your requirements.
A Browser, Writ Small For simple stuff, WebView is not significantly different from any other widget in Android— pop it into a layout, tell it which URL to navigate to via Java code, and you are finished. For example, here is a simple layout with a WebView (from WebKit/Browser1):
As with any other widget, you need to tell it how it should fill up the space in the layout (in this case, it fills all remaining space). The Java code is equally simple: package com.commonsware.android.browser1; import android.app.Activity;
159
160
CHAPTER 15: Embedding the WebKit Browser
import android.os.Bundle; import android.webkit.WebView; public class BrowserDemo1 extends Activity { WebView browser; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); browser=(WebView)findViewById(R.id.webkit); browser.loadUrl("http://commonsware.com"); } }
The only thing unusual with this edition of onCreate() is that we invoke loadUrl() on the WebView widget, to tell it to load a web page (in this case, the home page of some random firm). However, we also need to make one change to AndroidManifest.xml, requesting permission to access the Internet:
If we fail to add this permission, the browser will refuse to load pages. Permissions will be covered in greater detail in Chapter 38. The resulting activity looks like a web browser, but with hidden scrollbars, as shown in Figure 15–1.
CHAPTER 15: Embedding the WebKit Browser
Figure 15–1. The BrowserDemo1 sample application
As with the regular Android browser, you can pan around the page by dragging it, while the D-pad moves you around all the focusable elements on the page. What is missing is all the extra stuff that make up a web browser, such as a navigational toolbar. Now, you may be tempted to replace the URL in that source code with something that relies on JavaScript, such as Google’s home page. By default, JavaScript is turned off in WebView widgets. If you want to enable JavaScript, call getSettings().setJavaScriptEnabled(true); on the WebView instance. This option is covered in a bit more detail later in this chapter.
Loading It Up There are two main ways to get content into the WebView. One, described in the previous section, is to provide the browser with a URL and have the browser display that page via loadUrl(). The browser will access the Internet through whatever means are available to that specific device at the present time (Wi-Fi, 2G, 3G, WiMAX, well-trained tiny carrier pigeons, etc.). The alternative is to use loadData(). Here, you supply the HTML for the browser to view. You might use this to do the following:
Display a manual that was installed as a file with your application package
Display snippets of HTML you retrieved as part of other processing, such as the description of an entry in an Atom feed
161
162
CHAPTER 15: Embedding the WebKit Browser
Generate a whole user interface using HTML, instead of using the Android widget set
There are two flavors of loadData(). The simpler one allows you to provide the content, the MIME type, and the encoding, all as strings. Typically, your MIME type will be text/html and your encoding will be UTF-8 for ordinary HTML. For example, you could replace the loadUrl() invocation in the previous example with the following: browser.loadData("Hello, world!", "text/html", "UTF-8");
You would get the result shown in Figure 15–2.
Figure 15–2. The BrowserDemo2 sample application
This is also available as a fully buildable sample, as WebKit/Browser2.
Navigating the Waters As previously mentioned, the WebView widget doesn’t have a navigation toolbar. This allows you to use it in places where such a toolbar would be pointless and a waste of screen real estate. That being said, if you want to offer navigational capabilities, you can, but you have to supply the UI. WebView offers ways to perform garden-variety browser navigation, including the following methods:
reload(): Refreshes the currently viewed web page
CHAPTER 15: Embedding the WebKit Browser
goBack(): Goes back one step in the browser history
canGoBack(): Determines if there is any history to go back to
goForward(): Goes forward one step in the browser history
canGoForward(): Determines if there is any history to go forward to
goBackOrForward(): Goes backward or forward in the browser history, where a negative number as an argument represents how many steps to go backward, and a positive number represents how many steps to go forward
canGoBackOrForward(): Determines if the browser can go backward or forward the stated number of steps (following the same positive/negative convention as goBackOrForward())
clearCache(): Clears the browser resource cache
clearHistory(): Clears the browsing history
Entertaining the Client If you are going to use the WebView as a local UI (versus browsing the Web), you will want to be able to get control at key times, particularly when users click links. You will want to make sure those links are handled properly, either by loading your own content back into the WebView, by submitting an Intent to Android to open the URL in a full browser, or by some other means (see Chapter 22). Your hook into the WebView activity is via setWebViewClient(), which takes an instance of a WebViewClient implementation as a parameter. The supplied callback object will be notified of a wide range of events, from when parts of a page have been retrieved (onPageStarted(), etc.) to when you, as the host application, need to handle certain user- or circumstance-initiated events, such as onTooManyRedirects() or onReceivedHttpAuthRequest(). A common hook will be shouldOverrideUrlLoading(), where your callback is passed a URL (plus the WebView itself), and you return true if you will handle the request or false if you want default handling (e.g., actually fetch the web page referenced by the URL). In the case of a feed reader application, for example, you will probably not have a full browser with navigation built into your reader. In this case, if the user clicks a URL, you probably want to use an Intent to ask Android to load that page in a full browser. But if you have inserted a “fake” URL into the HTML, representing a link to some activityprovided content, you can update the WebView yourself.
163
164
CHAPTER 15: Embedding the WebKit Browser
As an example, let’s amend the first browser demo to make it an application that, upon a click, shows the current time. From WebKit/Browser3, here is the revised Java: public class BrowserDemo3 extends Activity { WebView browser; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); browser=(WebView)findViewById(R.id.webkit); browser.setWebViewClient(new Callback()); loadTime(); } void loadTime() { String page="" +new Date().toString() +""; browser.loadData(page, "text/html", "UTF-8"); } private class Callback extends WebViewClient { public boolean shouldOverrideUrlLoading(WebView view, String url) { loadTime(); return(true); } } }
Here, we load into the browser (loadTime()) a simple web page that consists of the current time, made into a hyperlink to the /clock URL. We also attach an instance of a WebViewClient subclass, providing our implementation of shouldOverrideUrlLoading(). In this case, no matter what the URL, we want to just reload the WebView via loadTime(). Running this activity gives the result shown in Figure 15–3.
CHAPTER 15: Embedding the WebKit Browser
Figure 15–3. The BrowserDemo3 sample application
Selecting the link and clicking the D-pad center button will “click” the link, causing the page to be rebuilt with the new time.
Settings, Preferences, and Options (Oh, My!) With your favorite desktop web browser, you have some sort of settings, preferences, or options window. Between that and the toolbar controls, you can tweak and twiddle the behavior of your browser, from preferred fonts to the behavior of JavaScript. Similarly, you can adjust the settings of your WebView widget as you see fit, via the WebSettings instance returned from calling the widget’s getSettings() method. There are lots of options on WebSettings to play with. Most appear fairly esoteric (e.g., setFantasyFontFamily()). However, here are some that you may find more useful:
Control the font sizing via setDefaultFontSize() (to use a point size) or setTextSize() (to use constants indicating relative sizes like LARGER and SMALLEST)
Control JavaScript via setJavaScriptEnabled() (to disable it outright) and setJavaScriptCanOpenWindowsAutomatically() (to merely stop it from opening pop-up windows)
Control web site rendering via setUserAgent(), so you can supply your own user agent string to make the web server think you are a desktop browser, another mobile device (e.g., an iPhone), or whatever
165
166
CHAPTER 15: Embedding the WebKit Browser
The settings you change are not persistent, so you should store them somewhere (such as via the Android preferences engine) if you are allowing your users to determine the settings, versus hard-wiring the settings in your application.
16
Chapter
Applying Menus Like applications for the desktop and some mobile operating systems, Android supports activities with application menus. Most Android phones have a dedicated menu key for popping up the menu; other devices offer alternate means for triggering the menu to appear, such as the onscreen button used by the Archos 5 Android tablet. Also, as with many GUI toolkits, you can create context menus for your Android applications. On a traditional GUI, a context menu might be triggered by the user clicking with the right-mouse button. On mobile devices, context menus typically appear when the user taps and holds over a particular widget. For example, if a TextView had a context menu, and the device was designed for finger-based touch input, you could push the TextView with your finger, hold it for a second or two, and a pop-up menu would appear.
Flavors of Menu Android refers to the two types of menu described in the preceding section as options menus and context menus. The options menu is triggered by pressing the hardware Menu button on the device, while the context menu is raised by a tap-and-hold on the widget sporting the menu. In addition, the options menu operates in one of two modes: icon and expanded. When the user first presses the Menu button, the icon mode will appear, showing up to the first six menu choices as large, finger-friendly buttons in a grid at the bottom of the screen. If the menu has more than six choices, the sixth button will be labeled More. Tapping the More option will bring up the expanded mode, showing the remaining choices not visible in the regular menu. The menu is scrollable, so the user can get to any of the menu choices.
167
168
CHAPTER 16: Applying Menus
Menus of Options Instead of building your activity’s options menu during onCreate(), the way you wire up the rest of your UI, you need to implement onCreateOptionsMenu(). This callback receives an instance of Menu. The first thing you should do is chain upward to the superclass (super.onCreateOptionsMenu(menu)), so the Android framework can add in any menu choices it feels are necessary. Then you can go about adding your own options, as described in this section. If you will need to adjust the menu during your activity’s use (e.g., disable a now-invalid menu choice), just hold onto the Menu instance you receive in onCreateOptionsMenu(). Alternatively, you can implement onPrepareOptionsMenu(), which is called just before displaying the menu each time it is requested. Given that you have received a Menu object via onCreateOptionsMenu(), you add menu choices by calling add(). There are many flavors of this method, which require some combination of the following parameters:
A group identifier (int), which should be NONE unless you are creating a specific grouped set of menu choices for use with setGroupCheckable() (described shortly)
A choice identifier (also an int), for use in identifying this choice in the onOptionsItemSelected() callback when a menu choice is chosen
An order identifier (yet another int), for indicating where this menu choice should be slotted if the menu has Android-supplied choices alongside your own; for now, just use NONE
The text of the menu choice, as a String or a resource ID
The add() family of methods all return an instance of MenuItem, where you can adjust any of the menu item settings you have already set (e.g., the text of the menu choice). You can also set the shortcuts for the menu choice, which are single-character mnemonics that choose that menu item when the menu is visible. Android supports both an alphabetic (or QWERTY) set of shortcuts and a numeric set of shortcuts. These are set individually by calling setAlphabeticShortcut() and setNumericShortcut(), respectively. The menu is placed into alphabetic shortcut mode by calling setQwertyMode() on the menu with a true parameter. The choice and group identifiers are keys used to unlock additional menu features, such as the following:
Calling MenuItem#setCheckable() with a choice identifier, to control if the menu choice has a two-state check box alongside the title, where the check box value is toggled when the user chooses that menu item
CHAPTER 16: Applying Menus
Calling Menu#setGroupCheckable() with a group identifier, to turn a set of menu choices into ones with a mutual-exclusion radio button between them, so that only one item in the group can be in the checked state at any time
Finally, you can create fly-out submenus by calling addSubMenu(), supplying the same parameters as addMenu(). Android will eventually call onCreatePanelMenu(), passing it the choice identifier of your submenu, along with another Menu instance representing the submenu itself. As with onCreateOptionsMenu(), you should chain upward to the superclass, and then add menu choices to the submenu. One limitation is that you cannot indefinitely nest submenus—a menu can have a submenu, but a submenu cannot have a sub-submenu. If the user makes a menu choice, your activity will be notified via the onOptionsItemSelected() callback that a menu choice was selected. You are given the MenuItem object corresponding to the selected menu choice. A typical pattern is to switch() on the menu ID (item.getItemId()) and take appropriate behavior. Note that onOptionsItemSelected() is used regardless of whether the chosen menu item was in the base menu or a submenu.
Menus in Context By and large, context menus use the same guts as options menus. The two main differences are how you populate the menu and how you are informed of menu choices. First, you need to indicate which widget(s) on your activity have context menus. To do this, call registerForContextMenu() from your activity, supplying the View that is the widget needing a context menu. Next, you need to implement onCreateContextMenu(), which, among other things, is passed the View you supplied in registerForContextMenu(). You can use that to determine which menu to build, assuming your activity has more than one. The onCreateContextMenu() method gets the ContextMenu itself, the View the context menu is associated with, and a ContextMenu.ContextMenuInfo, which tells you which item in the list the user did the tap-and-hold over, in case you want to customize the context menu based on that information. For example, you could toggle a checkable menu choice based on the current state of the item. It is also important to note that onCreateContextMenu() gets called each time the context menu is requested. Unlike the options menu (which is built only once per activity), context menus are discarded after they are used or dismissed. Hence, you do not want to hold onto the supplied ContextMenu object; just rely on getting the chance to rebuild the menu to suit your activity’s needs on an on-demand basis based on user actions. To find out when a context menu choice was chosen, implement onContextItemSelected() on the activity. Note that you get only the MenuItem instance that was chosen in this callback. As a result, if your activity has two or more context menus, you may want to ensure they have unique menu item identifiers for all their
169
170
CHAPTER 16: Applying Menus
choices, so you can distinguish between them apart in this callback. Also, you can call getMenuInfo() on the MenuItem to get the ContextMenu.ContextMenuInfo you received in onCreateContextMenu(). Otherwise, this callback behaves the same as onOptionsItemSelected() as described in the previous section.
Taking a Peek In the sample project Menus/Menus, you will find an amended version of the ListView sample (List) with associated menus. Since the menus do not affect the layout, the XML layout file does not need to be changed and thus is not reprinted here. However, the Java code has a few new behaviors: package com.commonsware.android.menus; import import import import import import import import import import import import import import
android.app.AlertDialog; android.app.ListActivity; android.content.DialogInterface; android.os.Bundle; android.view.ContextMenu; android.view.Menu; android.view.MenuItem; android.view.View; android.widget.AdapterView; android.widget.ArrayAdapter; android.widget.EditText; android.widget.ListView; android.widget.TextView; java.util.ArrayList;
public class MenuDemo extends ListActivity { private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; public static final int MENU_ADD = Menu.FIRST+1; public static final int MENU_RESET = Menu.FIRST+2; public static final int MENU_CAP = Menu.FIRST+3; public static final int MENU_REMOVE = Menu.FIRST+4 ; private ArrayList words=null; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); initAdapter(); registerForContextMenu(getListView()); } @Override public boolean onCreateOptionsMenu(Menu menu) { menu .add(Menu.NONE, MENU_ADD, Menu.NONE, "Add") .setIcon(R.drawable.ic_menu_add);
CHAPTER 16: Applying Menus
menu .add(Menu.NONE, MENU_RESET, Menu.NONE, "Reset") .setIcon(R.drawable.ic_menu_refresh); return(super.onCreateOptionsMenu(menu)); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { menu.add(Menu.NONE, MENU_CAP, Menu.NONE, "Capitalize"); menu.add(Menu.NONE, MENU_REMOVE, Menu.NONE, "Remove"); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_ADD: add(); return(true); case MENU_RESET: initAdapter(); return(true); } return(super.onOptionsItemSelected(item)); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info= (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); ArrayAdapter adapter=(ArrayAdapter)getListAdapter(); switch (item.getItemId()) { case MENU_CAP: String word=words.get(info.position); word=word.toUpperCase(); adapter.remove(words.get(info.position)); adapter.insert(word, info.position); return(true); case MENU_REMOVE: adapter.remove(words.get(info.position)); return(true); } return(super.onContextItemSelected(item)); } private void initAdapter() {
171
172
CHAPTER 16: Applying Menus
words=new ArrayList(); for (String s : items) { words.add(s); } setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, words)); } private void add() { final View addView=getLayoutInflater().inflate(R.layout.add, null); new AlertDialog.Builder(this) .setTitle("Add a Word") .setView(addView) .setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { ArrayAdapter adapter=(ArrayAdapter)getListAdapter(); EditText title=(EditText)addView.findViewById(R.id.title); adapter.add(title.getText().toString()); } }) .setNegativeButton("Cancel", null) .show(); } }
In onCreate(), we register our ListView widget as having a context menu. We also delegate loading the adapter to an initAdapter() private method, one that copies the data out of our static String array and pours it into an ArrayList, using the ArrayList for the ArrayAdapter. The reason we do this is that we want to be able to change the contents of the list on-the-fly, and that is much easier if we use an ArrayList rather than an ordinary String array. For the options menu, we override onCreateOptionsMenu() and add two menu items, one to add a new word to the list and one to reset the words to their initial state. These menu items have IDs defined locally as static data members (MENU_ADD and MENU_RESET), and they also sport icons copied from the Android open source project. If the user displays the menu, it looks as shown in Figure 16–1.
CHAPTER 16: Applying Menus
Figure 16–1. The MenuDemo sample application and its options menu
We also override onOptionsItemSelected(), which will be called if the user makes a choice from the menu. The supplied MenuItem has a getItemId() method that should map to either MENU_ADD or MENU_RESET. In the case of MENU_ADD, we call a private add() method that displays an AlertDialog with a custom View as its contents, inflated from res/layout/add.xml:
That produces a dialog like the one shown in Figure 16–2.
173
174
CHAPTER 16: Applying Menus
Figure 16–2. The same application, showing the Add a Word dialog
If the user taps the OK button, we get our ArrayAdapter and call add() on it, adding the entered word to the end of the list. If the user chooses MENU_RESET, we call initAdapter() again, setting up a new ArrayAdapter and attaching it to our ListActivity. For the context menu, we override onCreateContextMenu(). Once again, we define a pair of menu items with local IDs, MENU_CAP (to capitalize the long-tapped-upon word) and MENU_REMOVE (to remove the word). Since context menus have no icons, we can skip that part. That gives the user the context menu shown in Figure 16–3 if they long-tap on a word.
CHAPTER 16: Applying Menus
Figure 16–3. The same application, showing the context menu
We also override onContextMenuSelected(). Since this is a context menu for a ListView, our MenuItem has some extra information for us—specifically, which item was longtapped upon in the list. To do that, we call getMenuInfo() on the MenuItem and cast the result to be an AdapterView.AdapterContextMenuInfo. That object, in turn, has a position data member, which is the index into our array of the word the user chose. From there, we work with our ArrayAdapter to capitalize or remove the word, as requested.
Yet More Inflation Chapter 13 explained how you can describe Views via XML files and “inflate” them into actual View objects at runtime. Android also allows you to describe menus via XML files and inflate them when a menu is needed. This helps you keep your menu structure separate from the implementation of menu-handling logic, and it provides easier ways to develop menu-authoring tools.
Menu XML Structure Menu XML goes in res/menu/ in your project tree, alongside the other types of resources that your project might employ. As with layouts, you can have several menu XML files in your project, each with its own filename and the .xml extension. For example, from the Menus/Inflation sample project, here is a menu called option.xml:
175
176
CHAPTER 16: Applying Menus
You must start with a menu root element.
Inside a menu element are item elements and group elements, the latter representing a collection of menu items that can be operated upon as a group.
Submenus are specified by adding a menu element as a child of an item element, using this new menu element to describe the contents of the submenu.
If you want to detect when an item is chosen, or to reference an item or group from your Java code, be sure to apply an android:id, just as you do with View layout XML.
Menu Options and XML Inside the item and group elements, you can specify various options, matching up with corresponding methods on Menu or MenuItem, as follows:
Title: The title of a menu item is provided via the android:title attribute on an item element. This can be either a literal string or a reference to a string resource (e.g., @string/foo).
Icon: Menu items optionally have icons. To provide an icon, in the form of a reference to a drawable resource (e.g., @drawable/eject), use the android:icon attribute on the item element.
Order: By default, the order of the items in the menu is determined by the order in which they appear in the menu XML. You can change that order by specifying the android:orderInCategory attribute on the item element. This is a 0-based index of the order for the items associated with the current category. There is an implicit default category; groups can provide an android:menuCategory attribute to specify a different category to use for items in that group. Generally, though, it is simplest just to put the items in the XML in the order you want them to appear.
Enabled: Items and groups can be enabled or disabled, controlled in the XML via the android:enabled attribute on the item or group element. By default, items and groups are enabled. Disabled items and groups appear in the menu but cannot be selected. You can change
CHAPTER 16: Applying Menus
an item’s status at runtime via the setEnabled() method on MenuItem, or change a group’s status via setGroupEnabled() on Menu.
Visible: Items and groups can be visible or invisible, controlled in the XML via the android:visible attribute on the item or group element. By default, items and groups are visible. Invisible items and groups do not appear in the menu. You can change an item’s status at runtime via the setVisible() method on MenuItem, or change a group’s status via setGroupVisible() on Menu.
Shortcut: Items can have shortcuts—single letters (android:alphabeticShortcut) or numbers (android:numericShortcut) that can be pressed to choose the item without having to use the touchscreen, D-pad, or trackball to navigate the full menu.
Inflating the Menu Actually using the menu, once it’s defined in XML, is easy. Just create a MenuInflater and tell it to inflate your menu. The Menus/Inflation project is a clone of the Menus/Menus project, with the menu creation converted to use menu XML resources and MenuInflater. The options menu was converted to the XML shown previously in this section; here is the context menu:
The Java code is nearly identical, changing mostly in the implementation of onCreateOptionsMenu() and onCreateContextMenu(): @Override public boolean onCreateOptionsMenu(Menu menu) { new MenuInflater(this).inflate(R.menu.option, menu); return(super.onCreateOptionsMenu(menu)); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { new MenuInflater(this).inflate(R.menu.context, menu); }
Here, we see how MenuInflater “pours” the menu items specified in the menu resource (e.g., R.menu.option) into the supplied Menu or ContextMenu object. We also need to change onOptionsItemSelected() and onContextItemSelected() to use the android:id values specified in the XML:
177
178
CHAPTER 16: Applying Menus
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.add: add(); return(true); case R.id.reset: initAdapter(); return(true); } return(super.onOptionsItemSelected(item)); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info= (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); ArrayAdapter adapter=(ArrayAdapter)getListAdapter(); switch (item.getItemId()) { case R.id.cap: String word=words.get(info.position); word=word.toUpperCase(); adapter.remove(words.get(info.position)); adapter.insert(word, info.position); return(true); case R.id.remove: adapter.remove(words.get(info.position)); return(true); } return(super.onContextItemSelected(item)); }
In the Land of Menus and Honey Android 3.0 (a.k.a. Honeycomb) introduced a new look and feel for Android applications, particularly on tablets. Options menus in particular change from being something triggered by a Menu button to a drop-down menu from the action bar. Fortunately, this is backward-compatible, so your existing menus will not need to change to adopt this new look. The concept of the new Honeycomb look is covered in Chapter 26, and the action bar itself is covered in Chapter 27.
17
Chapter
Showing Pop-Up Messages Sometimes, your activity (or other piece of Android code) will need to speak up. Not every interaction with Android users will be tidy and containable in activities composed of views. Errors will crop up. Background tasks may take much longer than expected. Something asynchronous may occur, such as an incoming message. In these and other cases, you may need to communicate with the user outside the bounds of the traditional user interface. Of course, this is nothing new. Error messages in the form of dialog boxes have been around for a long time. More subtle indicators also exist, from task tray icons to bouncing dock icons to vibrating cell phones. Android has quite a few systems for letting you alert your users outside the bounds of an Activity-based UI. One, notifications, is tied heavily into intents and services and, as such, is covered Chapter 37. In this chapter, you will learn about two means of raising pop-up messages: toasts and alerts.
Raising Toasts A Toast is a transient message, meaning that it displays and disappears on its own without user interaction. Moreover, it does not take focus away from the currently active Activity, so if the user is busy writing the next Great Programming Guide, keystrokes will not be “eaten” by the message. Since a Toast is transient, you have no way of knowing if the user even notices it. You get no acknowledgment from the user, nor does the message stick around for a long time to pester the user. Hence, the Toast is mostly for advisory messages, such as indicating a long-running background task is completed, the battery has dropped to a low level, and so on.
179
180
CHAPTER 17: Showing Pop-Up Messages
Making a Toast is fairly easy. The Toast class offers a static makeText() method that accepts a String (or string resource ID) and returns a Toast instance. The makeText() method also needs the Activity (or other Context) plus a duration. The duration is expressed in the form of the LENGTH_SHORT constant or LENGTH_LONG constant to indicate, on a relative basis, how long the message should remain visible. If you would prefer your Toast be made out of some other View, rather than be a boring old piece of text, simply create a new Toast instance via the constructor (which takes a Context), and then call setView() to supply it with the view to use and setDuration() to set the duration. Once your Toast is configured, call its show() method, and the message will be displayed. You will see an example of this in action later in this chapter.
Alert! Alert! If you would prefer something in the more classic dialog box style, what you want is an AlertDialog. As with any other modal dialog box, an AlertDialog pops up, grabs the focus, and stays there until closed by the user. You might use this for a critical error, a validation message that cannot be effectively displayed in the base activity UI, or some other situation where you are sure that the user needs to see the message immediately. The simplest way to construct an AlertDialog is to use the Builder class. Following in true builder style, Builder offers a series of methods to configure an AlertDialog, each method returning the Builder for easy chaining. At the end, you call show() on the builder to display the dialog. Commonly used configuration methods on Builder include the following:
setMessage(): Sets the “body” of the dialog to be a simple textual message, from either a supplied String or a supplied string resource ID
setTitle() and setIcon(): Configure the text and/or icon to appear in the title bar of the dialog box
setPositiveButton(),setNegativeButton(): Indicate which button(s) should appear across the bottom of the dialog, where they should be positioned (left, center, or right, respectively), what their captions should be, and what logic should be invoked when the button is clicked (besides dismissing the dialog).
If you need to configure the AlertDialog beyond what the builder allows, instead of calling show(), call create() to get the partially built AlertDialog instance, configure it the rest of the way, and then call one of the flavors of show() on the AlertDialog itself. Once show() is called, the dialog will appear and await user input. Note that pressing any of the buttons will close the dialog, even if you have registered a listener for the button in question. Hence, if all you need a button to do is close the dialog, give it a caption and a null listener. There is no option, with AlertDialog, to have a button at the bottom invoke a listener yet not close the dialog.
CHAPTER 17: Showing Pop-Up Messages
Checking Them Out To see how these work in practice, take a peek at Messages/Message, containing the following layout:
The following is the Java code: public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); } public void showAlert(View view) { new AlertDialog.Builder(this) .setTitle("MessageDemo") .setMessage("Let's raise a toast!") .setNeutralButton("Here, here!", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dlg, int sumthin) { Toast .makeText(MessageDemo.this, "", Toast.LENGTH_SHORT) .show(); } }) .show(); } }
The layout is unremarkable—just a really large Button to show the AlertDialog. When you click the Button, we use a builder (new Builder(this)) to set the title (setTitle("MessageDemo")), message (setMessage("Let's raise a toast!")), and neutral button (setNeutralButton("Here, here!", new OnClickListener() ...) before showing the dialog. When the button is clicked, the OnClickListener callback triggers the Toast class to make us a text-based toast (makeText(this, "", LENGTH_SHORT)), which we then show(). The result is a typical dialog, as shown in Figure 17–1.
181
182
CHAPTER 17: Showing Pop-Up Messages
Figure 17–1. The MessageDemo sample application, after clicking the Raise an alert button
When you close the dialog via the button, it raises the toast, as shown in Figure 17–2.
Figure 17–2. The same application, after clicking the Make a toast button
18
Chapter
Handling Activity Lifecycle Events As you know, Android devices, by and large, are phones. As such, some activities are more important than others—taking a call is probably more important to users than playing Sudoku. And, since it is a phone, it probably has less RAM than your current desktop or notebook. As a result of the phone’s limited RAM, your activity may find itself being killed off because other activities are going on and the system needs your activity’s memory. Think of it as the Android equivalent of the circle of life—your activity dies so others may live, and so on. You cannot assume that your activity will run until you think it is complete, or even until the user thinks it is complete. This is one example, perhaps the most important, of how an activity’s life cycle will affect your own application logic. This chapter covers the various states and callbacks that make up an activity’s life cycle, and how you can hook into them appropriately.
Schroedinger’s Activity An activity, generally speaking, is in one of four states at any point in time:
Active: The activity was started by the user, is running, and is in the foreground. This is what you are used to thinking of in terms of your activity’s operation.
Paused: The activity was started by the user, is running, and is visible, but a notification or something is overlaying part of the screen. During this time, the user can see your activity but may not be able to interact with it. For example, if a call comes in, the user will get the opportunity to take the call or ignore it.
183
184
CHAPTER 18: Handling Activity Lifecycle Events
Stopped: The activity was started by the user, is running, but is hidden by other activities that have been launched or switched to. Your application will not be able to present anything meaningful to the user directly, but may communicate by way of a Notification.
Dead: Either the activity was never started (e.g., just after a phone reset) or the activity was terminated, perhaps due to lack of available memory.
Life, Death, and Your Activity Android uses the methods described in this section to call into your activity as the activity transitions between the four states listed in the previous section. Some transitions may result in multiple calls to your activity, and sometimes Android will kill your application without calling it. This whole area is rather murky and probably subject to change, so pay close attention to the official Android documentation as well as this section when deciding which events deserve attention and which you can safely ignore. Note that for all of these methods, you should chain upward and invoke the superclass’s edition of the method, or Android may raise an exception.
onCreate() and onDestroy() We have been implementing onCreate() in all of our Activity subclasses in all the examples. This method will be called in three situations:
When the activity is first started (e.g., since a system restart), onCreate() will be invoked with a null parameter.
If the activity had been running, then sometime later was killed off, onCreate() will be invoked with the Bundle from onSaveInstanceState() as a parameter (as described in the next section).
If the activity had been running and you have set up your activity to have different resources based on different device states (e.g., landscape versus portrait), your activity will be re-created and onCreate() will be called. Working with resources is covered in Chapter 23.
Here is where you initialize your UI and set up anything that needs to be done once, regardless of how the activity is used. On the other end of the life cycle, onDestroy() may be called when the activity is shutting down, either because the activity called finish() (which “finishes” the activity) or because Android needs RAM and is closing the activity prematurely. Note that onDestroy() may not be called if the need for RAM is urgent (e.g., an incoming phone
CHAPTER 18: Handling Activity Lifecycle Events
call), but the activity will still be shut down. Hence, onDestroy() is mostly for cleanly releasing resources you obtained in onCreate() (if any).
onStart(), onRestart(), and onStop() An activity can come to the foreground either because it is first being launched or because it is being brought back to the foreground after having been hidden (e.g., by another activity or by an incoming phone call). The onStart() method is called in either of those cases. The onRestart() method is called in the case where the activity had been stopped and is now restarting. Conversely, onStop() is called when the activity is about to be stopped.
onPause() and onResume() The onResume() method is called just before your activity comes to the foreground, either after being initially launched, after being restarted from a stopped state, or after a pop-up dialog (e.g., an incoming call) is cleared. This is a great place to refresh the UI based on things that may have occurred since the user was last looking at your activity. For example, if you are polling a service for changes to some information (e.g., new entries for a feed), onResume() is a fine time to both refresh the current view and, if applicable, kick off a background thread to update the view (e.g., via a Handler). Conversely, anything that steals your user away from your activity—typically, the activation of another activity—will result in your onPause() being called. Here, you should undo anything you did in onResume(), such as stopping background threads, releasing any exclusive-access resources you may have acquired (e.g., camera), and the like. Once onPause() is called, Android reserves the right to kill off your activity’s process at any point. Hence, you should not be relying on receiving any further events.
The Grace of State Mostly, the aforementioned methods are for dealing with things at the applicationgeneral level (e.g., wiring together the last pieces of your UI in onCreate() or closing down background threads in onPause()). However, a large part of the goal of Android is to have a patina of seamlessness. Activities may come and go as dictated by memory requirements, but ideally, users are unaware that this is going on. If, for example, a user was using a calculator, took a lunch break, and returned to that calculator, he should see whatever number he was working on before the break, unless he took some action to close down the calculator (e.g., pressed the Back button to exit it).
185
186
CHAPTER 18: Handling Activity Lifecycle Events
To make all this work, activities need to be able to save their application-instance state, and to do so quickly and cheaply. Since activities could be killed off at any time, activities may need to save their state more frequently than you might expect. Then, when the activity restarts, the activity should get its former state back, so it can restore the activity to the way it appeared previously. Think of it as establishing a bookmark, such that when the user returns to that bookmark, you can restore the application to the same state that it was in when the user left it. Saving instance state is handled by onSaveInstanceState(). This supplies a Bundle, into which activities can pour whatever data they need (e.g., the number showing on the calculator’s display). This method implementation needs to be speedy, so do not try to be fancy—just put your data in the Bundle and exit the method. That instance state is provided to you again in two places: in onCreate() and in onRestoreInstanceState(). It is your choice when you wish to reapply the state data to your activity—either callback is a reasonable option. The built-in implementation of onSaveInstanceState() will save likely mutable state from a subset of widgets. For example, it will save the text in an EditText, but it will not save the status of whether a Button is enabled or disabled. This works as long as the widgets are uniquely identified via their android:id attributes. Hence, if you implement onSaveInstanceState(), you can either chain upward and leverage the inherited implementation or not and override the inherited implementation. Similarly, some activities may not need onSaveInstanceState() to be implemented at all, as the built-in one handles everything that is needed.
19
Chapter
Handling Rotation Some Android devices offer a slide-out keyboard that triggers rotating the screen from portrait to landscape orientation. Other devices use accelerometers to determine when the screen rotates. As a result, it is reasonable to assume that switching from portrait to landscape orientation and back again may be something that users of your application will want to do. As this chapter describes, Android has a number of ways for you to handle screen rotation so that your application can properly handle either orientation. Keep in mind, though, that these facilities only help you to detect and manage the rotation process— you still must make sure your layouts look decent in each orientation.
A Philosophy of Destruction By default, when there is a change in the device configuration that might affect resource selection, Android will destroy and re-create any running or paused activities the next time they are to be viewed. This could happen for a variety of different configuration changes, including these:
Rotating the screen (i.e., orientation change)
Extending or hiding a physical keyboard on devices that have a sliding keyboard
Putting the device in a car or desk dock, or removing it from a dock
Changing the locale, and thereby changing the preferred language
Screen rotation is the change most likely to trip you up, since a change in orientation can cause your application to load a different set of resources (e.g., layouts). The key here is that Android’s default behavior of destroying and re-creating any running or paused activities is probably the behavior that is best for most of your activities. You do have some control over the matter, though, and can tailor how your activities respond to orientation changes or similar configuration switches.
187
188
CHAPTER 19: Handling Rotation
It’s All the Same, Just Different Since, by default, Android destroys and re-creates your activity on a rotation, you may only need to hook into the same onSaveInstanceState() that you would if your activity were destroyed for any other reason (e.g., low memory). Implement that method in your activity and fill in the supplied Bundle with enough information to get you back to your current state. Then, in onCreate() (or onRestoreInstanceState(), if you prefer), pick the data out of the Bundle and use it to restore your activity to the way it was. To demonstrate this, let’s take a look at the Rotation/RotationOne project. This and the other sample projects in this chapter use a pair of main.xml layouts, one in res/layout/ for use in portrait mode and one in res/layout-land/ for use in landscape mode. Here is the portrait layout:
Here is the similar landscape layout:
CHAPTER 19: Handling Rotation
Basically, both layouts contain a pair of buttons, each taking up half the screen. In portrait mode, the buttons are stacked; in landscape mode, they are side by side. If you were to simply create a project, put in those two layouts, and compile it, the application would appear to work just fine—a rotation (Ctrl+F12 in the emulator) will cause the layout to change. And while buttons lack state, if you were using other widgets (e.g., EditText), you would even find that Android hangs onto some of the widget state for you (e.g., the text entered in the EditText). What Android cannot help you with automatically is anything held outside the widgets.
Picking and Viewing a Contact This application lets users pick a contact and then view the contact, via separate buttons. The View button is enabled only after the user picks a contact via the Pick button. Let’s take a closer look at how this feat is accomplished. When the user clicks the Pick button, we call startActivityForResult(). This is a variation on startActivity(), designed for activities that are set up to return some sort of result—a user’s choice of file, contact, or whatever. Relatively few activities are set up this way, so you cannot expect to call startActivityForResult() and get answers from any activity you choose. In this case, we want to pick a contact. There is an ACTION_PICK Intent action available in Android that is designed for this sort of scenario. An ACTION_PICK Intent indicates to Android that we want to pick...something. That “something” is determined by the Uri we put in the Intent. In our case, it turns out that we can use an ACTION_PICK Intent for certain systemdefined Uri values to let the user pick a contact from the device’s list of contacts. In particular, on Android 2.0 and higher, we can use android.provider.ContactsContract.Contacts.CONTENT_URI for this purpose: public void pickContact(View v) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI); startActivityForResult(i, PICK_REQUEST); }
For Android 1.6 and earlier, there is a separate android.provider.Contacts.CONTENT_URI that we could use.
189
190
CHAPTER 19: Handling Rotation
The second parameter to startActivityForResult() is an identifying number, to help us distinguish this call to startActivityForResult() from any others we might make. Calling startActivityForResult() with an ACTION_PICK Intent for the Contacts.CONTENT_URI will bring up a contact-picker activity, supplied by Android. When the user taps a contact, the picker activity ends (e.g., via finish()), and control returns to our activity. At that point, our activity is called with onActivityResult(). Android supplies us with three pieces of information:
The identifying number we supplied to startActivityForResult(), so we can match this result to its original request
A result status, either RESULT_OK or RESULT_CANCELED, to indicate whether the user made a positive selection or abandoned the picker (e.g., by pressing the Back button)
An Intent that represents the result data itself, for a RESULT_OK response
The details of what is in the Intent will need to be documented by the activity that you called. In the case of an ACTION_PICK Intent for the Contacts.CONTENT_URI, the returned Intent has its own Uri (via getData()) that represents the chosen contact. In the RotationOne example, we stick that in a data member of the activity and enable the View button: @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data.getData(); viewButton.setEnabled(true); } } }
If the user clicks the now-enabled View button, we create an ACTION_VIEW Intent on the contact’s Uri, and call startActivity() on that Intent: public void viewContact(View v) { startActivity(new Intent(Intent.ACTION_VIEW, contact)); }
This will bring up an Android-supplied activity to view details of that contact.
Saving Your State Given that we have used startActivityForResult() to pick a contact, now we need to hang onto that contact when the screen orientation changes. In the RotationOne example, we do this via onSaveInstanceState(): package com.commonsware.android.rotation.one; import android.app.Activity;
CHAPTER 19: Handling Rotation
import import import import import import import
android.content.Intent; android.net.Uri; android.os.Bundle; android.provider.ContactsContract.Contacts; android.view.View; android.widget.Button; android.util.Log;
public class RotationOneDemo extends Activity { static final int PICK_REQUEST=1337; Button viewButton=null; Uri contact=null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); viewButton=(Button)findViewById(R.id.view); restoreMe(savedInstanceState); viewButton.setEnabled(contact!=null); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data.getData(); viewButton.setEnabled(true); } } } public void pickContact(View v) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI); startActivityForResult(i, PICK_REQUEST); } public void viewContact(View v) { startActivity(new Intent(Intent.ACTION_VIEW, contact)); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (contact!=null) { outState.putString("contact", contact.toString()); } } private void restoreMe(Bundle state) {
191
192
CHAPTER 19: Handling Rotation
contact=null; if (state!=null) { String contactUri=state.getString("contact"); if (contactUri!=null) { contact=Uri.parse(contactUri); } } } }
By and large, it looks like a normal activity...because it is. Initially, the “model”—a Uri named contact—is null. It is set as the result of spawning the ACTION_PICK subactivity. Its string representation is saved in onSaveInstanceState() and restored in restoreMe() (called from onCreate()). If the contact is not null, the View button is enabled and can be used to view the chosen contact. Visually, it looks pretty much as you would expect, as shown in Figures 19–1 and 19–2.
Figure 19–1. The RotationOne application, in portrait mode
CHAPTER 19: Handling Rotation
Figure 19–2. The RotationOne application, in landscape mode
The benefit to this implementation is that it handles a number of system events beyond mere rotation, such as being closed by Android due to low memory. For fun, comment out the restoreMe() call in onCreate() and try running the application. You will see that the application “forgets” a contact selected in one orientation when you rotate the emulator or device.
Now with More Savings! The problem with onSaveInstanceState() is that you are limited to a Bundle. That’s because this callback is also used in cases where your whole process might be terminated (e.g., low memory), so the data to be saved must be something that can be serialized and has no dependencies on your running process. For some activities, that limitation is not a problem. For others, it is more annoying. Take an online chat, for example. You have no means of storing a socket in a Bundle, so by default, you have to drop your connection to the chat server and reestablish it. That not only may be a performance hit, but it might also affect the chat itself, such as showing in the chat logs that you are disconnecting and reconnecting. One way to get past this is to use onRetainNonConfigurationInstance() instead of onSaveInstanceState() for “light” changes like a rotation. Your activity’s onRetainNonConfigurationInstance() callback can return an Object, which you can retrieve later via getLastNonConfigurationInstance(). The Object can be just about anything you want. Typically, it will be some kind of “context” object holding activity state, such as running threads, open sockets, and the like. Your activity’s onCreate() can call getLastNonConfigurationInstance(), and if you get a non-null response, you now have your sockets and threads and whatnot. The biggest limitation is that you do not want to put in the saved context anything that might reference a resource that will get swapped out, such as a Drawable loaded from a resource.
193
194
CHAPTER 19: Handling Rotation
Let’s take a look at the Rotation/RotationTwo sample project, which uses this approach to handling rotations. The layouts, and hence the visual appearance, is the same as with Rotation/RotationOne. Where things differ slightly is in the Java code: package com.commonsware.android.rotation.two; import import import import import import import import
android.app.Activity; android.content.Intent; android.net.Uri; android.os.Bundle; android.provider.ContactsContract.Contacts; android.view.View; android.widget.Button; android.util.Log;
public class RotationTwoDemo extends Activity { static final int PICK_REQUEST=1337; Button viewButton=null; Uri contact=null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); viewButton=(Button)findViewById(R.id.view); restoreMe(); viewButton.setEnabled(contact!=null); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data.getData(); viewButton.setEnabled(true); } } } public void pickContact(View v) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI); startActivityForResult(i, PICK_REQUEST); } public void viewContact(View v) { startActivity(new Intent(Intent.ACTION_VIEW, contact)); } @Override public Object onRetainNonConfigurationInstance() { return(contact);
CHAPTER 19: Handling Rotation
} private void restoreMe() { contact=null; if (getLastNonConfigurationInstance()!=null) { contact=(Uri)getLastNonConfigurationInstance(); } } }
In this case, we override onRetainNonConfigurationInstance(), returning the actual Uri for our contact, rather than a string representation of it. In turn, restoreMe() calls getLastNonConfigurationInstance(), and if it is not null, we hold onto it as our contact and enable the View button. The advantage here is that we are passing around the Uri rather than a string representation. In this case, that is not a big saving. But our state could be much more complicated, including threads, sockets, and other things we cannot pack into a Bundle. However, even the onRetainNonConfigurationInstance() approach to handling rotations may be too intrusive to your application. Suppose, for example, you are creating a real-time game, such as a first-person shooter. The “hiccup” your users experience as your activity is destroyed and re-created might be enough to get them shot, which they may not appreciate. While this would be less of an issue on the TMobile G1, since a rotation requires sliding open the keyboard and therefore is unlikely to be done mid-game, other devices might rotate based solely on the device’s position as determined by accelerometers. For applications such as this, there is a third possibility for handling rotations, which is to tell Android that you will handle them yourself, without any assistance from the framework.
DIY Rotation To handle rotations without Android’s assistance, do the following: 1.
Put an android:configChanges entry in your AndroidManifest.xml file, listing the configuration changes you want to handle yourself versus allowing Android to handle them for you.
2.
Implement onConfigurationChanged() in your Activity, which will be called when one of the configuration changes you listed in android:configChanges occurs.
Now, for any configuration change you want, you can bypass the whole activitydestruction process and simply get a callback letting you know of the change. To see this in action, turn to the Rotation/RotationThree sample application. Once again, our layouts are the same, so the application looks the same as the preceding two samples. However, the Java code is significantly different, because we are no longer concerned with saving our state, but rather with updating our UI to deal with the layout.
195
196
CHAPTER 19: Handling Rotation
But first, we need to make a small change to our manifest:
Here, we state that we will handle keyboardHidden and orientation configuration changes ourselves. This covers us for any cause of the rotation, whether it is a sliding keyboard or a physical rotation. Note that this is set on the activity, not the application. If you have several activities, you will need to decide for each which of the tactics outlined in this chapter you wish to use. In addition, we need to add an android:id to our LinearLayout containers, such as follows:
The Java code for this project is shown here: package com.commonsware.android.rotation.three;
CHAPTER 19: Handling Rotation
import import import import import import import import import
android.app.Activity; android.content.Intent; android.content.res.Configuration; android.net.Uri; android.os.Bundle; android.provider.ContactsContract.Contacts; android.view.View; android.widget.Button; android.widget.LinearLayout;
public class RotationThreeDemo extends Activity { static final int PICK_REQUEST=1337; Button viewButton=null; Uri contact=null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); viewButton=(Button)findViewById(R.id.view); viewButton.setEnabled(contact!=null); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data.getData(); viewButton.setEnabled(true); } } } public void pickContact(View v) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI); startActivityForResult(i, PICK_REQUEST); } public void viewContact(View v) { startActivity(new Intent(Intent.ACTION_VIEW, contact)); } public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); LinearLayout container=(LinearLayout)findViewById(R.id.container); if (newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE) { container.setOrientation(LinearLayout.HORIZONTAL); } else {
197
198
CHAPTER 19: Handling Rotation
container.setOrientation(LinearLayout.VERTICAL); } } }
Our onConfigurationChanged() needs to update the UI to reflect the orientation change. Here, we find our LinearLayout and tell it to change its orientation to match that of the device. The orientation field on the Configuration object will tell us how the device is oriented.
...But Google Does Not Recommend This You might think that onConfigurationChanged() and android:configChanges would be the ultimate solution. After all, we no longer have to worry about all that messy passing of data to the new activity as the old one is being destroyed. The onConfigurationChanged() approach is very sexy. However, Google does not recommend it. The primary concern is forgetting about resources. With the onConfigurationChanged() approach, you must ensure that every resource that might possibly have changed as a result of this configuration change gets updated. That includes strings, layouts, drawables, menus, animations, preferences, dimensions, colors, and all the others. If you fail to ensure that everything is updated completely, your app will have a whole series of little (or not so little) bugs as a result. Allowing Android to destroy and re-create your activity guarantees you will get the proper resources. All you need to do is arrange to pass the proper data from the old activity to the new activity. The onConfigurationChanged() approach is appropriate only where the user would be directly affected by a destroy-and-create cycle. For example, imagine a video-player application that is playing a streaming video. Destroying and re-creating the activity would necessarily cause the application to have to reconnect to the stream, losing buffered data in the process. Users will get frustrated if an accidental movement causes the device to change orientation and interrupt their video playback. In this case, since the user will perceive problems with a destroy-and-create cycle, onConfigurationChanged() is an appropriate choice.
Forcing the Issue Some activities simply are not meant to change orientation. Games, camera previews, video players, and the like may make sense only in landscape orientation, for example. While most activities should allow the user to work in any desired orientation, for activities where only one orientation makes sense, you can control it. To block Android from rotating your activity, all you need to do is add android:screenOrientation = "portrait" (or "landscape", as you prefer) to your AndroidManifest.xml file, as follows (from the Rotation/RotationFour sample project):
CHAPTER 19: Handling Rotation
Since this is applied on a per-activity basis, you will need to decide which of your activities may need this turned on. At this point, your activity is locked into whatever orientation you specified, regardless of what you do. Figures 19–3 and 19–4 show the same activity as in the previous three sections, but using the preceding manifest and with the emulator set for both portrait and landscape orientation. Note that the UI does not move a bit, but remains in portrait mode.
Figure 19–3. The RotationFour application, in portrait mode
199
200
CHAPTER 19: Handling Rotation
Figure 19–4. The RotationFour application, in landscape mode
Note that Android will still destroy and re-create your activity, even if you have the orientation set to a specific value as shown here. If you wish to avoid that, you also need to set android:configChanges in the manifest, as described earlier in this chapter. Or, you can still use onSaveInstanceState() or onRetainNonConfigurationInstance() to save your activity’s mutable state.
Making Sense of It All As noted at the beginning of this chapter, devices with a slide-out keyboard (such as T-Mobile G1, Motorola DROID/Milestone, etc.) change screen orientation when the keyboard is exposed or hidden, whereas other devices change screen orientation based on the accelerometer. If you have an activity that should change orientation based on the accelerometer, even if the device has a slide-out keyboard, just add android:screenOrientation = "sensor" to your AndroidManifest.xml file as follows (from the Rotation/RotationFive sample project):
CHAPTER 19: Handling Rotation
The sensor, in this case, tells Android you want the accelerometers to control the screen orientation, so the physical shift in the device orientation controls the screen orientation. Android 2.3 adds a number of other possible values for android:screenOrientation:
reverseLandscape and reversePortrait: Indicate that you want the screen to be in landscape or portrait orientation, respectively, but upside down compared to the normal landscape and portrait orientations
sensorLandscape and sensorPortrait: Indicate that you want the screen to be locked in landscape or portrait orientation, respectively, but the sensors can be used to determine which side is “up”
fullSensor: Allows the sensors to put the screen in any of the four possible orientations (portrait, reverse portrait, landscape, reverse landscape), whereas sensor toggles only between portrait and landscape
201
Chapter
20
Dealing with Threads Users like snappy applications. Users do not like applications that feel sluggish. The way to help make your application feel snappy to users is to use the standard threading capabilities built into Android. This chapter will walk you through the issues involved with thread management in Android and some of the options for keeping the UI crisp and responsive.
The Main Application Thread You might think that when you call setText() on a TextView, the screen is updated with the text you supply, right then and there. That is not how it works. Rather, everything that modifies the widget-based UI goes through a message queue. Calls to setText() do not update the screen; they just pop a message on a queue telling the operating system to update the screen. The operating system pops these messages off of this queue and does what the messages require. The queue is processed by one thread, variously called the main application thread and the UI thread. As long as that thread can keep processing messages, the screen will update, user input will be handled, and so on. However, the main application thread is also used for nearly all callbacks into your activity. Your onCreate(),onClick(),onListItemClick(), and similar methods are all called on the main application thread. While your code is executing in these methods, Android is not processing messages on the queue, meaning the screen does not update, user input is not handled, and so on. This, of course, is bad. So bad, in fact, that if you take more than a few seconds to do work on the main application thread, Android may display the dreaded “application not responding” (ANR) error, and your activity may be killed off. Hence, you want to make sure that all of your work on the main application thread happens quickly. This means that anything slow should be done in a background thread, so as not to tie up the main application thread. This includes activities such as the following:
Internet access, such as sending data to a web service or downloading an image
203
204
CHAPTER 20: Dealing with Threads
Significant file operations, since flash storage can be remarkably slow at times
Any sort of complex calculations
Fortunately, Android supports threads using the standard Thread class from Java, plus all the wrappers and control structures you would expect, such as the java.util.concurrent class package. However, there is one big limitation: you cannot modify the UI from a background thread. You can modify the UI only from the main application thread. Hence, you need to move long-running work into background threads, but those threads need to do something to arrange to update the UI using the main application thread. Android provides a wide range of tools to do just that, and these tools are the primary focus of this chapter.
Making Progress with ProgressBars If you are going to fork background threads to do work on behalf of the user, you should consider keeping the user informed that work is going on. This is particularly true if the user is effectively waiting for that background work to complete. The typical approach to keeping users informed of progress is some form of progress bar, like you see when you copy a bunch of files from place to place in many desktop operating systems. Android supports this through the ProgressBar widget. A ProgressBar keeps track of progress, defined as an integer, with 0 indicating no progress has been made. You can define the maximum end of the range—which value indicates progress is complete—via setMax(). By default, a ProgressBar starts with a progress of 0, though you can start from some other position via setProgress(). If you prefer your progress bar to be indeterminate, use setIndeterminate() and set it to true. In your Java code, you can either positively set the amount of progress that has been made (via setProgress()) or increment the progress from its current amount (via incrementProgressBy()). You can find out how much progress has been made via getProgress(). There are other alternatives for displaying progress—ProgressDialog, a progress indicator in the activity’s title bar, and so on—but a ProgressBar is a good place to start.
Getting Through the Handlers The most flexible means of making an Android-friendly background thread is to create an instance of a Handler subclass. You need only one Handler object per activity, and you do not need to manually register it. Merely creating the instance is sufficient to register it with the Android threading subsystem.
CHAPTER 20: Dealing with Threads
Your background thread can communicate with the Handler, which will do all of its work on the activity’s UI thread. This is important, as UI changes, such as updating widgets, should occur only on the activity’s UI thread. You have two options for communicating with the Handler: messages and Runnable objects.
Messages To send a Message to a Handler, first invoke obtainMessage() to get the Message object out of the pool. There are a few flavors of obtainMessage(), allowing you to create empty Message objects or ones populated with message identifiers and arguments. The more complicated your Handler processing needs to be, the more likely it is you will need to put data into the Message to help the Handler distinguish different events. Then, you send the Message to the Handler via its message queue, using one of the sendMessage...() family of methods, such as the following:
sendMessage(): Puts the message on the queue immediately
sendMessageAtFrontOfQueue(): Puts the message on the queue immediately and places it at the front of the message queue (versus the back, which is the default), so your message takes priority over all others
sendMessageAtTime(): Puts the message on the queue at the stated time, expressed in the form of milliseconds based on system uptime (SystemClock.uptimeMillis())
sendMessageDelayed(): Puts the message on the queue after a delay, expressed in milliseconds
sendEmptyMessage(): Sends an empty Message object to the queue, allowing you to skip the obtainMessage() step if you were planning on leaving it empty anyway
To process these messages, your Handler needs to implement handleMessage(), which will be called with each message that appears on the message queue. There, the Handler can update the UI as needed. However, it should still do that work quickly, as other UI work is suspended until the Handler is finished. For example, let’s create a ProgressBar and update it via a Handler. Here is the layout from the Threads/Handler sample project:
The ProgressBar, in addition to setting the width and height as normal, also employs the style property. This particular style indicates the ProgressBar should be drawn as the traditional horizontal bar showing the amount of work that has been completed. And here is the Java: package com.commonsware.android.threads; import import import import import import
android.app.Activity; android.os.Bundle; android.os.Handler; android.os.Message; android.widget.ProgressBar; java.util.concurrent.atomic.AtomicBoolean;
public class HandlerDemo extends Activity { ProgressBar bar; Handler handler=new Handler() { @Override public void handleMessage(Message msg) { bar.incrementProgressBy(5); } }; AtomicBoolean isRunning=new AtomicBoolean(false); @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); bar=(ProgressBar)findViewById(R.id.progress); } public void onStart() { super.onStart(); bar.setProgress(0); Thread background=new Thread(new Runnable() { public void run() { try { for (int i=0;i=100) { markAsDone(); } } } @Override public Object onRetainNonConfigurationInstance() { task.detach(); return(task); } void updateProgress(int progress) { bar.setProgress(progress); } void markAsDone() { findViewById(R.id.completed).setVisibility(View.VISIBLE); } static class RotationAwareTask extends AsyncTask { RotationAsync activity=null; int progress=0; RotationAwareTask(RotationAsync activity) { attach(activity); } @Override protected Void doInBackground(Void... unused) { for (int i=0;i