When Android was being invented, its designers faced many choices whose outcome would determine the success or failure of their project. Once they had rejected all the other smartphone operating systems, both closed and open source, and decided to build their own atop the Linux kernel, they were faced with somewhat of a blank canvas. One important choice was which programming language to use; they wisely chose Java. But once that choice was made, there was the choice of user interface technology to deploy: Java ME, Swing, the Standard Widget Toolkit (SWT), or none of the above.
Java ME is the Java Micro Edition, Sun/Oracle’s official standard API for cell phones and other small devices. Java ME was once a pretty big success story: tens if not hundreds of millions of cell phones have a Java ME runtime inside. Also, all BlackBerry devices made from around 2000 to around 2010, and all BlackBerry smartphone applications for BlackBerry OSes 5, 6 and 7 (but not including BlackBerry OS 10), were based on Java ME. But the Java ME GUI was regarded as too limiting by the Android team, having been designed for the days when cell phones had really tiny screens and limited functionality (it’s noteworthy that BlackBerry came to a similar conclusion, dropping Java ME when it came time to produce BlackBerry 10).
Swing is the Java Standard Edition (desktop Java, Java SE, a.k.a. JDK or JRE) GUI. It is based on Java’s earlier widget toolkit, the Abstract Window Toolkit (AWT). Swing can make some beautiful GUI music in the right hands, but is rather large and uses too much overhead for Android.
SWT is the GUI layer developed for use in the Eclipse IDE itself and in Eclipse rich clients. It is an abstraction layer, and depends on the underlying operating system–specific toolkit (e.g., Win32 in the Microsoft arena, GTK under Unix/Linux, etc.).
The final option, and the one ultimately chosen, was to go it alone. The Android designers thus built their own GUI toolkit designed specifically for smartphones. But they took many good ideas from the other toolkits, and learned from the mistakes that had been made along the way.
To learn any new GUI framework is, necessarily, a lot of work. Making your apps work well in the community of apps for that UI is even more work. Recognizing this, Google has set up the Android Design site. Another set of guidelines that can help is the Android Patterns site, which is not about coding but about showing designers how the Android visual experience is supposed to work. Illustrated, crowdsourced, and recommended!
One word of terminological warning: the term “widget” has two distinct meanings. All GUI controls such as buttons, labels, and the like are widgets and appear in the android.widget
package. This package also contains the “layout containers” (ViewGroup
subclasses), which are rather like a combination of JPanel
and LayoutManager
in Swing. Simple widgets and layouts are subclassed from View
, so collectively they are often referred to as a view. The other kind of widget is one that can appear on an Android Home screen; these are now called “app widgets” to distinguish them from the basic ones and are in their own package, android.appwidget
. This type of widget is commonly used for status displays such as news and weather updates, updates from friends/social streams, and the like. We have a recipe on app widgets (Recipe 6.30), at the end of this chapter. While we’ll try to use the terms widget and app widget correctly, you sometimes have to infer from the context which meaning is intended.
This chapter covers the main GUI elements in Android. The following chapter covers
the “things that go bump in your device”: menus, dialogs, toasts, and notifications.
The one after that treats the all-important topic of list views (ListView
and RecyclerView
).
Ian Darwin
Use the user interface guidelines. But which ones?
UI guidelines have been around almost since Xerox PARC invented GUIs in the 1980s and showed them to Microsoft and Apple. A given set of guidelines must be appropriate to the platform. General guidelines for mobile devices are available from several sources. Android.com publishes advice too.
The official Android UI Guidelines are probably as good a starting place as any, especially if you already have some background in UI design. If not, some of the other works discussed in this recipe may help you understand UI design issues.
For some thoughtful UI pattern notes, see the Android Developers blog.
One of the oldest GUI guides is Microsoft’s The Gui Guide: International Terminology for the Windows Interface. This was less about UI design than about internationalization; it came with a floppy disk (remember those?) containing recommended translations for common Microsoft Windows GUI element names into a dozen or so common languages. This book is rather dated today.
In the 1980s and 1990s Sun’s user interface development was heavily influenced by Xerox PARC, in XView, Sun’s long-defunct Unix implementation of OPEN LOOK, and in the “Java Look and Feel,” respectively. A classic but technology-specific work from this time and place is the Java Look and Feel Design Guidelines (Addison-Wesley).
A more general work is Designing Visual Interfaces: Communication-Oriented Techniques by Kevin Mullet and Darrell Sano (Prentice Hall). This is a thorough discussion of the design issues, mostly from a desktop perspective (Mac, Unix, Windows), but the principles spelled out here are useful in dealing with human–computer interaction issues.
Concluding the desktop front is the more recent Microsoft-oriented book About Face: The Essentials of Interaction Design (Wiley). Now in its third edition, this book was originally written by Alan Cooper, known as the “Father of Visual Basic.”
Ian Darwin
Use Material Design, Android’s new visual paradigm for application development—or, as Android puts it, “a comprehensive guide for visual, motion, and interaction design across platforms and devices.” You can make your apps look great using this modern set of visual approaches.
The main steps you need to implement in creating or updating an app for Material Design include the following:
Read the official Material Design specification.
Create or update your layouts following Material Design guidelines.
Add elevation to your View
objects, causing them to cast shadows.
Consider using new features, such as card widgets, and new versions of widgets such as the RecyclerView
in place of the older ListView
.
Add or customize animations in your app.
And do all this while maintaining backward compatibility!
Google introduced the material design approach in Android 50. It is also used in Google web applications, supported by a web toolkit called Material Design Lite. The name comes from the analogy with physical material, e.g., either “physical stuff” or “fabric.” Material Design is a largely two-dimensional framework, except that objects (such as View
objects in Android) have elevation, which causes them to cast shadows (drop-shadow effects have been with us for decades, but this formalizes them a bit), and can be animated on entry, activation, or exit.
The basic step in using a material theme is to base your application’s theme on material rather than the older Holo-based themes. You will typically tailor the colors used in your theme. For example, in the AndroidManifest.xml file, you might have:
<application
android:name=
".AndroidApplication"
android:icon=
"@drawable/icon"
android:label=
"@string/app_name"
android:theme=
"@style/AppTheme"
>
android:allowBackup="true"> ...</application>
You would then have a styles.xml file defining AppTheme
, based on android:Theme.Material
, as per Example 6-1. The five main colors you can set are shown in Figure 6-1.
<resources>
<style
name=
"AppTheme"
parent=
"android:Theme.Material"
>
<item
name=
"android:colorPrimary"
>
@color/primary</item>
<item
name=
"android:colorPrimaryDark"
>
@color/primary_dark</item>
<item
name=
"android:colorAccent"
>
@color/accent</item>
</style>
</resources>
The actual values of these colors must be defined in an XML file. We’ll choose colors from the palettes listed in the specification—here we’re using the 300, 500, and 700 colors of the Orange palette:
<resources>
<color
name=
"primary"
>
#FF9800</color>
<color
name=
"primary_dark"
>
#F57C00</color>
<color
name=
"accent"
>
#FFCC80</color>
</resources>
Creating your layouts is a matter of applying the design prescriptions as you go along, paying attention to sizes, distances, and the like.
Adding elevation is partly a matter of using the new android:elevation
attribute:
<Button ... android:elevation="5sp"/>
Elevation is used to represent the fact that material has thickness and,
for example, if you place a book on a table, the book will cast a shadow.
Material Design recommends that the action bar (see Recipe 6.5) always have an elevation of 4dp
.
Purely for demonstration purposes (we don’t recommend ever doing this in a real app), Example 6-2 shows
code that produces various elevations; this code allows you, by dragging the slider, to view the effects of
different elevations on the drop shadow (see Figure 6-2).
public
void
onCreate
(
Bundle
b
)
{
Button
raisable
=
(
Button
)
findViewById
(
R
.
id
.
elevator
);
SeekBar
control
=
(
SeekBar
)
findViewById
(
R
.
id
.
elevatorControl
);
control
.
setOnSeekBarChangeListener
(
new
SeekBar
.
OnSeekBarChangeListener
()
{
public
void
onProgressChanged
(
SeekBar
seekBar
,
int
progress
,
boolean
fromUser
)
{
raisable
.
setElevation
(
progress
);
raisable
.
setText
(
getString
(
R
.
string
.
raise_me
)
+
" "
+
progress
);
}
// Two methods from SeekBar.OnSeekBarChangeListener interface omitted
});
}
Please refer to other recipes on using new features such as Card
widgets (Recipe 6.13) and new versions of widgets such as the RecyclerView
(Recipe 8.1) in place of the older ListView
.
There are even new icons for Material Design. Quoting its download page:
Material design system icons are simple, modern, friendly, and sometimes quirky. Each icon is created using our design guidelines to depict in simple and minimal forms the universal concepts used commonly throughout a UI. Ensuring readability and clarity at both large and small sizes, these icons have been optimized for beautiful display on all common platforms and display resolutions.
As if to emphasize that Material isn’t just for Android, the icons are available in several forms, for web view (as images or as a web font), for Android, and even for iOS. See https://design.google.com/icons/ for information on the Material Icons package.
Google’s Android team has published, well, a lot of “material” on Material Design, including the following:
As well, several good third-party discussions have been published, including these by Greg Nudelman:
Ian Darwin
As in the case of Java SE and most other GUI packages, there are multiple components you can use to control the layout of individual GUI components. Java SE’s AWT and Swing provide two classes that work together: Container
and LayoutManager
. A Container
has a LayoutManager
instance to perform layout calculations on its behalf. Android, having been conceived for smaller devices, combines these two functions into a single class, android.view.ViewGroup
. There are many subclasses of ViewGroup
intended for you to use. While LinearLayout
is the most well-known, there are many others. There are also some subclasses that are not intended for use as arbitrary layout managers, such as the drop-down–like Spinner
(see Recipe 6.14). The following table should help you get a handle on which one(s) to use.
Name | Basic idea | AbsoluteLayout |
---|---|---|
Absolute positioning; almost never the right choice! |
|
Multiple |
|
Equal-sized |
|
|
|
Complex layouts, like HTML tables; more efficient than nesting |
|
A set of rows, each with some number of columnsa |
|
Tabbed view |
|
Vertical divide of the screen |
a See also Recipe 1.25. |
All modern IDEs for Android come with a built-in drag-and-drop visual layout editor that lets you drag and drop GUI components to arrange the layout as you want it. There also used to be a standalone GUI builder tool called DroidDraw, but it seems to have been abandoned by its original author, left behind when Google shut down GoogleCode. There have been multiple attempts to revive DroidDraw; you can find them with a GitHub search if you have some reason for not using the tools that come with your IDE.
Alex Leffelman
Decouple your user interface from your data model so that the destruction of your Activity doesn’t affect your state data.
It’s a situation that all Android developers (except those who read this part of this book in time) run into with their very first applications: “My application works great, but when I change my phone’s orientation everything resets!”
By design, when a device’s configuration (read: orientation) changes, the Android UI framework destroys the current Activity and re-creates it for the new configuration. This enables the designer to optimize the layout for different screen orientations and sizes. However, this causes a problem for the developer who wishes to maintain the state of the Activity as it was before the orientation change destroyed the screen. Attempting to address this problem can lead to many complicated solutions, some more graceful than others. But if we take a step back and design our applications wisely, we can write cleaner, more robust code that makes life easier for everyone.
A graphical user interface is exactly what its name describes. It is a graphical representation of an underlying data model that allows the user to interface with and manipulate the data. It is not the data model itself. Let’s talk our way through an example to illustrate why that is an important point to make.
Consider a tic-tac-toe application. A simple main Activity for this would most likely include at bare minimum a GridView
(with appropriate Adapter
) to display the board and a TextView
to tell the user whose turn it is. When the user clicks a square in the grid, an appropriate X or O is placed in that grid cell. As new Android developers, we find it logical to also include a two-dimensional array containing a representation of the board to store its data so that we can determine if the game is over, and if so, who won (see Example 6-3).
public
class
TicTacToeActivity
extends
Activity
{
private
TicTacToeState
[][]
mBoardState
;
private
GridView
mBoard
;
private
TextView
mTurnText
;
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
setContentView
(
R
.
layout
.
main
);
mBoardState
=
new
TicTacToeState
[
3
][
3
];
mBoard
=
(
GridView
)
findViewById
(
R
.
id
.
board
);
mTurnText
=
(
TextView
)
findViewById
(
R
.
id
.
turn_text
);
// Set up Adapter, OnClickListeners, etc., for mBoard
}
}
This is easy enough to imagine and implement, and everything works great—except that when you turn your phone sideways in the middle of an intense round of tic-tac-toe, you have a fresh board staring you in the face and your inevitable victory is postponed. As described earlier, the UI framework just destroyed your Activity and re-created it, calling onCreate()
and resetting the board data.
While reading the code in Example 6-3, you might have said to yourself, “Hey, that Bundle savedInstanceState
looks promising!” And you’d be right. For this painfully simple example, you could stick your board data into a Bundle
and use it to reload your screen. There’s even a pair of methods, onRetainNonConfigurationInstance()
and getLastNonConfigurationInstance()
, that let you pass any Object
you want from your old, destroyed Activity to your newly created one. For this example you could just pass your mBoardState
array to your new Activity, and you’d be all set. But you’re going to be writing big, successful, amazing apps any day now, and that just doesn’t scale well with complicated interfaces. We can do better!
This is why separating your GUI from your data model is so handy. Your GUI can be destroyed, re-created, and changed, but the underlying data can survive unharmed through as many UI changes as you can throw at it. Let’s separate our game state out into a separate data class (see Example 6-4).
public class TicTacToeGame { private TicTacToeState[][] mBoardState; public TicTacToeGame() { mBoardState = new TicTacToeState[3][3]; // Initialize } public TicTacToeState getCellState(int row, int col) { return mBoardState[row][col]; } public void setCellState(int row, int col, TicTacToeState state) { mBoardState[row][col] = state; } // Other methods to determine whose turn it is, if the game is over, etc. }
This will not only help us maintain our application state but is generally just good object-oriented design.
Now that we have our data safely outside of the volatile Activity, how do we access it to build our interface? There are two common approaches: we can declare all variables in TicTacToeGame
as static
and access them through static methods; or we can design TicTacToeGame
as a singleton, allowing access to one global instance to be used throughout our application.
I prefer the second option purely from a design perspective. We can turn TicTacToeGame
into a singleton by making the constructor private
and adding the following lines to the top of the class:
private static TicTacToeGame instance = new TicTacToeGame(); public static TicTacToeGame getInstance() { return instance; };
Now all we have to do is obtain the game data and set our UI elements to appropriately display the data. It’s most useful to wrap this in its own function—refreshUI()
, perhaps—so that it can be used whenever our Activity makes a change to the data. For example, when a user clicks a cell of the board, there need only be two lines of code in the listener: one call to modify the data model (via our TicTacToeGame
singleton), and one call to refresh the UI.
It may be obvious, but it is worth mentioning that your data classes survive only as long as your application’s process is running. If it is killed by the user or the system, naturally the data is lost. That situation necessitates more persistent storage through the filesystem or databases and is outside the scope of this recipe.
This approach very effectively decouples your visual representation of the data from the data itself, and makes orientation changes trivial. Simply calling refreshUI()
in your onCreate(Bundle)
method is enough to ensure that whenever your Activity is destroyed and re-created, it can access the data model and display itself correctly. And as an added bonus, you’re now practicing better object-oriented design and will see your code base become cleaner, more scalable, and easier to maintain.
Ian Darwin
Use either a Toolbar
or the older ActionBar
in your layout, and use a mixture of XML and code
to control the action bar’s functionality.
The action bar was introduced in Android Honeycomb (3.0, API 11) as a standard component for most normal applications. The one common use case for an app that doesn’t have an action bar is full-screen applications such as cameras, photo display/editing apps, or video games.
The action bar consists of a full-width rectangle about 5% of the vertical size of the screen, with an icon and program name at the left, an “overflow menu” icon (replacing the hard Menu button present on older Android hardware) at the right (three dots in a vertical row), and optionally actions to the left of the overflow menu. A typical layout is shown in Figure 6-3.
There are several ways to get an ActionBar
. Assuming you are targeting modern versions (4.0 and later), you will
automatically have an action bar unless you provide an application theme that is not derived directly or
indirectly from the Holo theme. This will be implemented by the “standard”
android.app.ActionBar
class.
One downside of this is that some features have been added since the time the ActionBar
was introduced (Android 3.0), in modern versions such as Android 7.0; thus you may be calling methods that exist in modern versions but will not exist in the library on the user’s device if the device is running an older version of Android. This would, of course, end badly.
Accordingly, the current recommendation is to use the “v7 appcompat” library.
This library provides two different ActionBar
implementations, the
appcompat ActionBar
and the recommended
appcompat ToolBar
class.
The ToolBar
is more general than the ActionBar
and can be used in different places, though we’ll only
consider its use as an action bar here.
The recommended steps for your action bar are thus:
Ensure that v7 appcompat is included; the coordinates for this library as of API 24 are com.android.support:appcompat-v7:24.1.1
. But use the latest one for the API you are targeting.
Make your Activities extend AppCompatActivity
, not Activity
.
In the AndroidManifest.xml file, set the application
element to use a NoActionBar
theme so you don’t get a native ActionBar
added (two action bars will not coexist well).
In the layout file, use an android.support.v7.widget.Toolbar
(as in Example 6-5) and position it at the top of your layout
so it will look like a standard action bar.
In your Activity
startup code, set this toolbar as the action bar using setCompatActionBar()
.
Example 6-5 is the layout file with a ToolBar
, as generated by Android Studio.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
xmlns:tools=
"http://schemas.android.com/tools"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:fitsSystemWindows=
"true"
tools:context=
"com.androidcookbook.actionbarcompat.MainActivity"
>
<android.support.design.widget.AppBarLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:theme=
"@style/AppTheme.AppBarOverlay"
>
<android.support.v7.widget.Toolbar
android:id=
"@+id/toolbar"
android:layout_width=
"match_parent"
android:layout_height=
"?attr/actionBarSize"
android:background=
"?attr/colorPrimary"
app:popupTheme=
"@style/AppTheme.PopupOverlay"
/>
</android.support.design.widget.AppBarLayout>
<LinearLayout
android:orientation=
"vertical"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
app:layout_behavior=
"@string/appbar_scrolling_view_behavior"
>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Hello World!"
android:padding=
"5dp"
/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
The CoordinatorLayout
is part of the appcompat library, and requires use of the
app:layout_behavior
attribute on its immediate children; without it, they are ignored.
Example 6-6 is the Activity code for this sample application.
public
class
MainActivity
extends
AppCompatActivity
{
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
activity_main
);
Toolbar
toolbar
=
(
Toolbar
)
findViewById
(
R
.
id
.
toolbar
);
setSupportActionBar
(
toolbar
);
}
...
}
Once you have created your action bar, you can control it in various ways. To make your action bar disappear or reappear, you can use:
getCompatActionBar
().
hide
()
getCompatActionBar
().
show
()
In fact, any of the standard appcompat ActionBar
methods can be used.
The getCompatActionBar()
method doesn’t return the toolbar directly, but returns it wrapped in a
ToolbarActionBar
which, as the name implies, provides all the ActionBar
methods.
In the first paragraphs of this recipe, I referred to the “overflow menu” without explaining that.
The traditional Android Options menu (described in Recipe 7.3) appears in response to the “Menu button,”
but modern Android devices are not required to have a physical button for this purpose.
Instead, the three dots at the right of the action bar (see Figure 6-3) call up the menu.
However—and this is one of the original uses for the action bar—you can “promote” menu items
from the Activity’s menu to the action bar!
All you have to do is add the showAsAction
attribute in the menu.xml file.
This attribute can take the value never
(which is the default),
ifRoom
(whose meaning is obvious), or always
(which is also obvious,
but whose use is not recommended; you should use ifRoom
in preference to this last choice).
Here is a Help menu item added to the default menu file:
<menu
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
xmlns:tools=
"http://schemas.android.com/tools"
tools:context=
"com.androidcookbook.actionbarcompat.MainActivity"
>
<item
android:id=
"@+id/action_settings"
android:orderInCategory=
"100"
android:title=
"@string/action_settings"
app:showAsAction=
"never"
/>
<item
android:id=
"@+id/action_help"
android:title=
"Help"
app:showAsAction=
"ifRoom"
/>
</menu>
This produces the layout shown in Figure 6-4.
As with other menu items, you can use text, icons, or both.
You will find more information in the official documentation. The “Share action” in the action bar is treated specially, in Recipe 6.6.
The source code for this example is in the Android Cookbook repository, in the subdirectory ActionBarCompat (see “Getting and Using the Code Examples”).
Ian Darwin
Sharing information is one of the canonical uses of mobile and computing devices. Having one application use another to handle data is a prime feature of the Android platform. Android offers a “Share” menu that lets you pass text, images, or almost anything else off to any of a number of applications to be handled.
For example, let’s examine how to export (share) a short string as “plain text”; this will be sharable to (acceptable by) quite a few applications, but Android will choose the “most popular” to put at the top of the Share menu.
We start by adding a menu item that will go into the ActionBar
:
<menu
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<item
android:id=
"@+id/menu_item_share"
android:showAsAction=
"ifRoom"
android:title=
"@string/action_share"
android:actionProviderClass=
"android.widget.ShareActionProvider"
/>
...</menu>
In our onCreate()
method, we create an Intent with an action of ACTION_SEND
, a content type of plain text, and an Extra
of the string we want to share. There is nothing special about text—this mechanism can share almost any kind of data, as long as there’s at least one app that has registered with an Intent filter for the given content type:
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
activity_main
);
mShareIntent
=
new
Intent
();
mShareIntent
.
setAction
(
Intent
.
ACTION_SEND
);
mShareIntent
.
setType
(
"text/plain"
);
mShareIntent
.
putExtra
(
Intent
.
EXTRA_TEXT
,
"From me to you, this text is new."
);
}
Finally, in our menu creation method, we find the MenuItem
by its ID and ask it for its ActionProvider
(this is where API level 14 is required!). If we find that, we just add the share Intent to it:
@Override
public
boolean
onCreateOptionsMenu
(
Menu
menu
)
{
// Inflate the menu; this adds items to the action bar if it is present
getMenuInflater
().
inflate
(
R
.
menu
.
main
,
menu
);
// Find the MenuItem that we know has the ShareActionProvider
MenuItem
item
=
menu
.
findItem
(
R
.
id
.
menu_item_share
);
// Get its ShareActionProvider
mShareActionProvider
=
(
ShareActionProvider
)
item
.
getActionProvider
();
// Connect the dots: give the ShareActionProvider its Share Intent
if
(
mShareActionProvider
!=
null
)
{
mShareActionProvider
.
setShareIntent
(
mShareIntent
);
}
// Return true so Android will know we want to display the menu
return
true
;
}
That really is all there is to it.
When you first run the app, it looks like Figure 6-5.
Tap the Share icon and the Share menu appears—all courtesy of the ShareActionProvider
! As mentioned, the most likely apps are at the top of the list; the rest are delegated to the “See all” section (Figure 6-6).
I picked the Messaging app and, just as a quick reality check, sent it to myself, as in Figure 6-7.
The message arrives, as in Figure 6-8!
Note that if you later go back to the app that started the sharing, if there is room in the action bar, the app you chose to share with (in my case, Messaging) appears beside the Share icon—a neat optimization!
Ian Darwin
Use the FragmentManager
and layouts to arrange Fragment
views.
Fragments were introduced with Android 3.0 and have become increasingly common since. It’s possible to use them in a simple Activity, as in Example 6-7.
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
activity_main
);
}
Surprisingly, for the simplest use of a single Fragment, there is no code needed. The work is done in the layout, as in Example 6-8.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
xmlns:tools=
"http://schemas.android.com/tools"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:fitsSystemWindows=
"true"
tools:context=
"com.androidcookbook.fragmentsimple.MainActivity"
>
<android.support.design.widget.AppBarLayout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:theme=
"@style/AppTheme.AppBarOverlay"
>
<android.support.v7.widget.Toolbar
android:id=
"@+id/toolbar"
android:layout_width=
"match_parent"
android:layout_height=
"?attr/actionBarSize"
android:background=
"?attr/colorPrimary"
app:popupTheme=
"@style/AppTheme.PopupOverlay"
/>
</android.support.design.widget.AppBarLayout>
<fragment
android:id=
"@+id/fragment"
android:name=
".MainActivityFragment"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
app:layout_behavior=
"@string/appbar_scrolling_view_behavior"
tools:layout=
"@layout/fragment_main"
/>
</android.support.design.widget.CoordinatorLayout>
Notice that unlike with regular View
s (such as Button
and TextView
), where the name is the simple name of the class, the fragment
element is not capitalized, but treated specially; you always subclass Fragment
, so the actual class name must be provided by the android:name
attribute.
The Fragment
itself is a class in our application, so it exists as a Java class, shown in Example 6-9. And a Fragment
has a View
, so it has a layout XML file, shown in Example 6-10.
public
class
MainActivityFragment
extends
Fragment
{
public
MainActivityFragment
()
{
// Constructor with arguments may be needed in more sophisticated apps
}
/** Like Menus, Fragments must be inflated by the developer */
@Override
public
View
onCreateView
(
LayoutInflater
inflater
,
ViewGroup
container
,
Bundle
savedInstanceState
)
{
return
inflater
.
inflate
(
R
.
layout
.
fragment_main
,
container
,
false
);
}
}
<RelativeLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:tools=
"http://schemas.android.com/tools"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
tools:context=
"com.androidcookbook.fragmentsdemos.MainActivityFragment"
tools:showIn=
"@layout/activity_main"
>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Hello World!"
/>
</RelativeLayout>
In more sophisticated Activities, one will typically use the FragmentManager
to load Fragments into the Activity dynamically. The FragmentManager
is transaction-based; a simple use might be something like:
Fragment
frag
=
new
MyDemoFragment
();
FragmentTransaction
tx
=
getFragmentManager
();
tx
.
beginTransaction
();
tx
.
add
(
container
,
frag
);
// Or: tx.replace(container, newFragment);
tx
.
commit
();
This can also be written in a fluent API style, as:
getFragmentManager
().
beginTransaction
()
.
add
(
container
,
new
MyDemoFragment
())
.
commit
();
In a support library–based Activity, use getSupportFragmentManager()
instead of getFragmentManager()
.
One main use of the Fragment
subclasses is to have multiple views in place concurrently. Consider a list-detail application. In a small-screen device, or in a device that is narrow when in portrait mode, it makes sense to have a single view—either the list of items or the details of one item—onscreen at a time. However, on a tablet in landscape mode, where you have lots of width available, it makes sense to have the list and one item’s details side-by-side (see Figure 6-9).
As you can only have one Activity onscreen in an application, this would require a lot of shuffling
around of View
objects if you were to try to implement it using a single Activity.
Using Fragments, however, makes it much easier.
Fragments can be thought of as mini-Activities in some ways, but they must reside within an actual Activity. In our list-detail example, we will have a DisplayActivity
, with both a ListFragment
and a DetailFragment
contained within it. In portrait mode, only the ListFragment
will be shown, but in landscape mode on any device of reasonable size, the two Fragments will display side-by-side. This is commonly performed by using different view configurations based on the resources system, and triggering on this in the code to either use the FragmentManager
to install the second Fragment, or to start the detail view as an Activity so it appears in front of the List. In our demo we have a TaskListActivity
, which is launched from the Tasks menu item in the action bar; TaskListActivity
contains the code in Example 6-11 (slightly simplified).
holder
.
mView
.
setOnClickListener
(
new
View
.
OnClickListener
()
{
@Override
public
void
onClick
(
View
v
)
{
// This view will be created (but empty) on wide devices; show DetailFragment
if
(
findViewById
(
R
.
id
.
task_detail_container
)
!=
null
)
{
Bundle
arguments
=
new
Bundle
();
arguments
.
putString
(
TaskDetailFragment
.
ARG_ITEM_ID
,
holder
.
mItem
.
id
);
TaskDetailFragment
fragment
=
new
TaskDetailFragment
();
fragment
.
setArguments
(
arguments
);
// Replace the empty container with the actual DetailFragment
getSupportFragmentManager
().
beginTransaction
()
.
replace
(
R
.
id
.
task_detail_container
,
fragment
)
.
commit
();
}
else
{
// Not on a wide device, show the DetailActivity
Context
context
=
v
.
getContext
();
Intent
intent
=
new
Intent
(
context
,
TaskDetailActivity
.
class
);
intent
.
putExtra
(
TaskDetailFragment
.
ARG_ITEM_ID
,
holder
.
mItem
.
id
);
context
.
startActivity
(
intent
);
}
}
});
The layout configuration for the container is as follows:
Contains the List
(RecyclerView
; see Recipe 8.1) with no ViewGroup
around it.
Contains the List
in a horizontal LinearLayout
, which also has an empty FrameLayout
with android:id="@+id/task_detail_container"
and android:layout_width="0dp"
, the latter causing it not to show until it is replaced by the Fragment. The fact that task_detail_container
only exists in the wide view is tested in the code to control the switch between Fragment and Activity, so the code will display correctly if the user switches the device orientation while the app is running.
Note that a Fragment is not an Activity. All the same life-cycle methods (Recipe 1.2) are available, but there are several additional ones. The following should be kept in mind:
Call getActivity()
in the Fragment for API calls that need a Context or Activity reference.
Implement onCreateView()
to connect your View
with that of the Activity.
Implement onActivityCreated()
to be notified when your owning Activity is completely set up.
Note that onCreateView()
(rather than onCreate()
) is where you should create your View
, and that you are responsible for inflating the View
yourself. For example:
public
View
onCreateView
(
LayoutInflater
inf
,
ViewGroup
container
)
{
return
inf
.
inflate
(
R
.
layout
.
This_Fragment
'
s_Layout
,
container
,
false
)
;
}
The source code for this example is in the Android Cookbook repository, in the subdirectory FragmentsDemos (see “Getting and Using the Code Examples”).
Ian Darwin
Creating a button in your layout is simple. In the XML layout, you can create a button like so:
<Button
android:id=
"@+id/start_button"
android:text=
"@string/start_button_label"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
/>
In your Activity’s onCreate()
method, find the button by its ViewID
(in this example, R.id.start_button
). Call its setOnClickListener()
method with an OnClickListener
.
In the OnClickListener
implementation, check for the ViewID
and perform the relevant action:
public
class
MainActivity
extends
Activity
implements
OnClickListener
{
public
void
onCreate
()
{
startButton
=
findViewById
(
R
.
id
.
start_button
);
startButton
.
setOnClickListener
(
this
);
...
}
@Override
public
void
onClick
(
View
v
)
{
switch
(
v
.
getId
())
{
case
R
.
id
.
start_button
:
// Start whatever it is the start button starts...
...
case
R
.
id
.
some_other_button
:
// etc.
}
}
}
An experienced Java programmer would expect to use an anonymous inner class for the onClickListener
, as has been done in AWT and Swing since Java 1.1. For performance reasons, early Android documentation recommended against this, suggesting instead that you have the Activity implement OnClickListener
and check the ViewID
(i.e., analogous to the Java 1.0 way of doing things). As with Swing, as the power of devices has increased such old-style ways of doing things are becoming less popular, though you will likely still see both styles in use for some time.
Rachee Singh
Use an image button. This requires less effort than a text view with descriptive text, since an image can explain the scenario much better than a lot of words can.
Making your own image button requires defining the characteristics of the button in an XML file that should be placed in /res/drawable. This file specifies the three states of an image button:
Pressed state
Focused state
Other states (optional)
For instance:
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<item
android:drawable=
"@drawable/play_pressed"
android:state_checked=
"true"
/>
<item
android:drawable=
"@drawable/play"
/>
</selector>
So, for each of these states, the ID of an image is specified (the image present in /res/drawable as a .png file). When the button is pressed, the play_pressed
image is displayed. There are two such buttons in the sample application: the Play button and the Settings button. In the .java file of the application, the onClick
aspect of the buttons can be taken care of. In this recipe, a toast is displayed with some appropriate text. Programmers can start a new Activity from here, or broadcast an Intent, or do many other things based on their requirements.
In Figure 6-10, the left screenshot shows the Play button not pressed, and the right screenshot shows the Play button pressed.
The source code for this example is in the Android Cookbook repository, in the subdirectory ImageButtonDemo (see “Getting and Using the Code Examples”).
Ian Darwin
You want a round graphic button that will appear in front of your application (similar to the “Add” button found on many Google apps, such as the GMail app in Figure 6-11), and you want to respond to this button being pressed.
Use a FloatingActionButton
.
The FloatingActionButton
appears in the lower right of your application window and is often used for a rounded “+”
button with an associated action such as adding a contact, creating a new message to be sent, and so on.
While there have always been ways to provide this functionality, it is now available as a supported component (in the support library; see Recipe 1.21) in Android.
It is as easy to use as a regular button; just add it to your XML layout, like so:
<android.support.design.widget.FloatingActionButton
android:id=
"@+id/floatingButton"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_gravity=
"bottom|end"
android:layout_margin=
"@dimen/fab_margin"
android:src=
"@android:drawable/ic_dialog_info"
/>
Because this button is in the support library rather than in android.widget
, we have to list its full class
in the layout file. We give it an id
so we can refer to it. The gravity and padding are recommended for
the button to appear in the lower right. We use src
to indicate the drawable to display inside the circular
button (it’s named as it is to remind us that we’re not providing the complete drawable, unlike with an ImageButton
), and use either android:onClick
in the XML or findViewById()
and setOnclickListener()
in code
to specify what to do when the button is tapped. This example uses android:onClick="runMe"
in the XML and the following code:
public
void
runMe
(
View
v
)
{
final
String
msg
=
"You pressed my button"
;
Log
.
d
(
TAG
,
msg
);
Toast
.
makeText
(
this
,
msg
,
Toast
.
LENGTH_SHORT
).
show
();
}
The result is shown in Figure 6-12.
The new Snackbar
component, introduced at around the same time as the FloatingActionButton
, is discussed in Recipe 7.1.
Android Studio’s New Activity wizard includes a “Basic Activity” choice that provides a preconfigured FloatingActionButton
, which out of the box shows a Snackbar.
The source code for this example is in the Android Cookbook repository, in the subdirectory FloatingButtonSnackbarDemo (see “Getting and Using the Code Examples”).
Daniel Fowler
When writing software, very rarely is there only one way to do things. This holds true when wiring up View
events; half a dozen techniques are shown in this recipe.
When a View
fires an event, an application will not respond to it unless the app is listening for it. To detect the event, a listener is instantiated and assigned to the View
. Take, for example, the onClick
event, the most widely used event in Android apps. Nearly every View
that can be added to an app screen will fire the event when the user taps it (on touch screens) or presses the trackpad/trackball when the View
has focus. This event is listened to by a class implementing the OnClickListener
interface. The class instance is then assigned to the required View
using the View
’s setOnClickListener()
method.
In the following section, the HandleClick
inner class sets the text of a TextView
(textview1
) when a Button
(button1
) is pressed.
In Example 6-12, a nested class called HandleClick
implementing OnClickListener
is declared as a member of the Activity
(main
). This is useful when several listeners require similar processing that can be handled by a single class.
public
class
MainActivity
extends
Activity
{
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
// Attach an instance of HandleClick to the Button
findViewById
(
R
.
id
.
button1
).
setOnClickListener
(
new
HandleClick
());
}
private
class
HandleClick
implements
OnClickListener
{
public
void
onClick
(
View
arg0
)
{
Button
btn
=
(
Button
)
arg0
;
// Cast view to a button
// Get a reference to the TextView
TextView
tv
=
(
TextView
)
findViewById
(
R
.
id
.
textview1
);
// Update the TextView text
tv
.
setText
(
"You pressed "
+
btn
.
getText
());
}
}
}
A variation on this would involve making the inner class public and putting it in its own source file. In theory, any Activity that needed a copy of such a class could create its own instance thereof. It is relatively rare, however, that action listeners are generic enough to be shared in this way.
In Java an interface can be used as a type. A variable is declared as an OnClickListener
and assigned using new OnClickListener() {}
, while behind the scenes Java is creating an object (an anonymous class) that implements OnClickListener
. This has similar benefits to the first technique (see Example 6-13).
When an instance is being saved, many Java developers (including this Cookbook’s main author) consider it a good practice to use interface variables rather than the specific implementation type for the variable.
public
class
MainActivity
extends
Activity
{
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
// Use the handleClick variable to attach the event listener
findViewById
(
R
.
id
.
button1
).
setOnClickListener
(
handleClick
);
}
private
OnClickListener
handleClick
=
new
OnClickListener
()
{
public
void
onClick
(
View
arg0
)
{
Button
btn
=
(
Button
)
arg0
;
TextView
tv
=
(
TextView
)
findViewById
(
R
.
id
.
textview1
);
tv
.
setText
(
"You pressed "
+
btn
.
getText
());
}
};
}
Declaring the OnClickListener
within the call to the setOnClickListener()
method is common. This method is useful when each listener does not have functionality that could be shared with other listeners, though some novice developers find this type of code difficult to understand. Again, Java is creating an object that implements the interface behind the scenes for new OnClickListener() {}
(see Example 6-14).
public
class
MainActivity
extends
Activity
{
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
findViewById
(
R
.
id
.
button1
).
setOnClickListener
(
new
OnClickListener
()
{
public
void
onClick
(
View
arg0
)
{
Button
btn
=
(
Button
)
arg0
;
TextView
tv
=
(
TextView
)
findViewById
(
R
.
id
.
textview1
);
tv
.
setText
(
"You pressed "
+
btn
.
getText
());
}
});
}
}
The Activity
itself can implement the OnClickListener
(see Example 6-15). Since the Activity
object (main
) already exists, this saves a small amount of memory by not requiring another object to host the onClick()
method. It does make public a method that is unlikely to be used elsewhere, however, and implementing multiple events will make the declaration of main
long.
public
class
MainActivity
extends
Activity
implements
OnClickListener
{
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
findViewById
(
R
.
id
.
button1
).
setOnClickListener
(
this
);
}
public
void
onClick
(
View
arg0
)
{
Button
btn
=
(
Button
)
arg0
;
TextView
tv
=
(
TextView
)
findViewById
(
R
.
id
.
textview1
);
tv
.
setText
(
"You pressed "
+
btn
.
getText
());
}
}
In Android 7 and later, using an up-to-date SDK which should include the Java 8 tooling, you can use a lambda expression to shorten the code.
Lambdas can be recognized by the ->
syntax, introduced into the Java language with Java SE version 8.
An example use of a lambda expression is shown in Example 6-16.
public
class
MainActivity
extends
Activity
{
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
View
v
=
findViewById
(
R
.
id
.
button1
);
v
.
setOnClickListener
(
v
->{
Button
btn
=
(
Button
)
arg0
;
TextView
tv
=
(
TextView
)
findViewById
(
R
.
id
.
textview1
);
tv
.
setText
(
"You pressed "
+
btn
.
getText
());
});
}
}
One restriction is that a lambda expression can only be used to implement a functional interface—that is, an interface that has only a single abstract method in it—since the name of the method (as well as the name of the interface itself) is inferred by the compiler. Fortunately, most “action event” listener interfaces are functional.
The name of a method defined in the Activity
can be assigned to the android:onClick
attribute in a layout file (see Example 6-17). This can save you from having to write a lot of boilerplate code.
public
class
MainActivity
extends
Activity
{
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
}
public
void
handleClick
(
View
arg0
)
{
Button
btn
=
(
Button
)
arg0
;
TextView
tv
=
(
TextView
)
findViewById
(
R
.
id
.
textview1
);
tv
.
setText
(
"You pressed "
+
btn
.
getText
());
}
}
In the layout file the Button
would be declared with the android:onClick
attribute:
<Button
android:id=
"@+id/button1"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Button 1"
android:onClick=
"handleClick"
/>
The first five techniques for handling events can be used with other event types (onLongClick
, onKey
, onTouch
, onCreateContextMenu
, onFocusChange
). The technique described in this subsection only applies to the onClick
event. The layout file in Example 6-18 declares an additional two buttons. Using the android:onClick
attribute, you don’t need an additional code is required than that defined earlier; that is, no additional findViewById()
and setOnClickListener()
method for each button. This should appear as in Figure 6-13.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:orientation=
"vertical"
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
>
<TextView
android:id=
"@+id/textview1"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Click a button."
android:textSize=
"20dp"
/>
<LinearLayout
android:orientation=
"horizontal"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
>
<Button
android:id=
"@+id/button1"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Button 1"
android:onClick=
"HandleClick"
/>
<Button
android:id=
"@+id/button2"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Button 2"
android:onClick=
"HandleClick"
/>
<Button
android:id=
"@+id/button3"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"Button 3"
android:onClick=
"HandleClick"
/>
</LinearLayout>
</LinearLayout>
Deciding which technique to use to wire up a listener will depend on the functionality required, how much code is reusable across View
s, and how easy the code will be for future maintainers to understand. Ideally the code should be succinct and easy to read.
Blake Meike
Use CheckBox
es or RadioButton
s as appropriate.
These views are probably familiar to you from other user interfaces. They allow the user to choose from multiple options. CheckBox
is typically used when you want to offer multiple selections with a yes/no or true/false choice for each. RadioButton
is used when only one choice is allowed at a time.
Android has adapted these familiar components to make them more useful in a touch-screen environment. Figure 6-14 shows these multiple-choice views laid out in an Android application.
The layout XML file that created the screen in Figure 6-14 looks like this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
android:orientation=
"vertical"
>
<CheckBox
android:id=
"@+id/cbxBox1"
android:layout_width=
"32dp"
android:layout_height=
"32dp"
android:checked=
"false"
/>
<TextView
android:id=
"@+id/txtCheckBox"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"CheckBox: Not checked"
/>
<RadioGroup
android:id=
"@+id/rgGroup1"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
>
<RadioButton
android:id=
"@+id/RB1"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Button1"
/>
<RadioButton
android:id=
"@+id/RB2"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Button2"
/>
<RadioButton
android:id=
"@+id/RB3"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Button3"
/>
</RadioGroup>
<TextView
android:id=
"@+id/txtRadio"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"RadioGroup: Nothing picked"
/>
</LinearLayout>
The XML file just lists each view we want on the screen along with the attributes we want. A RadioGroup
is also a ViewGroup
, so it can contain the appropriate RadioButton
views.
Example 6-19 is the Java file that responds to user clicks.
package
com
.
androidcookbook
.
checkboxradiobutton
;
import
android.app.Activity
;
import
android.os.Bundle
;
import
android.view.Menu
;
import
android.view.View
;
import
android.widget.CheckBox
;
import
android.widget.RadioButton
;
import
android.widget.RadioGroup
;
import
android.widget.TextView
;
public
class
SelectExample
extends
Activity
{
private
CheckBox
checkBox
;
private
TextView
txtCheckBox
,
txtRadio
;
/** Called when the Activity is first created. */
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
activity_select_example
);
checkBox
=
(
CheckBox
)
findViewById
(
R
.
id
.
cbxBox1
);
txtCheckBox
=
(
TextView
)
findViewById
(
R
.
id
.
txtCheckBox
);
// React to events from the CheckBox
checkBox
.
setOnClickListener
(
new
CheckBox
.
OnClickListener
()
{
public
void
onClick
(
View
v
)
{
if
(
checkBox
.
isChecked
())
{
txtCheckBox
.
setText
(
"CheckBox: Box is checked"
);
}
else
{
txtCheckBox
.
setText
(
"CheckBox: Not checked"
);
}
}
});
final
RadioGroup
rg
=
(
RadioGroup
)
findViewById
(
R
.
id
.
rgGroup1
);
// React to events from the RadioGroup
rg
.
setOnCheckedChangeListener
(
new
RadioGroup
.
OnCheckedChangeListener
()
{
@Override
public
void
onCheckedChanged
(
RadioGroup
group
,
int
checkedId
)
{
RadioButton
rb
=
(
RadioButton
)
findViewById
(
rg
.
getCheckedRadioButtonId
());
txtRadio
.
setText
(
rb
.
getText
()
+
" picked"
);
}
});
txtRadio
=
(
TextView
)
findViewById
(
R
.
id
.
txtRadio
);
}
@Override
public
boolean
onCreateOptionsMenu
(
Menu
menu
)
{
getMenuInflater
().
inflate
(
R
.
menu
.
activity_select_example
,
menu
);
return
true
;
}
}
These views work as follows:
CheckBox
The CheckBox
view takes care of flipping its state back and forth and displaying a checkmark when the state is true
, but not when it is false
. All you have to do is create an OnClickListener
to catch click events, and you can add whatever code you want to react.
RadioGroup
As mentioned earlier, the RadioGroup
view is really a ViewGroup
that contains any number of RadioButton
views. The user can select only one of the buttons at a time, and you capture the selection either by setting a RadioGroup.OnCheckedChangeListener
on the group or by setting OnClickListener
s for each RadioButton
.
RadioButton
A RadioButton
is one of the buttons to be placed into a RadioGroup
; it displays its given text as well as a circle to indicate which one of the buttons in the group is currently selected.
Taken together, these views let you provide a short set of choices and have the user select one or multiple choices from those offered.
Ian Darwin
Use a Card
when you want self-contained ViewGroup
s with a nice border, commonly many of them in a ListView
or a RecyclerView
.
Card
is a new ViewGroup
, subclassing FrameLayout
, that provides a border and shadow.
It is part of the compatibility package, so that it can work with old as well as new versions of Android.
Card
is easy to use as long as you remember that it is a FrameLayout
(see Recipe 6.3).
Items placed directly in a FrameLayout
appear in a stack, and if they are different sizes, parts of various ones will be visible.
In our example, which is drawn from a hypothetical real estate home listing project,
we place a RelativeLayout
with a photograph (ImageView
) and some descriptive text (TextView
)
together in a Card
, and place the Card
in our main layout.
We are using default shadow and border settings, but overriding the size and corner radius.
The Card
’s layout file is shown in Example 6-20.
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:card_view=
"http://schemas.android.com/apk/res-auto"
xmlns:tools=
"http://schemas.android.com/tools"
android:id=
"@+id/card_view"
android:layout_gravity=
"center"
android:layout_width=
"300dp"
android:layout_height=
"340dp"
card_view:cardCornerRadius=
"6dp"
tools:context=
"com.androidcookbook.carddemo.CardActivity"
>
<RelativeLayout
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:orientation=
"vertical"
>
<ImageView
android:id=
"@+id/house_front_view"
android:layout_alignParentTop=
"true"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
/>
<TextView
android:id=
"@+id/house_descr"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:layout_alignParentBottom=
"true"
/>
</RelativeLayout>
</android.support.v7.widget.CardView>
The code that shows this Card
’s layout only populates one Card
(see Example 6-21), but it would be easy to turn that into an adapter for use on a ListView
or RecyclerView
, and the layout file is also already suitable for such use.
public
class
CardActivity
extends
AppCompatActivity
{
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
card_layout
);
// Dynamically set the image and text
ImageView
img
=
(
ImageView
)
findViewById
(
R
.
id
.
house_front_view
);
Drawable
d
=
ContextCompat
.
getDrawable
(
this
,
R
.
drawable
.
fixer_upper_1
);
img
.
setImageDrawable
(
d
);
TextView
descr
=
(
TextView
)
findViewById
(
R
.
id
.
house_descr
);
descr
.
setText
(
"Beautiful fixer-upper! Only used on weekends
by respectable Hobbit couple!"
);
}
}
The results of these few lines of code can be seen in Figure 6-15.
The developer documentation on creating lists and cards.
The source code for this example is in the Android Cookbook repository, in the subdirectory CardDemo (see “Getting and Using the Code Examples”).
Ian Darwin
You want to offer a drop-down choice.
Generally known as a combo box, the spinner is the analog of the HTML select
element or the Swing JComboBox
. It provides a drop-down chooser whose values appear to float over the screen when the spinner is clicked. One item can be selected and the floating version will pop down, displaying the selection in the spinner (see Figure 6-16).
Like all standard components, the spinner can be created and customized in XML or in code. In this example, the term “context” or “reading context” is used to indicate when a patient’s blood pressure reading was taken (after breakfast, after lunch, etc., as shown in Figure 6-16), so that the health care practitioner can understand the value in context of the patient’s day. Here is an excerpt from res/layout/main.xml:
<Spinner
android:id=
"@+id/contextChooser"
android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
android:prompt=
"@string/context_choice"
/>
Ideally the list of values will come from a resource file, so as to be internationalizable. Here is the file res/values/contexts.xml containing the XML values for the list of times to choose:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
name=
"context_choice"
>
When Reading Taken</string>
<string-array
name=
"context_names"
>
<item>
Breakfast</item>
<item>
Lunch</item>
<item>
Dinner</item>
<item>
Snack</item>
</string-array>
</resources>
To tie the list of strings to the spinner at compile time, just use <Spinner android:entries="@array/context_names" …>
in the layout file.
This is the simplest way to hook the list into the spinner, and is used for “Chooser1” in the example code.
To tie the list of strings to the spinner at runtime, just look up the Spinner
object and set the values, as shown in Example 6-22. You might want to do so if you need to modify any of the list entries at runtime, for example, or if the list is coming
from a Java language enum
. “Chooser2” in Example 6-22 gets the list from the XML, and “Chooser3” gets it from the Java enum.
// Spinner 1 gets its labels automatically from an XML array
Spinner
contextChooser1
=
(
Spinner
)
findViewById
(
R
.
id
.
contextChooser1
);
contextChooser1
.
setOnItemSelectedListener
(
listener
);
// Spinner 2 gets its labels programmatically from the XML array
Spinner
contextChooser2
=
(
Spinner
)
findViewById
(
R
.
id
.
contextChooser2
);
ArrayAdapter
<
CharSequence
>
adapter2
=
ArrayAdapter
.
createFromResource
(
this
,
R
.
array
.
reading_context_names
,
android
.
R
.
layout
.
simple_spinner_item
);
contextChooser2
.
setAdapter
(
adapter2
);
contextChooser2
.
setOnItemSelectedListener
(
listener
);
// Spinner 3 gets its labels programmatically from a Java language enum
Spinner
contextChooser3
=
(
Spinner
)
findViewById
(
R
.
id
.
contextChooser3
);
ArrayAdapter
<
ReadingContext
>
adapter3
=
new
ArrayAdapter
<
ReadingContext
>(
this
,
android
.
R
.
layout
.
simple_spinner_item
,
ReadingContext
.
values
());
contextChooser3
.
setAdapter
(
adapter3
);
contextChooser3
.
setOnItemSelectedListener
(
listener
);
That is all you need in order for the spinner to appear, and to allow the user to select items (see Figure 6-16). If you want to know the chosen value as soon as the user has selected it, you can send an OnItemSelectedListener
instance to the Spinner
class’s setOnItemSelectedListener()
method. This interface has two callback methods, setItemSelected()
and setNothingSelected()
. Both are called with the Spinner
(but the argument is declared as a ViewAdapter
); the former method is called with two integer arguments, the list position and the identity of the selected item, like so:
OnItemSelectedListener
listener
=
new
OnItemSelectedListener
()
{
@Override
public
void
onItemSelected
(
AdapterView
<?>
spinner
,
View
arg1
,
int
pos
,
long
id
)
{
Toast
.
makeText
(
SpinnerDemoActivity
.
this
,
"You selected "
+
spinner
.
getSelectedItem
(),
Toast
.
LENGTH_SHORT
).
show
();
}
@Override
public
void
onNothingSelected
(
AdapterView
<?>
spinner
)
{
Toast
.
makeText
(
SpinnerDemoActivity
.
this
,
"Nothing selected."
,
Toast
.
LENGTH_SHORT
).
show
();
}
};
On the other hand, you may not need the value from the spinner until the user fills in multiple items and clicks a button. In this case, you can simply call the Spinner
class’s getSelectedItem()
method, which returns the item placed in that position by the Adapter. Assuming you placed strings in the list, you can just call toString()
to get back the given String
value.
The source code for this project is in the Android Cookbook repository, in the subdirectory SpinnerDemo (see “Getting and Using the Code Examples”).
Ian Darwin
The View
class has a method to enable/disable long-click support, setLongClickable(boolean)
, and the corresponding setOnLongClickListener(OnLongClickListener)
method. In Example 6-23 we listen for long-clicks on a View
and respond by popping up a PopupMenu
, which will be modal and will appear in front of the ListView
.
final
View
myView
=
findViewById
(
R
.
id
.
myView
);
...
myView
.
setOnLongClickListener
(
new
OnLongClickListener
()
{
@Override
public
boolean
onLongClick
(
View
view
)
{
PopupMenu
p
=
new
PopupMenu
(
Main
.
this
,
view
);
p
.
getMenuInflater
().
inflate
(
R
.
layout
.
main_popup_menu
,
p
.
getMenu
());
p
.
show
();
return
true
;
}
});
The pop-up menu will be dismissed when you click one of its items; the list of menu items comes from the XML file res/menu/main_popup_menu.xml, which just contains a series of item
elements with the text for the menu items.
Note that calling setOnLongClickListener()
has the side effect of calling setLongClickEnabled(true)
.
Note also that adding an OnClickListener
to a ListView
(or other multi-item view) does not work as you might expect; the list items simply get dispatched as per a normal click. Instead, you must use the setOnItemLongClickListener()
method that takes, unsurprisingly, an instance of OnItemLongClickListener()
, which will be invoked when you long-press on an item in the list.
In fact, you can even simplify this for a ListView
by preinflating your menu and passing it to the Activity
’s setContextMenu(view, menu)
method.
Ian Darwin
Use a TextView
when you want the user to have read-only access to text; this includes what most other GUI API packages call a Label
, there being no explicit Label
class in android.widget
. Use an EditText
when you want the user to have read/write access to text; this includes what other packages may call a TextField
or a TextArea
.
EditText
is a direct subclass of TextView
. Note that EditText
has many direct and indirect subclasses, many of which are GUI controls in their own right, such as CheckBox
, RadioButton
, and the like. A further subclass is the AutoCompleteTextView
, which, as the name implies, allows for auto-completion when the user types the first few letters of some data item. As with the recipes in Chapter 8, there is an adapter to provide the completable text items.
Placing an EditText
or TextView
is trivial using the XML layout. Assigning the initial value to be displayed is also simple using XML. It is possible to set the value directly using the following:
<TextView
android:text=
"Welcome!"
/>
However, it is preferable to use a value like "@string/welcome_text"
and define the string in strings.xml so that it can be changed and internationalized more readily.
Since TextView
and EditText
are used throughout this book, we do not have a sample application that uses them. One is provided with the Android API Examples, called LabelView
, if you need it.
Daniel Fowler
When an application needs input from a user, sometimes only a specific type of value is required; maybe a whole number, a decimal number, a number between two values, or words that are capitalized. When defining an EditText
in a layout, attributes such as android:inputType
can be used to constrain what the user is able to type. This automatically reduces the amount of code required later on because there are fewer checks to perform on the data that was entered. The TextWatcher
interface is also useful for restricting values. In the following example an EditText
only allows a value between 0 and 100—for example, to represent a percentage. There is no need to check the value because it is all done as the user types.
Here is a simple layout with one such EditText
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:orientation=
"vertical"
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
>
<EditText
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:id=
"@+id/percent"
android:text=
"0"
android:maxLength=
"3"
android:inputType=
"number"
/>
</LinearLayout>
The EditText
is given a starting value of zero with android:text="0"
, and the number of characters that can be typed has been limited to three with android:maxLength="3"
because the largest number we need, 100, only has three digits. Finally, the user is restricted to only positive numbers with android:inputType="number"
.
It is a good idea to review the attributes that Android views support, because defining views in the XML layout can reduce the amount of code you need to write. For further details on the attributes supported by EditText
see the Android documentation on the TextView
, from which EditText
is subclassed.
Within Example 6-24’s Activity
class, an inner class can be used to implement the TextWatcher
interface (or, for example, the Activity
class itself could implement the interface). The afterTextChanged()
method is overridden and will be called when the text changes as the user types. In this method the value being typed is checked to see if it is greater than 100. If so, it is set to 100. There is no need to check for values less than zero because they cannot be entered, because of the XML attributes. The try-catch
is needed for when all the numbers are deleted, in which case the test for values greater than 100 would cause an exception (trying to parse an empty string).
TextWatcher
also has a beforeTextChanged()
and an onTextChanged()
method that you can override, but they are not used in this example.
class
CheckPercentage
implements
TextWatcher
{
@Override
public
void
afterTextChanged
(
Editable
s
)
{
try
{
Log
.
d
(
"Percentage"
,
"input: "
+
s
);
if
(
Integer
.
parseInt
(
s
.
toString
())>
100
)
s
.
replace
(
0
,
s
.
length
(),
"100"
);
}
catch
(
NumberFormatException
nfe
)
{
// Empty
}
}
@Override
public
void
beforeTextChanged
(
CharSequence
s
,
int
start
,
int
count
,
int
after
)
{
// Not used, details on text just before it changed
// Used to track in detail changes made to text, e.g. to implement an undo
}
@Override
public
void
onTextChanged
(
CharSequence
s
,
int
start
,
int
before
,
int
count
)
{
// Not used, details on text at point change made
}
}
Finally, in the onCreate()
method for the Activity, the inner class implementing TextWatcher
is connected to the EditText
using its addTextChangedListener()
method:
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
EditText
percentage
=(
EditText
)
findViewById
(
R
.
id
.
percent
);
percentage
.
addTextChangedListener
(
new
CheckPercentage
());
}
Note that it is fine to change the EditText
value in afterTextChanged()
as its internal Editable
class is passed in. However, you cannot change it by altering the CharSequence
passed into beforeTextChanged()
and onTextChanged()
.
Running this example, with Logcat running, should show the values being set, as shown in Figure 6-17.
Also remember that if you change the value in the EditText
, it will cause the afterText
Changed()
method to be called again. Care must be taken to ensure that the code using TextWatcher
does not result in endless looping.
The developer documentation on the TextView
class, EditText
class, and TextWatcher
interface.
Rachee Singh
Use the AutoCompleteTextView
widget that acts as a cross between an EditText
and a Spinner
, enabling auto-completion.
The demo layout here includes a TextView
that supports auto-completion. Auto-completion is done using an AutoCompleteTextView
widget. Example 6-25 shows the layout XML code.
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:orientation=
"vertical"
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
>
<TextView
android:id=
"@+id/field"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
/>
<AutoCompleteTextView
android:id=
"@+id/autocomplete"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:completionThreshold=
"2"
/>
</LinearLayout>
The completionThreshold
field in the AutoCompleteTextView
sets the minimum number of characters that the user has to enter in the TextView
so that auto-completion options corresponding to his input to show up.
The Activity
in which we are implementing auto-completion should implement TextWatcher
so that we can override the onTextChanged()
method:
public
class
AutoComplete
extends
Activity
implements
TextWatcher
{
We need to override the unimplemented methods: onTextChanged()
, beforeTextChanged()
, and afterTextChanged()
.
We also require three fields:
A handle onto the TextView
A handle onto the AutoCompleteTextView
A list of String
items from which the auto-completion will choose
These three items are shown here:
private
TextView
field
;
private
AutoCompleteTextView
autocomplete
;
String
autocompleteItems
[]
=
{
"apple"
,
"banana"
,
"mango"
,
"pineapple"
,
"apricot"
,
"orange"
,
"pear"
,
"grapes"
};
Our onTextChanged()
method, shown next, simply copies the current value of the text field into another text field—this is not mandatory, but in this demo it will show you what values are being set in the auto-completion component:
@Override
public
void
onTextChanged
(
CharSequence
arg0
,
int
arg1
,
int
arg2
,
int
arg3
)
{
field
.
setText
(
autocomplete
.
getText
());
}
In the onCreate()
method of the same Activity
, we get a handle on the TextView
and the AutoCompleteTextView
components of the layout. We also set a String
adapter for the AutoCompleteTextView
:
setContentView
(
R
.
layout
.
main
);
field
=
(
TextView
)
findViewById
(
R
.
id
.
field
);
autocomplete
=
(
AutoCompleteTextView
)
findViewById
(
R
.
id
.
autocomplete
);
autocomplete
.
addTextChangedListener
(
this
);
autocomplete
.
setAdapter
(
new
ArrayAdapter
<
String
>(
this
,
android
.
R
.
layout
.
simple_dropdown_item_1line
,
autocompleteItems
));
You can download the source code for this example from Google Docs.
Jonathan Fuerth
There are two extra twists to using SimpleCursorAdapter
instead of ArrayAdapter
:
You need to tell the Adapter which column to use to fill the TextView
after the user selects a completion.
You need to tell the Adapter how to requery based on the user’s latest input in the text field. Otherwise, it shows all rows returned by the cursor and the list never shrinks to include the items of actual interest.
The following example code would typically be found in the onCreate()
method of the Activity that contains the AutoCompleteTextView
. It retrieves the AutoCompleteTextView
from the Activity’s layout, creates a SimpleCursorAdapter
, configures that SimpleCursorAdapter
to work with the AutoCompleteTextView
, and then assigns the Adapter to the View.
The two important differences from the ArrayAdapter
example in the Android Developers Guide are marked in Example 6-26.
final
AutoCompleteTextView
itemName
=
(
AutoCompleteTextView
)
findViewById
(
R
.
id
.
item_name_view
)
;
SimpleCursorAdapter
itemNameAdapter
=
new
SimpleCursorAdapter
(
this
,
R
.
layout
.
completion_item
,
itemNameCursor
,
fromCol
,
toView
)
;
itemNameAdapter
.
setStringConversionColumn
(
itemNameCursor
.
getColumnIndexOrThrow
(
GroceryDBAdapter
.
ITEM_NAME_COL
)
)
;
itemNameAdapter
.
setFilterQueryProvider
(
new
FilterQueryProvider
(
)
{
public
Cursor
runQuery
(
CharSequence
constraint
)
{
String
partialItemName
=
null
;
if
(
constraint
!
=
null
)
{
partialItemName
=
constraint
.
toString
(
)
;
}
return
groceryDb
.
suggestItemCompletions
(
partialItemName
)
;
}
}
)
;
itemName
.
setAdapter
(
itemNameAdapter
)
;
With ArrayAdapter
, there is no need to specify how to convert the user’s selection into a String
. However, SimpleCursorAdapter
supports using one column for the text of the suggestion, and a different column for the text that’s fed into the text field after the user selects a suggestion. Although the most common case is to use the same text for the suggestion as you get in the text field after picking it, this is not the default. The default is, as is often the case in Java, to use toString()
, e.g., to fill the text view with the toString()
representation of your cursor—something like android.database.sqlite.SQLiteCursor@f00f00d0
. That is not what you want!
With ArrayAdapter
, the system takes care of filtering the alternatives to display only those strings that start with what the user has typed into the text field so far. The SimpleCursorAdapter
is more flexible, but again, the default behavior is not useful. If you fail to write a FilterQueryProvider
for your adapter, the AutoCompleteTextView
will simply show the initial set of suggestions no matter what the user types. With the FilterQueryProvider
, the suggestions work as expected.
Rachee Singh
Android provides the password
attribute on the EditText
class, which provides the needed behavior.
If your application requires the user to enter a password, the EditText
being used should be special. It should hide the characters entered. This can be done by adding this property to the EditText
in XML:
android:inputType="textPassword"
Figure 6-18 shows how the password EditText
would look.
Jonathan Fuerth
Figure 6-19 shows a simple layout with three text fields (EditText
views) and a Submit button.
Note the Enter key at the bottom right. Pressing it causes the currently focused text field to expand vertically to accommodate another line of text. This is not what you normally want!
Here is the code for the layout in Figure 6-19:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:orientation=
"vertical"
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
>
<EditText
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Field 1"
/>
<EditText
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Field 2"
/>
<EditText
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Field 3"
/>
<Button
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_gravity=
"center_horizontal"
android:text=
"Submit"
/>
</LinearLayout>
Figure 6-20 shows a better version of the same UI, with a Next key where Enter was.
Besides being more convenient for users, this also prevents people from entering multiple lines of text into a field that was only intended to hold a single line.
Here’s how to tell Android to display a Next button on your keyboard. Note the android:imeOptions
attributes on each of the three EditText
views:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:orientation=
"vertical"
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
>
<EditText
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Field 1"
android:singleLine=
"true"
android:imeOptions=
"actionNext"
/>
<EditText
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Field 2"
android:singleLine=
"true"
android:imeOptions=
"actionNext"
/>
<EditText
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:text=
"Field 3"
android:singleLine=
"true"
android:imeOptions=
"actionDone"
/>
<Button
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_gravity=
"center_horizontal"
android:text=
"Submit"
/>
</LinearLayout>
Finally, notice the actionDone
on the third text field: the button that follows is not focusable in touch mode, and if it were, it wouldn’t display a keyboard anyway. As you might guess, actionDone
puts a Done button where the Enter key normally goes. Pressing the Done button simply hides the keyboard.
The android:singleLine
attribute is required for the imeOptions
to take effect.
The official Android documentation says that this “constrains the text to a single horizontally scrolling line instead of letting it wrap onto multiple lines, and advances focus instead of inserting a newline when you press the enter key.” This is marked as deprecated in the current SDK, but the replacement (android:maxLines="1"
) does not cause the imeOptions
to take effect.
There are a number of refinements you can make to the appearance of the software keyboard, including hints about the input type, suggested capitalization, and even select-all-on-focus behavior. They are all worth investigating. Every little touch can make your app more pleasurable to use.
The Android API documentation for TextView, especially the section on imeOptions
.
The source code for this project is in the Android Cookbook repository, in the subdirectory SoftKeyboardEnterNext (see “Getting and Using the Code Examples”).
Rachee Singh
If the application must react differently to different key-presses, you need to override the onKeyDown()
method in the Activity’s Java code. This method takes the KeyCode
as an argument so that, within a switch-case
block, different actions can be carried out (see Example 6-27).
public
boolean
onKeyDown
(
int
keyCode
,
KeyEvent
service
)
{
switch
(
keyCode
)
{
case
KeyEvent
.
KEYCODE_HOME
:
keyType
.
setText
(
"Home Key Pressed!"
);
break
;
case
KeyEvent
.
KEYCODE_DPAD_CENTER
:
keyType
.
setText
(
"Center Key Pressed!"
);
break
;
case
KeyEvent
.
KEYCODE_DPAD_DOWN
:
keyType
.
setText
(
"Down Key Pressed!"
);
break
;
// And so on...
}
}
You can download the source code for this example from Google Docs.
Ian Darwin
RatingBar
provides the now-familiar “rating” user interface experience, where a user is asked to rank or rate something using star classification (the RatingBar
doesn’t display the thing to be rated; that’s up to the rest of your app). RatingBar
is a subclass of ProgressBar
, extended to display a whole number of icons (the stars) in the bar. Its primary properties are:
numStars
The number of stars to display (int
)
rating
The user’s chosen rating (float
, because of stepSize
)
stepSize
The increment for selection (float
; common values are 1.0 and 0.5, depending on how fine-grained you want the rating to be)
isIndicator
A boolean
, set to true
to make this read-only
These properties are normally set in the XML:
<RatingBar
android:id=
"@+id/serviceBar"
android:gravity=
"center"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:numStars=
"5"
android:rating=
"3"
android:stepSize=
"1.0"
android:isIndicator=
'false'
/>
The RatingBar
maintains its rating value internally. You can find out how the user has rated the item in two ways:
Invoke the getRating()
method.
Provide a change notification listener of type OnRatingBarChangeListener
.
The OnRatingBarChangeListener
has a single method, onRatingChanged()
, called with three arguments:
RatingBar rBar
The event source, a reference to the particular RatingBar
float fRating
The rating that was set
boolean fromUser
true
if set by a user, false
if set programmatically
Example 6-28 simulates a customer survey; it creates two RatingBar
s, one to rate service and another to rate price (the XML for both is identical except for the android:id
). In the main program, an OnRatingBarChangeListener
is created to display touchy-feely–sounding feedback for the given rating (the rating is converted to an int
and a switch
statement is used to generate a message for the toast).
public
class
MainActivity
extends
Activity
{
/** Called when the Activity is first created. */
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
OnRatingBarChangeListener
barChangeListener
=
new
OnRatingBarChangeListener
()
{
@Override
public
void
onRatingChanged
(
RatingBar
rBar
,
float
fRating
,
boolean
fromUser
)
{
int
rating
=
(
int
)
fRating
;
String
message
=
null
;
switch
(
rating
)
{
case
1
:
message
=
"Sorry you're really upset with us"
;
break
;
case
2
:
message
=
"Sorry you're not happy"
;
break
;
case
3
:
message
=
"Good enough is not good enough"
;
break
;
case
4
:
message
=
"Thanks, we're glad you liked it."
;
break
;
case
5
:
message
=
"Awesome - thanks!"
;
break
;
}
Toast
.
makeText
(
Main
.
this
,
message
,
Toast
.
LENGTH_LONG
).
show
();
}
};
final
RatingBar
sBar
=
(
RatingBar
)
findViewById
(
R
.
id
.
serviceBar
);
sBar
.
setOnRatingBarChangeListener
(
barChangeListener
);
final
RatingBar
pBar
=
(
RatingBar
)
findViewById
(
R
.
id
.
priceBar
);
pBar
.
setOnRatingBarChangeListener
(
barChangeListener
);
Button
doneButton
=
(
Button
)
findViewById
(
R
.
id
.
doneButton
);
doneButton
.
setOnClickListener
(
new
OnClickListener
()
{
@Override
public
void
onClick
(
View
arg0
)
{
String
message
=
String
.
format
(
"Final Answer: Price %.0f/%d, Service %.0f/%d%nThank you!"
,
sBar
.
getRating
(),
sBar
.
getNumStars
(),
pBar
.
getRating
(),
pBar
.
getNumStars
()
);
// Thank the user
Toast
.
makeText
(
Main
.
this
,
message
,
Toast
.
LENGTH_LONG
).
show
();
// And upload the numbers to a database, hopefully...
// That's all for this Activity, and hence this App
finish
();
}
});
}
}
There is more than one RatingBar
, so we don’t save the value in the listener, because an incomplete survey is not useful; in the Done button action listener we fetch both values and display them, and this would be the place to save them. Your mileage may vary: it may make more sense to save them in the OnRatingBarChangeListener
.
If you’re not used to printf
-like formatting, the String.format
call uses %.0f
to format the float
as an int
, instead of casting it (since we have to do nice formatting anyway). Ideally the format message should be from the XML strings, but this is only a demo program.
The main UI is shown in Figure 6-21.
When the user clicks the Done button, she will see the Farewell message displayed in the desktop window (see Figure 6-22).
When you wish to both display the current “average” or similar measure ratings from a community and allow users to enter their own ratings, it is customary to display the current ratings read-only and to create a pop-up dialog where a user can enter her particular rating. This is described on the Android Patterns website.
The source code for this project is in the Android Cookbook repository, in the subdirectory RatingBarDemo (see “Getting and Using the Code Examples”).
Ian Darwin
The animation specification is created in XML files in the anim directory. In this example, we want the text entry field to be able to shake either left to right (to emulate a person shaking his head from side to side, which means “no” or “I disagree” in many parts of the world) or up and down (a person nodding in agreement). So, we create two animations, horizontal.xml and vertical.xml. Here is horizontal.xml:
<?xml version="1.0" encoding="utf-8"?>
<translate
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:fromXDelta=
"0"
android:toXDelta=
"10"
android:duration=
"1000"
android:interpolator=
"@anim/cycler"
/>
The file vertical.xml is identical except it uses fromYDelta
and toYDelta
.
The Interpolator
—the function that drives the animation—is contained in another file, cycler.xml, shown here:
<?xml version="1.0"?>
<cycleInterpolator
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:cycles=
"5"
/>
To apply one of the two animations to a View
component, you need a reference to it. You can, of course, use the common findViewById(R.id.*)
. You can also use the Activity
method getCurrentFocus()
if you are dealing with the current input (focused) View
component; this avoids coupling to the name of a particular component, if you know that your animation will always apply to the current input object. In my code I know this is true because the animation startup is done in an onClick()
method. Alternatively, you could use the View
that is passed into the onClick()
method, but that would make the button shake, not the text field.
I won’t show the whole application, but here is the onClick()
method that contains all the animation code (see Example 6-29).
@Override
public
void
onClick
(
View
v
)
{
String
answer
=
answerEdit
.
getText
().
toString
();
if
(
"yes"
.
equalsIgnoreCase
(
answer
))
{
getCurrentFocus
().
startAnimation
(
AnimationUtils
.
loadAnimation
(
getApplicationContext
(),
R
.
anim
.
vertical
));
return
;
}
if
(
"no"
.
equalsIgnoreCase
(
answer
))
{
getCurrentFocus
().
startAnimation
(
AnimationUtils
.
loadAnimation
(
getApplicationContext
(),
R
.
anim
.
horizontal
));
return
;
}
Toast
.
makeText
(
this
,
"Try to be more definite, OK?"
,
Toast
.
LENGTH_SHORT
).
show
();
}
The shaking effect is convenient for drawing the user’s attention to an input that is incorrect, but it can easily be overdone. Use judiciously!
Adrian Cowham
Use Android’s haptic controls to provide instant physical feedback.
Building confidence among users that their actions had an effect is a requirement for any app on any platform. The canonical example is displaying a progress bar to let users know an action is being processed. For touch interfaces this technique still applies, but the advantage of a touch interface is that developers have the opportunity to provide physical feedback, as users are capable of actually feeling the device react to their actions.
Android has some stock haptic controls, but if these don’t satisfy your needs you can gain control of the device’s vibrator for custom feedback.
Custom control of the device’s vibrator requires permission. This is something you’ll have to explicitly list in your AndroidManifest.xml file. If you’re paranoid about asking for permission or if you already have a long list of permissions, you may want to use the stock Android haptic feedback options.
Also be aware that some devices don’t have a vibrator; when you run the examples in this recipe on such devices, you will not receive haptic feedback.
I’ll start by showing the more complicated example first, custom haptic feedback.
Your first step is to request the necessary permission. Add the following line to your AndroidManifest.xml file:
<uses-permission
android:name=
"android.permission.VIBRATE"
/>
Now define a listener to respond to touch events. The CustomHapticListener
shown in Example 6-30 is implemented as a private nonstatic inner class of the main Activity because it needs access to the Context
method getSystemService()
.
private class CustomHapticListener implements View.OnClickListener { // Duration in milliseconds to vibrate private final int durationMs; public CustomHapticListener(int ms) { durationMs = ms; } @Override public void onClick(View v) { Vibrator vibe = (Vibrator) getSystemService(VIBRATOR_SERVICE); vibe.vibrate(durationMs); } }
and are the important lines. gets a reference to the Vibrator service and vibrates the device. If you have not requested the vibrate permission, will throw an exception.
Now register the listener. In your Activity’s onCreate()
method, you’ll need to get a reference to the GUI element you want to attach haptic feedback to and then register the OnClickListener
defined earlier:
@Override
public
void
onCreate
(
Bundle
savedInstance
)
{
Button
customBtn
=
(
Button
)
findViewById
(
R
.
id
.
btn_custom
);
customBtn
.
setOnTouchListener
(
new
CustomHapticListener
(
100
)
);
}
That’s it; you’re in control of the haptic feedback. Now we’ll move on to using stock Android haptic feedback.
First things first: to use stock Android haptic feedback events you must enable this on a view-by-view basis. That is, you must explicitly enable haptic feedback for each view. You can enable haptic feedback declaratively in your layout file or programmatically in Java. To enable haptic feedback in your layout, simply add the android:hapticFeedbackEnabled="true"
attribute to your view(s). Here’s an abbreviated example:
<button
android:hapticFeedbackEnabled=
"true"
>
</button>
Here’s how you do the same thing in code:
Button
keyboardTapBtn
=
(
Button
)
findViewById
(
btnId
);
keyboardTapBtn
.
setHapticFeedbackEnabled
(
true
);
Now that haptic feedback has been enabled, the next step is to register a listener in which to
then perform the actual feedback.
For this example we’ll use a touch listener so that touch events will invoke the feedback;
Example 6-31 shows how to register an OnTouchListener
and perform haptic feedback when a user touches the view.
// Initialize some buttons with the stock Android haptic feedback values private void initializeButtons() { // Initialize the buttons with the standard haptic feedback options initializeButton( R.id.btn_keyboardTap, HapticFeedbackConstants.KEYBOARD_TAP ); initializeButton( R.id.btn_longPress, HapticFeedbackConstants.LONG_PRESS ); initializeButton( R.id.btn_virtualKey, HapticFeedbackConstants.VIRTUAL_KEY ); } // Helper method to initialize single buttons and register an OnTouchListener // to perform the haptic feedback private void initializeButton( int btnId, int hapticId ) { Button btn = ( Button ) findViewById( btnId ); btn.setOnTouchListener( new HapticTouchListener( hapticId ) ); } // Class to handle touch events and respond with haptic feedback private class HapticTouchListener implements OnTouchListener { private final int feedbackType; public HapticTouchListener( int type ) { feedbackType = type; } public int feedbackType() { return feedbackType; } @Override public boolean onTouch(View v, MotionEvent event) { // Only perform feedback when the user touches the view, as opposed // to lifting a finger off the view if( event.getAction() == MotionEvent.ACTION_DOWN ) { // Perform the feedback v.performHapticFeedback( feedbackType() ); } return true; } }
You’ll notice that in lines through , I’m initializing three different buttons with three different haptic feedback constants. These are Android’s stock values; two of the three seem to provide exactly the same feedback.
Example 6-31 is part of a test app I wrote to demonstrate haptic feedback, and I could not tell the difference between HapticFeedbackConstants.LONG_PRESS
and HapticFeedbackConstants.KeyboardTap
. HapticFeedbackConstants.VIRTUAL_KEY
, when tested, does not appear to provide any feedback.
is where the haptic feedback is performed. All in all, providing haptic feedback is pretty simple, but remember that if you want control of the device’s vibrator you must request permission in your AndroidManifest.xml file. If you choose to use the stock Android haptic feedback options, make sure you enable haptic feedback for your views either in the layout or programmatically.
The blog post by the author of this recipe, entitled “Android’s Haptic Feedback”.
The source code for this project is in the Android Cookbook repository, in the subdirectory HapticFeedback (see “Getting and Using the Code Examples”).
Pratik Rupwal
Replace the content view of the tab with the new Activity you want to move to.
When a “calling” Activity within a TabView
calls another Activity through an Intent, the TabView
gets replaced by the view of the called Activity. To show the called Activity within the TabView
, we can replace the view of the calling Activity with the view of the called Activity so that the TabView
remains stable. To achieve this we need to extend the calling Activity from ActivityGroup
rather than Activity
.
In Example 6-32 the Calling
Activity extended from ActivityGroup
has been set within a TabView
.
// 'Calling' Activity
public
class
Calling
extends
ActivityGroup
implements
OnClickListener
{
Button
b1
;
Intent
i1
;
/** Called when the Activity is first created. */
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
calling
);
b1
=(
Button
)
findViewById
(
R
.
id
.
changeactivity
);
b1
.
setOnClickListener
();
}
public
void
onClick
(
View
view
)
{
// Create an intent to call the 'Called' Activity
i1
=
new
Intent
(
this
.
getBaseContext
(),
Called
.
class
);
// Call the method to replace the view.
replaceContentView
(
"Called"
,
i1
);
}
// This replaces the view of the 'Calling' Activity with the 'Called' Activity.
public
void
replaceContentView
(
String
id
,
Intent
newIntent
)
{
// Obtain the view of the 'Called' Activity using its Intent 'newIntent'
View
view
=
getLocalActivityManager
().
startActivity
(
id
,
newIntent
.
addFlags
(
Intent
.
FLAG_ACTIVITY_CLEAR_TOP
))
.
getDecorView
();
// Set the above view to the content of the 'Calling' Activity.
this
.
setContentView
(
view
);
}
}
The “called Activity” can also call another Activity (say CalledSecond
), as shown here:
// 'Called Activity'
public
class
Called
extends
Activity
implements
OnClickListener
{
Button
b1
;
Intent
i1
;
Calling
caller
;
/** Called when the Activity is first created. */
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
called
);
b1
=(
Button
)
findViewById
(
R
.
id
.
changeactivity
);
b1
.
setOnClickListener
();
}
public
void
onClick
(
View
view
)
{
// Create an Intent to call the 'CalledSecond' Activity
i1
=
new
Intent
(
this
.
getBaseContext
(),
CalledSecond
.
class
);
/**
* 'CalledSecond' can be any Activity, even
* 'Calling' (in case backward navigation is required)
*/
// Initialize the object of the 'Calling' class
caller
=(
Calling
)
getParent
();
// Call the method to replace the view.
caller
.
replaceContentView
(
"CalledSecond"
,
i1
);
}
}
Shraddha Shravagi
Create a simple Activity that shows a loading image instead of a black screen.
Sometimes it takes time for an Activity to fetch user-requested data from a database or the internet, and then to load the data onto the user’s screen. In such cases, usually the screen goes black while the user waits for the data to load. The following scenario illustrates this:
ProfileList
(the user selects one profile) → black screen → ProfileData
Instead of showing the user a black screen while she waits for the data to load, you can show an image, as illustrated in the following scenario:
ProfileList
(the user selects one profile) → LoadingScreenActivity
→ ProfileData
In this recipe we will create a simple loading screen that appears for 2.5 seconds while the next Activity loads.
To do this, you need to start by creating a LoadingScreen
layout file. This layout creates a screen that displays a “loading” message and a progress bar:
<LinearLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:layout_width=
"fill_parent"
android:gravity=
"center"
android:orientation=
"vertical"
android:layout_height=
"fill_parent"
android:background=
"#E5E5E5"
>
<TextView
android:text=
"Please wait while your data is being loaded..."
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textColor=
"#000000"
>
</TextView>
<ProgressBar
android:id=
"@+id/mainSpinner1"
android:layout_gravity=
"center"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:indeterminate=
"true"
style=
"?android:attr/progressBarStyleInverse"
>
</ProgressBar>
</LinearLayout>
Next, create a LoadingScreen
class file (see Example 6-33).
public
class
LoadingScreenActivity
extends
Activity
{
// Introduce a delay
private
final
int
WAIT_TIME
=
2500
;
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
System
.
out
.
println
(
"LoadingScreenActivity screen started"
);
setContentView
(
R
.
layout
.
loading_screen
);
findViewById
(
R
.
id
.
mainSpinner1
).
setVisibility
(
View
.
VISIBLE
);
new
Handler
().
postDelayed
(
new
Runnable
()
{
@Override
public
void
run
()
{
// Simulating a long-running task
try
{
Thread
.
sleep
(
1000
);
}
catch
(
InterruptedException
e
)
{
// Can't happen
}
System
.
out
.
println
(
"Going to Profile Data"
);
/* Create an Intent that will start the ProfileData Activity */
Intent
mainIntent
=
new
Intent
(
LoadingScreenActivity
.
this
,
ProfileData
.
class
);
LoadingScreenActivity
.
this
.
startActivity
(
mainIntent
);
LoadingScreenActivity
.
this
.
finish
();
}
},
WAIT_TIME
);
}
}
This will load the next Activity once WAIT_TIME
has elapsed.
Now all you need to do is to create an Intent to launch the loading screen Activity:
protected
void
onListItemClick
(
ListView
l
,
View
v
,
int
position
,
long
id
)
{
super
.
onListItemClick
(
l
,
v
,
position
,
id
);
Intent
intent
=
new
Intent
(
ProfileList
.
this
,
LoadingScreenActivity
.
class
);
startActivity
(
intent
);
}
Daniel Fowler
Define an Android shape in an XML file and assign it to a layout’s background
attribute.
The drawable folder, under res in an Android project, is not restricted to bitmaps (PNG or JPG files); it can also hold shapes defined in XML files. These shapes can then be reused in the project. A shape can be used to put a border around a layout. This example shows how to make a rectangular border with curved corners.
Create a new file called customborder.xml in the drawable folder. Right-click on the drawable folder and select New → Drawable Resource File from the context menu (or, with that folder selected, use the File menu). In the New Drawable Resource File dialog, enter a filename of customborder. Set the “Root element” to shape, and click OK. Enter the following XML, defining the border shape, into the customborder.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<shape
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:shape=
"rectangle"
>
<corners
android:radius=
"20dp"
/>
<padding
android:left=
"10dp"
android:right=
"10dp"
android:top=
"10dp"
android:bottom=
"10dp"
/>
<solid
android:color=
"#CCCCCC"
/>
</shape>
The attribute android:shape
is set to rectangle
(shape files also support oval
, line
, and ring
). rectangle
is the default value, so you can leave out this attribute if you’re defining a rectangle. For detailed information on shape files, refer to the Android documentation on shapes.
The element corners
sets the rectangle corners to be rounded; it is possible to set a different radius on each corner (see the Android reference).
The padding
attributes are used to move the contents of the view to which the shape is applied, to prevent the contents from overlapping the border.
The border color here is set to a light gray (the hexadecimal RGB value #CCCCCC
).
Shapes also support gradients, but that is not used in our example; again, see the Android documentation to see how a gradient is defined.
The shape is applied using android:background="@drawable/customborder"
.
Within the layout other views can be added as usual. In this example a single TextView
has been added, and the text is white (hexadecimal RGB value #FFFFFF
). The background is set to blue, plus some transparency to reduce the brightness (hexadecimal alpha RGB value #A00000FF
).
Finally, the layout is offset from the screen edge by placing it into another layout with a small amount of padding. The full layout file looks like this:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:padding="5dp"> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/customborder"> <TextView android:layout_width="fill_parent" android:layout_height="fill_parent" android:text="Text View" android:textSize="20dp" android:textColor="#FFFFFF" android:gravity="center_horizontal" android:background="#A00000FF" /> </LinearLayout> </LinearLayout>
This produces the result shown in Figure 6-23.
The developer documentation on shapes.
The source code for this project is in the Android Cookbook repository, in the subdirectory LayoutBorder (see “Getting and Using the Code Examples”).
Pratik Rupwal
Use the GestureDetector
class to detect simple gestures such as tapping, scrolling, swiping, or flipping.
The sample application in this recipe has four views, each a different color. It also has two modes: SCROLL and FLIP. The application starts in FLIP mode. In this mode, when you perform the swipe/fling gesture in a left-to-right or top-to-bottom direction, the view changes back and forth. When a long-press is detected, the application changes to SCROLL mode, in which you can scroll the displayed view. While in this mode, you can double-tap the screen to bring the screen back to its original position. When a long-press is detected again, the application changes to FLIP mode.
This recipe focuses on gesture detection, hence the animation between those views is not discussed. Refer to Recipe 6.24 for an example of shaking a view using an animation, as well as the Android documentation for android.view.animation
.
Example 6-34 provides an introduction to simple gesture detection in Android. Our GestureDetector
class detects gestures using the supplied MotionEvent
class. We use this class along with the onTouchEvent()
method. Inside this method we call GestureDetector.onTouchEvent()
. The GestureDetector
class identifies the gestures or events that occurred and reports back to us using the GestureDetector.OnGestureListener
callback interface. We create an instance of the GestureDetector
class by passing the Context
and GestureDetector.OnGestureListener
listener. The double-tap event is not present in the GestureDetector.OnGestureListener
callback interface; this event is reported using another callback interface, GestureDetector.OnDoubleTapListener
. To use this callback interface we have to register a GestureDetector.OnDoubleTapListener
for these events. The MotionEvent
class contains all the values corresponding to a movement and touch event. This class holds values such as the x- and y- positions at which the event occurred, the timestamp at which the event occurred, and the mouse pointer index.
...
import
android.view.GestureDetector
;
...
import
android.view.animation.OvershootInterpolator
;
import
android.view.animation.TranslateAnimation
;
public
class
FlipperActivity
extends
Activity
implements
GestureDetector
.
OnGestureListener
,
GestureDetector
.
OnDoubleTapListener
{
final
private
int
SWIPE_MIN_DISTANCE
=
100
;
final
private
int
SWIPE_MIN_VELOCITY
=
100
;
private
ViewFlipper
flipper
=
null
;
private
ArrayList
<
TextView
>
views
=
null
;
private
GestureDetector
gesturedetector
=
null
;
private
Vibrator
vibrator
=
null
;
int
colors
[]
=
{
Color
.
rgb
(
255
,
128
,
128
),
Color
.
rgb
(
128
,
255
,
128
),
Color
.
rgb
(
128
,
128
,
255
),
Color
.
rgb
(
128
,
128
,
128
)
};
private
Animation
animleftin
=
null
;
private
Animation
animleftout
=
null
;
private
Animation
animrightin
=
null
;
private
Animation
animrightout
=
null
;
private
Animation
animupin
=
null
;
private
Animation
animupout
=
null
;
private
Animation
animdownin
=
null
;
private
Animation
animdownout
=
null
;
private
boolean
isDragMode
=
false
;
private
int
currentview
=
0
;
/
**
Initializes
the
first
screen
and
animation
to
be
applied
to
the
screen
*
after
detecting
the
gesture
.
*/
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
flipper
=
new
ViewFlipper
(
this
);
gesturedetector
=
new
GestureDetector
(
this
,
this
);
vibrator
=
(
Vibrator
)
getSystemService
(
VIBRATOR_SERVICE
);
gesturedetector
.
setOnDoubleTapListener
(
this
);
flipper
.
setInAnimation
(
animleftin
);
flipper
.
setOutAnimation
(
animleftout
);
flipper
.
setFlipInterval
(
3000
);
flipper
.
setAnimateFirstView
(
true
);
prepareAnimations
();
prepareViews
();
addViews
();
setViewText
();
setContentView
(
flipper
);
}
private
void
prepareAnimations
()
{
animleftin
=
new
TranslateAnimation
(
Animation
.
RELATIVE_TO_PARENT
,
+
1.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
);
animleftout
=
new
TranslateAnimation
(
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
-
1.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
);
animrightin
=
new
TranslateAnimation
(
Animation
.
RELATIVE_TO_PARENT
,
-
1.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
);
animrightout
=
new
TranslateAnimation
(
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
+
1.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
);
animupin
=
new
TranslateAnimation
(
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
+
1.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
);
animupout
=
new
TranslateAnimation
(
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
-
1.0f
);
animdownin
=
new
TranslateAnimation
(
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
-
1.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
);
animdownout
=
new
TranslateAnimation
(
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
0.0f
,
Animation
.
RELATIVE_TO_PARENT
,
+
1.0f
);
animleftin
.
setDuration
(
1000
);
animleftin
.
setInterpolator
(
new
OvershootInterpolator
());
animleftout
.
setDuration
(
1000
);
animleftout
.
setInterpolator
(
new
OvershootInterpolator
());
animrightin
.
setDuration
(
1000
);
animrightin
.
setInterpolator
(
new
OvershootInterpolator
());
animrightout
.
setDuration
(
1000
);
animrightout
.
setInterpolator
(
new
OvershootInterpolator
());
animupin
.
setDuration
(
1000
);
animupin
.
setInterpolator
(
new
OvershootInterpolator
());
animupout
.
setDuration
(
1000
);
animupout
.
setInterpolator
(
new
OvershootInterpolator
());
animdownin
.
setDuration
(
1000
);
animdownin
.
setInterpolator
(
new
OvershootInterpolator
());
animdownout
.
setDuration
(
1000
);
animdownout
.
setInterpolator
(
new
OvershootInterpolator
());
}
private
void
prepareViews
()
{
TextView
view
=
null
;
views
=
new
ArrayList
<
TextView
>();
for
(
int
color:
colors
)
{
view
=
new
TextView
(
this
);
view
.
setBackgroundColor
(
color
);
view
.
setTextColor
(
Color
.
BLACK
);
view
.
setGravity
(
Gravity
.
CENTER_HORIZONTAL
|
Gravity
.
CENTER_VERTICAL
);
views
.
add
(
view
);
}
}
private
void
addViews
()
{
for
(
int
index
=
0
;
index
<
views
.
size
();
++
index
)
{
flipper
.
addView
(
views
.
get
(
index
),
index
,
new
LayoutParams
(
LayoutParams
.
FILL_PARENT
,
LayoutParams
.
FILL_PARENT
));
}
}
private
void
setViewText
()
{
String
text
=
getString
(
isDragMode
?
R
.
string
.
app_info_drag
:
R
.
string
.
app_info_flip
);
for
(
int
index
=
0
;
index
<
views
.
size
();
++
index
)
{
views
.
get
(
index
).
setText
(
text
);
}
}
/** Gets invoked when a screen touch is detected. */
@Override
public
boolean
onTouchEvent
(
MotionEvent
event
)
{
return
gesturedetector
.
onTouchEvent
(
event
);
}
/** The onDown() method is called when the user first touches the screen;
* the MotionEvent parameter represents the event that corresponds to
* the touch event. */
@Override
public
boolean
onDown
(
MotionEvent
e
)
{
return
false
;
}
/** The onFling() method is called whenever the user swipes the screen
* in any direction (i.e., touches the screen and immediately
* moves the finger in any direction). */
@Override
public
boolean
onFling
(
MotionEvent
event1
,
MotionEvent
event2
,
float
velocityX
,
float
velocityY
)
{
if
(
isDragMode
)
return
false
;
final
float
ev1x
=
event1
.
getX
();
final
float
ev1y
=
event1
.
getY
();
final
float
ev2x
=
event2
.
getX
();
final
float
ev2y
=
event2
.
getY
();
final
float
xdiff
=
Math
.
abs
(
ev1x
-
ev2x
);
final
float
ydiff
=
Math
.
abs
(
ev1y
-
ev2y
);
final
float
xvelocity
=
Math
.
abs
(
velocityX
);
final
float
yvelocity
=
Math
.
abs
(
velocityY
);
if
(
xvelocity
>
this
.
SWIPE_MIN_VELOCITY
&&
xdiff
>
this
.
SWIPE_MIN_DISTANCE
)
{
if
(
ev1x
>
ev2x
)
{
// Swipe left
--
currentview
;
if
(
currentview
<
0
)
{
currentview
=
views
.
size
()
-
1
;
}
flipper
.
setInAnimation
(
animleftin
);
flipper
.
setOutAnimation
(
animleftout
);
}
else
{
// Swipe right
++
currentview
;
if
(
currentview
>=
views
.
size
())
{
currentview
=
0
;
}
flipper
.
setInAnimation
(
animrightin
);
flipper
.
setOutAnimation
(
animrightout
);
}
flipper
.
scrollTo
(
0
,
0
);
flipper
.
setDisplayedChild
(
currentview
);
}
else
if
(
yvelocity
>
this
.
SWIPE_MIN_VELOCITY
&&
ydiff
>
this
.
SWIPE_MIN_DISTANCE
)
{
if
(
ev1y
>
ev2y
)
{
// Swipe up
--
currentview
;
if
(
currentview
<
0
)
{
currentview
=
views
.
size
()
-
1
;
}
flipper
.
setInAnimation
(
animupin
);
flipper
.
setOutAnimation
(
animupout
);
}
else
{
// swipe down
++
currentview
;
if
(
currentview
>=
views
.
size
())
{
currentview
=
0
;
}
flipper
.
setInAnimation
(
animdownin
);
flipper
.
setOutAnimation
(
animdownout
);
}
flipper
.
scrollTo
(
0
,
0
);
flipper
.
setDisplayedChild
(
currentview
);
}
return
false
;
}
/** The onLongPress() method is called when the user touches the screen
* and holds it for a period of time. The MotionEvent parameter represents
* the event that corresponds to the touch event. */
@Override
public
void
onLongPress
(
MotionEvent
e
)
{
vibrator
.
vibrate
(
200
);
flipper
.
scrollTo
(
0
,
0
);
isDragMode
=
!
isDragMode
;
setViewText
();
}
/** The onScroll() method is called when the user touches the screen
* and moves their finger to another location on the screen. */
@Override
public
boolean
onScroll
(
MotionEvent
e1
,
MotionEvent
e2
,
float
distanceX
,
float
distanceY
)
{
if
(
isDragMode
)
flipper
.
scrollBy
((
int
)
distanceX
,
(
int
)
distanceY
);
return
false
;
}
/** The onShowPress() method is called when the user touches the screen,
* before they lift or move their finger. This event is mostly used for
* giving visual feedback to the user to show their action. */
@Override
public
void
onShowPress
(
MotionEvent
e
)
{
}
/** The onSingleTapUp() method is called when the user taps the screen. */
@Override
public
boolean
onSingleTapUp
(
MotionEvent
e
)
{
return
false
;
}
/** The onDoubleTap() method is called when a double-tap event has occurred.
* The only parameter, MotionEvent, corresponds to the double-tap
* event that occurred. */
@Override
public
boolean
onDoubleTap
(
MotionEvent
e
)
{
flipper
.
scrollTo
(
0
,
0
);
return
false
;
}
/** The onDoubleTapEvent() method is called for all events that occurred
* within the double-tap; i.e., down, move, and up events. */
@Override
public
boolean
onDoubleTapEvent
(
MotionEvent
e
)
{
return
false
;
}
/** The onSingleTapConfirmed() method is called when a single tap
* has occurred and been confirmed, but this is not same as the
* single-tap event in the GestureDetector.OnGestureListener. This
* is called when the GestureDetector detects and confirms that
* this tap does not lead to a double-tap. */
@Override
public
boolean
onSingleTapConfirmed
(
MotionEvent
e
)
{
return
false
;
}
}
When the mode of the application changes the user is notified with a vibration. To use the Vibrator service, set the following permission in your application’s AndroidManifest.xml file:
<uses-permission
android:name=
"android.permission.VIBRATE"
></uses-permission>
The application uses some strings, which are declared under res/values/string.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
name=
"app_info_drag"
>
GestureDetector sample. Current Mode: SCROLL Drag the view using finger. Long press to change the mode to FLIP. Double tap to reposition the view to normal.</string>
<string
name=
"app_name"
>
Gesture Detector Sample</string>
<string
name=
"app_info_flip"
>
GestureDetector sample. Current Mode: FLIP Swipe left, right, up, down to change the views Long press to change to mode to SCROLL</string>
</resources>
The documentation for the GestureOverlayView
class for handling complex gestures in Android.
Catarina Reis
Create an Application
widget, which is a simple GUI control that appears on the Home screen and allows users to easily interact with an existing application (Activity and/or Service).
In this recipe we will create a widget that starts a Service that updates its visual components. The widget, called CurrentMoodWidget
, presents the user’s current mood in the form of a text smiley. The current mood smiley changes to a random mood smiley whenever the user clicks the smiley image button. In Figure 6-24, the screenshot on the left shows the initial view, and the screenshot on the right shows the view after a random change.
Here’s how to create this simple app widget:
Start by creating a new Android project (CurrentMoodWidgetProject
). Use “Current Mood” as the application name and “oreillymedia.cookbook.android.spikes” as the package name. Do not create an Activity. Set the minimum SDK version
to anything modern (technically the minimum API level is 8, for Android 2.2, the version that introduced app widgets).
Add the string definitions for the widget in the file res/values/string.xml, according to the following name/value pairs:
widgettext
—current mood
widgetmoodtext
—:)
Add the images that will appear in the widget’s button (smile_icon.png). Place these under the res/drawable structure.
Create a new layout file inside res/layout, under the project structure, that will define the widget layout (widgetlayout.xml) according to the following structure:
<TextView
android:text=
"@string/widgettext"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_weight=
"0.8"
android:layout_gravity=
"center_vertical"
android:textColor=
"#000000"
></TextView>
<TextView
android:text=
"@string/widgetmoodtext"
android:id=
"@+id/widgetMood"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_weight=
"0.3"
android:layout_gravity=
"center_vertical"
android:textColor=
"#000000"
></TextView>
<ImageButton
android:id=
"@+id/widgetBtn"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_weight=
"0.5"
android:src=
"@drawable/smile_icon"
android:layout_gravity=
"center_vertical"
></ImageButton>
Provide the widget provider setup configuration by first creating the res/xml folder under the project structure and then creating an XML file (widgetproviderinfo.xml) with the following parameters:
<appwidget-provider
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:minWidth=
"220dp"
android:minHeight=
"72dp"
android:updatePeriodMillis=
"86400000"
android:initialLayout=
"@layout/widgetlayout"
>
</appwidget-provider>
Create the Service that reacts to the user interaction with the smiley image button (CurrentMoodService.java); see Example 6-35.
@Override
public
int
onStartCommand
(
Intent
intent
,
int
flags
,
int
startId
)
{
super
.
onStart
(
intent
,
startId
);
updateMood
(
intent
);
stopSelf
(
startId
);
return
START_STICKY
;
}
private
void
updateMood
(
Intent
intent
)
{
if
(
intent
!=
null
)
{
String
requestedAction
=
intent
.
getAction
();
if
(
requestedAction
!=
null
&&
requestedAction
.
equals
(
UPDATEMOOD
))
{
this
.
currentMood
=
getRandomMood
();
int
widgetId
=
intent
.
getIntExtra
(
AppWidgetManager
.
EXTRA_APPWIDGET_ID
,
0
);
AppWidgetManager
appWidgetMan
=
AppWidgetManager
.
getInstance
(
this
);
RemoteViews
views
=
new
RemoteViews
(
this
.
getPackageName
(),
R
.
layout
.
widgetlayout
);
views
.
setTextViewText
(
R
.
id
.
widgetMood
,
currentMood
);
appWidgetMan
.
updateAppWidget
(
widgetId
,
views
);
}
}
}
Implement the widget provider class (CurrentMoodWidgetProvider.java); see Example 6-36.
@Override
public
void
onUpdate
(
Context
context
,
AppWidgetManager
appWidgetManager
,
int
[]
appWidgetIds
)
{
super
.
onUpdate
(
context
,
appWidgetManager
,
appWidgetIds
);
for
(
int
i
=
0
;
i
<
appWidgetIds
.
length
;
i
++)
{
int
appWidgetId
=
appWidgetIds
[
i
];
RemoteViews
views
=
new
RemoteViews
(
context
.
getPackageName
(),
R
.
layout
.
widgetlayout
);
Intent
intent
=
new
Intent
(
context
,
CurrentMoodService
.
class
);
intent
.
setAction
(
CurrentMoodService
.
UPDATEMOOD
);
intent
.
putExtra
(
AppWidgetManager
.
EXTRA_APPWIDGET_ID
,
appWidgetId
);
PendingIntent
pendingIntent
=
PendingIntent
.
getService
(
context
,
0
,
intent
,
0
);
views
.
setOnClickPendingIntent
(
R
.
id
.
widgetBtn
,
pendingIntent
);
appWidgetManager
.
updateAppWidget
(
appWidgetId
,
views
);
}
}
Finally, declare the Service and the app widget provider in the manifest file (AndroidManifest.xml):
<service
android:name=
".CurrentMoodService"
>
</service>
<receiver
android:name=
".CurrentMoodWidgetProvider"
>
<intent-filter>
<action
android:name=
"android.appwidget.action.APPWIDGET_UPDATE"
/>
</intent-filter>
<meta-data
android:name=
"android.appwidget.provider"
android:resource=
"@xml/widgetproviderinfo"
/>
</receiver>
The source code for this example is in the Android Cookbook repository, in the subdirectory CurrentMoodWidget (see “Getting and Using the Code Examples”).
98.82.120.188