As we have seen, there are three ways to implement a new behavior in an application. In increasing order of complexity, you can:
Find a toolbox widget that already does nearly what you need and extend it.
Use the handler mechanism demonstrated previously in Example 10-4.
Override event receiver methods and implement them yourself.
Handling raw events across multiple platforms can be quite complicated. Different devices, for instance, may have radically different keypads: for instance, four-key versus five-key D-pads. Some devices still require triple-tapping to enter alphabetic information. This kind of diversity is a serious issue in the mobile environment and can be a nightmare for the developer who wants to keep her application portable.
When designing your application, it’s clearly smart to let the framework do as much as possible. The best option is to find some toolbox widget that has nearly the behavior you require and extend it to meet your needs. The toolkit provides extensive tools for doing this: XML attributes, fine-grained and overridable methods, and so on.
If it isn’t possible to customize an existing widget, you should consider the listener mechanism, demonstrated previously in Example 10-5. Only when it is necessary to change the existing behavior of a widget should you consider overriding event receiver methods.
User interface frameworks have different names for the components from which they’re composed: the text boxes, buttons, canvases, and other components that you use to create your unique application user interface. Android generically calls them Views, and the documentation defines them simply as:
View: An object that knows how to draw itself to the screen. |
So any object that draws itself is a View, and Views that can contain or group other Views are appropriately called ViewGroups. Views are arranged and displayed on the screen according to a Layout, which gives Android hints about how you’d like to see the Views arranged. In the next few sections we’ll look first at simple Views, then at ViewGroups, and finally at Layouts. Since expandability is a core principle for Android, we will also look at what you need to do to define your own custom Views and Layouts.
As we’ve already seen, Views and Layouts both have attributes that can either be defined in Java source code or in the XML file associated with the Activity that uses the View or Layout. When the attributes are in an XML file, they are “inflated” at runtime, meaning that they are applied to their respective Views by the Android framework to determine how the Views look and operate.
There are so many attributes that it doesn’t make sense to list
them all in these examples. We describe the key ones, and the rest are
explained in the documentation that comes with the Android SDK. A quick
search for
android.widget.
view_name
will
give you the class definition for that View, including all the
attributes available for it, and a description of each.
The Views in the following section are the meat and potatoes of your application; essential widgets that you’ll use over and over and that your users will be familiar with from other applications.
A TextView, as shown in the line “This is some text” in Figure 11-1, is just what you’d expect: a place to display a text string. The vanilla TextView is for display only, whereas EditText is a predefined subclass of TextView that includes rich editing capabilities.
Each TextView has the attributes you’d expect of such a component: you can change its height, width, font, text color, background color, and so forth. TextViews also have some useful unique attributes:
autoLink
If set (true), finds URLs in the displayed text and automatically converts them to clickable links.
autoText
If set (true), finds and corrects simple spelling errors in the text.
editable
If set (true), indicates that the program has defined an input method to receive input text (default is false for TextView, and true for EditText).
inputMethod
Identifies the input method (EditText defines one for generic text).
Example 11-1 shows how to use a TextView and an EditText with Buttons. (Buttons are covered in the next section.) It also shows the XML layout file (main.xml), which uses pretty standard and recommended layout parameters.
<?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/txtDemo" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <EditText android:id="@+id/eTxtDemo" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/btnDone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Log it" /> </LinearLayout>
Example 11-2 contains the accompanying Java source (TextViewDemo.java).
package com.oreilly.demo; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; public class TextViewDemo extends Activity { private static TextView txt1; private static EditText etxt1; private static Button btn1; // Create a button click listener for the Done button. private final Button.OnClickListener btnDoneOnClick = new Button.OnClickListener() { public void onClick(View v) { String input = etxt1.getText().toString(); //Log the input string Log.v("TextViewDemo", input); etxt1.setText(""); } }; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //Get pointers to the Views defined in main.xml txt1 = (TextView) findViewById(R.id.txtDemo); etxt1 = (EditText) findViewById(R.id.eTxtDemo); btn1 = (Button) findViewById(R.id.btnDone); //Set the string displayed in TextView1 txt1.setText("This is some text."); //Set the OnClickListener for the Done button btn1.setOnClickListener(btnDoneOnClick); } }
Here are some of the highlights of the code:
Defines a ClickListener that we’ll attach to the “Log it” Button.
Because onCreate
is
executed just once, as soon as Android instantiates this View,
we put all the configuration we need here.
Loads the XML layout file for the application by setting
the ContentView
to main.xml.
Finds the Views that are defined in main.xml.
Puts an initial string into the TextView. (We also could have done this in the XML file, as was done in the MicroJobs application in Initialization in MicroJobs.java.)
Connects the Button with the ClickListener.
Now the user can enter and edit text in the EditText, and when
he clicks on “Log it”, the OnClickListener
is called and the text is
written to the logcat log. The string in the EditText is cleared
out, and the widget is ready for another entry.
The Button View is just a button, printed with some text to
identify it, that the user can click to invoke some action. The
previous section created a Button and connected it to an OnClickListener
method that executes when
the Button is clicked.
Android has a very visual, mobile-oriented user interface, so you might want to use a button with an image on it rather than one with text. Android provides the ImageButton View for just that purpose. You can adapt Example 11-2 to use an ImageButton by making one change in the XML file and another in the Java code:
In main.xml, replace
the Button definition for btnDone
with an ImageButton:
... <ImageButton android:id="@+id/btnDone" android:layout_width="wrap_content" android:layout_height="wrap_content" /> ...
In TextViewDemo.java,
redefine btn1
as an
ImageButton and add a line to set the image to a PNG image in
the drawable
directory:
... private static ImageButton btn1; ... /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //Get pointers to the Views defined in main.xml txt1 = (TextView) findViewById(R.id.txtDemo); etxt1 = (EditText) findViewById(R.id.eTxtDemo); btn1 = (ImageButton) findViewById(R.id.btnDone); ... //Set the image for the Done button btn1.setImageResource(R.drawable.log); ...
The button now appears as shown in Figure 11-2.
Adapter
s and AdapterView
s are an important and useful basis for several of the
views discussed in the rest of this chapter. Using extensions to
these classes, you can address an extremely wide variety of
situations.
The AdapterView
is a
generic, list-oriented view of data. Any collection of data objects
that can be ordered in some relatively stable way can be displayed
through an AdapterView
. An AdapterView
is always associated with an
Adapter
, which acts as the bridge
between it and the underlying data collection. The Adapter
has two responsibilities:
At the request of the AdapterView
, the Adapter
must be able to find the data
object that corresponds to a particular index. It must, in other
words, be able to find the data object that is visible in the
AdapterView
at a particular
location.
Inversely, the Adapter
must be able to supply a view through which the data at a
particular index can be displayed.
It takes only a moment’s reflection to understand how the
AdapterView
works: It is a
ViewGroup
that contains all the
machinery necessary to serve as both the View and Controller for a
collection of generic widgets. It can lay them out on the display,
pass in clicks and keystrokes, and so on. It need never concern
itself with what the subviews actually display; it distinguishes
them only by their indexes. Whenever it needs to perform either of
the two operations that are not entirely generic—creating a new view
or getting the data object attached to a particular view—it relies
on the Adapter
to convert an
index into either a data object or the view of a data object.
The AdapterView
requests
new views from an implementation of the Adapter
interface, as it needs them, for
display. For instance, as a user scrolls though a list of contacts,
the AdapterView
requests a new
view for each new contact that becomes visible. As an optimization,
the AdapterView
may offer a view
that is no longer visible (in this case, one that has scrolled off
the display) for reuse. This can dramatically reduce memory churn
and speed up display.
When offered a recycled view, however, the Adapter
must verify that it is the right
kind of view through which to display the data object at the
requested index. This is necessary because the Adapter
is not limited to returning
instances of a single view class in response to the request for a
view. If the Adapter
represents
several kinds of objects, it might create several different types of
views, each applicable to some subset of the data objects in the
collection. A list of contacts, for instance, might have two
entirely different view classes: one for displaying acquaintances
that are currently online and another for those who are not. The
latter might completely ignore clicks, whereas the former would open
a new chat session when clicked.
Although AdapterView
and
Adapter
are both abstract and
cannot be directly instantiated, the UI toolkit includes several
prebuilt Adapter
s and AdapterView
s that can be used unmodified
or further subclassed to provide your own customizations. ListAdapter
and SpinnerAdapter
are particularly
useful Adapter
s, while ListView
, GridView
, Spinner
, and Gallery
are all handy subclasses of
AdapterView
. If you plan to
create your own subclass of AdapterView
, a quick look at the code for
one of these classes will get you off to a running start.
A good example of the use of an AdapterView
can be found in Gallery and GridView. The Gallery
view in that section is a subclass
of AdapterView
, and uses a
subclass of Adapter
called
ImageAdapter
.
The Views we present in this section are probably familiar to you from other user interfaces. Their purpose is to allow the user to choose from multiple options. CheckBoxes are typically used when you want to offer multiple selections with a yes/no or true/false choice for each. RadioButtons are used when only one choice is allowed at a time.
Spinners are similar to combo boxes in some frameworks. A combo box typically displays the currently selected option, along with a pull-down list from which the user can click on another option to select it.
Android has adapted these familiar components to make them more useful in a touchscreen environment. Figure 11-3 shows the three types of multiple-choice Views laid out on an Android application, with the Spinner pulled down to show the options.
The layout XML file that created the screen in the figure 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" > <CheckBox android:id="@+id/cbxBox1" android:layout_width="20dp" android:layout_height="20dp" 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:text="Button1" /> <RadioButton android:id="@+id/RB2" android:text="Button2" /> <RadioButton android:id="@+id/RB3" android:text="Button3" /> </RadioGroup> <TextView android:id="@+id/txtRadio" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="RadioGroup: Nothing picked" /> <Spinner android:id="@+id/spnMusketeers" android:layout_width="250dp" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="2dp" /> </LinearLayout>
The file just lists each View we want on the screen along with the attributes we want. A RadioGroup is really a ViewGroup, so it contains the appropriate RadioButton Views. Example 11-3 shows the Java file that responds to user clicks.
package com.oreilly.select; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import com.google.android.maps.GeoPoint; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Spinner; import android.widget.TextView; import android.widget.AdapterView.OnItemSelectedListener; public class SelectExample extends Activity { private CheckBox checkBox; private TextView txtCheckBox, txtRadio; private RadioButton rb1, rb2, rb3; private Spinner spnMusketeers; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); checkBox = (CheckBox) findViewById(R.id.cbxBox1); txtCheckBox = (TextView) findViewById(R.id.txtCheckBox); txtRadio = (TextView) findViewById(R.id.txtRadio); rb1 = (RadioButton) findViewById(R.id.RB1); rb2 = (RadioButton) findViewById(R.id.RB2); rb3 = (RadioButton) findViewById(R.id.RB3); spnMusketeers = (Spinner) findViewById(R.id.spnMusketeers); // 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"); } } }); // React to events from the RadioGroup rb1.setOnClickListener(new RadioGroup.OnClickListener() { public void onClick(View v){ txtRadio.setText("Radio: Button 1 picked"); } }); rb2.setOnClickListener(new RadioGroup.OnClickListener() { public void onClick(View v){ txtRadio.setText("Radio: Button 2 picked"); } }); rb3.setOnClickListener(new RadioGroup.OnClickListener() { public void onClick(View v){ txtRadio.setText("Radio: Button 3 picked"); } }); // Set up the Spinner entries List<String> lsMusketeers = new ArrayList<String>(); lsMusketeers.add("Athos"); lsMusketeers.add("Porthos"); lsMusketeers.add("Aramis"); ArrayAdapter<String> aspnMusketeers = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, lsMusketeers); aspnMusketeers.setDropDownViewResource (android.R.layout.simple_spinner_dropdown_item); spnMusketeers.setAdapter(aspnMusketeers); // Set up a callback for the spinner spnMusketeers.setOnItemSelectedListener( new OnItemSelectedListener() { public void onNothingSelected(AdapterView<?> arg0) { } public void onItemSelected(AdapterView<?> parent, View v, int position, long id) { // Code that does something when the Spinner value changes } }); } }
The Views work as follows:
The CheckBox View takes care of flipping its state back and forth and displaying the appropriate checkmark when the state is true. All you have to do is create an “OnClickListener” to catch click events, and you can add whatever code you want to react.
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 selections by setting OnClickListeners for each RadioButton. Note that clicking on one of the RadioButtons does not fire a click event for the RadioGroup.
Spinners require the most work of these three Views, but can also provide the best use of scarce screen real estate. As shown, the Spinner is normally collapsed to the currently selected entry, and when you touch the down arrow on the right, it presents a drop-down list of the other choices. To make that happen, you must:
Create a list of the selections (which can be a dynamic list built and changed by your application).
Create an ArrayAdapter from the list that the
Spinner can use for its drop-down list. Note that the
formats shown for the ArrayAdapter (simple_spinner_item
and simple_spinner_dropdown_item
)
are defined by Android; they do not appear in your
resource XML files.
Create an onItemSelectedListener for the Spinner to
capture select events. The
listener has to contain both an onItemSelected
method and an
onNothingSelected
method.
ViewGroups are Views that contain child Views. Each ViewGroup
class embodies a different set of
assumptions about how to display its child Views. All ViewGroups
descend from the android.view.ViewGroup
class. Layouts, which
we’ll discuss later in the chapter, are a subset of ViewGroups.
The Gallery ViewGroup (Figure 11-4) displays multiple items in a horizontally scrolling list. The currently selected item is locked in the center of the screen. Any items that approach the edge of the screen begin to fade, giving the user the impression that there may be more items “around the corner.” The user can scroll horizontally through the items within the gallery. This ViewGroup is useful when you want to present a large set of possible choices to the user without using too much screen real estate.
A GridView (Figure 11-5, shown later) is very similar to a Gallery. Like a Gallery, the GridView displays many child Views that the user can manipulate. But in contrast to a Gallery, which is a one-dimensional list that the user can scroll horizontally, a GridView is a two-dimensional array that the user can scroll vertically.
The Gallery
and GridView
classes both descend from the
AdapterView
class, so you need a
subclass of Adapter to provide a standardized way to access the
underlying data. Any class that implements the Adapter
class must implement the following
abstract functions from that class:
int getCount
Returns the number of items in the data set represented by the Adapter.
Object getItem(int
position)
Returns the object in the Adapter function (Adapter class) at the given position.
long getItem(int
position)
Returns the row ID within the Adapter of the object at the given position.
View getView(int position, View
convertView, ViewGroup parent)
Returns a View object that will display the data in the given position in the data set.
The ApiDemos application’s views.Gallery1.java file shows off the Gallery ViewGroup nicely. The demo displays a variety of images for the user to select, and when the user does select one, the image’s index number briefly appears as toast.
The ApiDemos application also includes two example GridView Activities that show how to use the GridView. We will not examine the GridView here, because the Gallery example is so similar.
Example 11-4 shows how to use a Gallery ViewGroup. Example 11-4 shows the XML layout file (gallery_1.xml).
Here are some of the highlights of the layout code:
The id
for the Gallery
View is gallery
. As you have
seen before, the id
is used
by the findViewById function to hook a Java
Object to the XML object named in the layout file.
layout_width
is set to
fill_parent
so that the
Gallery’s width will be the same as the parent’s.
layout_height
is set to
wrap_content
, meaning that
the height will be as high as the tallest child.
Now we’ll turn our attention to the Java implementation, Gallery1.java, shown in Example 11-5. We’ve modified the code from ApiDemos slightly to remove some features that do not add to our understanding of the Gallery ViewGroup.
public class Gallery1 extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.gallery_1); // Reference the Gallery view Gallery g = (Gallery) findViewById(R.id.gallery); // Set the adapter to our custom adapter (below) g.setAdapter(new ImageAdapter(this)); // Set a item click listener, and just Toast the clicked position g.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int position, long id) { Toast.makeText(Gallery1.this, "" + position, Toast.LENGTH_SHORT).show(); } }); }
Here are some of the highlights of the code:
In the Gallery’s onCreate
method, create a Gallery
object hooked to the id
named gallery
from the XML layout.
Display each user option using the custom adapter defined in Example 11-6 (shown next).
Set up a click listener on the Gallery
object.
Display the the index (position) within the ImageAdapter of the photo the user clicked on as a Toast pop up.
In Example 11-5, the
setAdapter
function tells the
Gallery
object to use the
ImageAdapter
object as its
Adapter. Example 11-6
defines our ImageAdapter
class.
This ImageAdapter
implements all
of the abstract functions required in its base class, BaseAdapter
. For the simple case
of this demo, picture resources represent the data that the Gallery
view is displaying. An integer array, mImageIds
, contains the resource IDs of
the picture resources.
public class ImageAdapter extends BaseAdapter { int mGalleryItemBackground; private Context mContext; private Integer[] mImageIds = { R.drawable.gallery_photo_1, R.drawable.gallery_photo_2, R.drawable.gallery_photo_3, R.drawable.gallery_photo_4, R.drawable.gallery_photo_5, R.drawable.gallery_photo_6, R.drawable.gallery_photo_7, R.drawable.gallery_photo_8 }; public ImageAdapter(Context c) { mContext = c; TypedArray a = obtainStyledAttributes(android.R.styleable.Theme); mGalleryItemBackground = a.getResourceId( android.R.styleable.Theme_galleryItemBackground, 0); a.recycle(); } public int getCount() { return mImageIds.length; } public Object getItem(int position) { return position; } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { ImageView i = new ImageView(mContext); i.setImageResource(mImageIds[position]); i.setScaleType(ImageView.ScaleType.FIT_XY); i.setLayoutParams(new Gallery.LayoutParams(136, 88)); // The preferred Gallery item background i.setBackgroundResource(mGalleryItemBackground); return i; } } }
Here are some of the highlights of the code:
Defines the mImageIds
array. Each element holds a resource reference to an image that
appears in the Gallery, and each image resource name maps to the
filename in the resources directory. Thus R.drawable.gallery_photo_1
maps
directly to /res/drawable/gallery_photo_1.jpg in
the resource directory.
Sets the image for this position in the Gallery to the
image in the corresponding element of mImageIds
.
setScaleType
controls
how the image is resized to match the size of its
container.
This call to setLayoutParams
sets the size of the
ImageView
container.
ListView is similar to Gallery, but uses a vertically scrolling
list in place of Gallery’s horizontally scrolling list. To create a
ListView that takes up the entire screen, Android provides the
ListActivity
class (Figure 11-6).
The ApiDemos application includes many examples of
ListActivity. The simplest is the List1
class, which displays a huge number
of cheese names in a list. The cheese names are kept in a simple
String
array (who knew there were
that many cheese varieties!):
public class List1 extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Use an existing ListAdapter that will map an array // of strings to TextViews setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mStrings)); } private String[] mStrings = { "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi", "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", ...
Filling the ListView in the ListActivity is a simple matter of
calling setListAdapter
and
passing it an ArrayAdapter
that
contains a reference to the list of strings.
A ScrollView is a container for another View that lets the user scroll that View vertically (a scrollbar is optional). A ScrollView often contains a LinearLayout, which in turn contains the Views that make up the form.
Don’t confuse ScrollView with ListView. Both Views present the user with a scrollable set of Views, but the ListView is designed to display a set of similar things, such as the cheeses in the previous section. The ScrollView, on the other hand, allows an arbitrary View to scroll vertically. The Android documentation warns that one should never house a ListView within a ScrollView, because that defeats the performance optimizations of a ListView.
A ScrollView is a FrameLayout, which means that it can have only one child View. The most popular View for this purpose is a LinearLayout.
The following layout code from ApiDemos, scroll_view_2.xml, shows how to set up a ScrollView. The XML layout resource is sufficient; this example includes no extra Java code:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:scrollbars="none"> <LinearLayout android:id="@+id/layout" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/scroll_view_2_text_1"/> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/scroll_view_2_button_1"/> </LinearLayout> </ScrollView>
Here are some of the highlights of the code:
The unnamed ScrollView fills the width of the screen and is as tall as it needs to be to contain all of its contents. It has no scrollbars, but that’s not a problem, because scrollbars act only as visual queues in Android; they’re not as important in UIs that scroll by flicking as opposed to mousing.
The child view is a LinearLayout.
The XML layout file has two controls within the LinearLayout: a TextView and a Button. The Java code that uses this layout creates 63 more buttons, to ensure that the example LinearLayout will be larger than the screen device and big enough to scroll.
Most modern UIs provide an interface element that lets the user flip through many pages of information quickly using tabs, with each “screen” of information available when its tab is pressed. Android’s option is the TabHost View. Figures 11-7 through 11-10 show how it operates.
Android enables the developer to choose between three different approaches for setting the tab’s content. The developer can:
Set the content of a tab to an Intent. Figures 11-7 and 11-9 use this method.
Use a TabContentFactory to create the tab’s content on-the-fly. Figure 11-8 uses this method.
Retrieve the content from an XML layout file, much like that of a regular Activity. Figure 11-10 uses this method.
We’ll examine each of these possibilities using a modified Activity from the ApiDemos application. The fourth tab is not part of the ApiDemos, but combines some other TabHost demonstration Activities in ApiDemos.
Let’s start by looking at the tabs4.xml layout file (Example 11-7).
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/view4" android:background="@drawable/green" android:layout_width="fill_parent" android:layout_height="fill_parent" android:text="@string/tabs_4_tab_4"/> </FrameLayout>
Here are some of the highlights of the code:
And now we’ll dissect the Java code that produces the tabs (Example 11-8).
public class Tabs4 extends TabActivity implements TabHost.TabContentFactory { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final TabHost tabHost = getTabHost(); LayoutInflater.from(this).inflate(R.layout.tabs4, tabHost.getTabContentView(), true); tabHost.addTab(tabHost.newTabSpec("tab1") .setIndicator("intent") .setContent(new Intent(this, List1.class))); tabHost.addTab(tabHost.newTabSpec("tab2") .setIndicator("factory", getResources().getDrawable(R.drawable.star_big_on)) .setContent(this)); tabHost.addTab(tabHost.newTabSpec("tab3") .setIndicator("destroy") .setContent(new Intent(this, Controls2.class) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))); tabHost.addTab(tabHost.newTabSpec("tab4") .setIndicator("layout") .setContent(R.id.view4)); } public View createTabContent(String tag) { final TextView tv = new TextView(this); tv.setText("Content for tab with tag " + tag); return tv; } }
Here are some of the highlights of the code:
To implement tabs, you need to extend TabActivity instead of just Activity. This gives you all the tab functionality.
The tabHost
variable
allows you to define the tabs and their contents.
This basically says “using the LayoutInflater
from my current
Context
, inflate the XML
layout referenced by R.layout.tabs4
into the content
section of the tabHost
.”
Whew. As mentioned before, XML layout files are normally
inflated automatically when setContentView
runs. However, in this
case the XML layout must be instantiated manually. Note that
this XML layout is used only in the fourth tab.
Sets up the first tab (Figure 11-7). The
title is arbitrary, but we’ve called this tab intent
as documentation that its
contents are an Intent.
Set the content of the first tab to the List1.class
in this application. This
simply brings up the referenced class in the tab. This is a
slick way to make the contents of a regular application visible
inside a tab.
Now we’re setting up the second tab (Figure 11-8). This is how you put an image on a tab face.
This tab’s contents are filled in by a factory method in
this class. Notice that the class implements the TabHost.TabContentFactory
interface.
Set the content for the third tab (Figure 11-9) from an Intent. Using an Intent here is similar to navigating from one Activity in your application to another by using an intent. However, using tabs, the user can navigate back and forth between separate parts of your application quickly and easily.
Adding this flag to the tabHost
creates a new instance of the
View each time it is displayed. In the case of the demo, all
changes to the UI will be lost if you navigate away from the tab
and then back to it.
This tab displays the TextView from the XML layout item
referenced by R.id.view4
. The
TextView was set up in item 1 of Example 11-7.
This is the factory method that creates the view for the second tab. The factory must return a view that the tab will use as its content. In this case, we create a very simple TextView that displays the tag associated with the tab.
Layouts are Android’s solution to the variety of screens that come on Android devices: they can have different pixel densities, different dimensions, and different aspect ratios. Typical Android devices, such as the HTC G1 mobile phone, even allow changing the screen orientation (portrait or landscape) while applications are running, so the layout infrastructure needs to be able to respond on the fly. Layouts are intended to give developers a way to express the physical relationship of Views as they are drawn on the screen. As Android inflates the Layout, it uses the developer requests to come up with a screen layout that best approximates what the developer has asked for.
Looking a little deeper, layouts in Android are in the form of a tree, with a single root and a hierarchy of Views. Look back at any of the XML Layout files in the previous section and you’ll see that the XML tags create just such a hierarchy, with a screen Layout as the root of the tree. Each View in the tree is termed the parent of the Views it contains and the child of the View that contains it. Layout is a two-pass process:
Traversing the tree from the root, each View in the layout records its dimensional request—in other words, how much vertical height and horizontal width it needs to display itself in the final display.
Again traversing the tree from the root, each parent View uses the available layout information to position its children as requested. If the requests can’t be followed explicitly, Android does its best to make everything fit on the screen. If there are no requests given, it uses a default set of layout parameters. Each parent can pass layout information on to its children, telling them where they are positioned and what screen dimensions they have been granted (they might get less than they requested).
A Layout is a View itself, so there’s nothing wrong with having multiple Layouts in a single layout XML file—they just have to be arranged in a hierarchy. So it’s perfectly valid to have a vertical LinearLayout that includes a TableLayout as one of its rows. You’ll learn a lot more about layouts in Chapter 12.
The Frame Layout is sort of a null layout specification. It reserves space on the screen for a single View to be drawn, and the View is always located at the upper left of the space. There is no way to specify a different location for the View, and there can be only one View in the Layout. If more than one View is defined in the layout file, they are just drawn on top of each other, all pinned to the upper-left corner.
LinearLayouts are used extensively in Android applications, and we used them in example code earlier. A LinearLayout asks that the contained Views be layed out as either a series of rows (vertical LinearLayout) or a series of columns (horizontal LinearLayout). In a vertical LinearLayout, all the rows are the same width (the width of the widest child). In a horizontal LinearLayout, there is one row of Views, all the same height (the height of the tallest child).
Figure 11-11 shows an example of a vertical LinearLayout, and Figure 11-12 is an example of a horizontal one. Both have EditText Views as children. Example 11-9 shows the XML resource file that produces the vertical layout, and Example 11-10 shows the file that created the horizontal one.
<?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="EditText1" /> <EditText android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="EditText2" /> <EditText android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="EditText3" /> <EditText android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="EditText4" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="fill_parent" > <EditText android:layout_width="wrap_content" android:layout_height="fill_parent" android:text="E1" /> <EditText android:layout_width="wrap_content" android:layout_height="fill_parent" android:text="E2" /> <EditText android:layout_width="wrap_content" android:layout_height="fill_parent" android:text="E3" /> <EditText android:layout_width="wrap_content" android:layout_height="fill_parent" android:text="E4" /> </LinearLayout>
The horizontal layout might not look exactly as you would
think: how come E4 is narrower than the other three? The answer is
that there is a default minimum width for an EditText. If you build
and run the horizontal example and type something into EditText E1,
you’ll see that it expands in width as the line gets longer, which
is just what we asked for with
android:layout_width="wrap_content"
.
In addition to the usual dimensional parameters for child
Views (width, height,
padding), you can include a weight for each child
(attribute android:layout_weight=;weight
). The
weight tells the layout manager how you want to use unfilled space,
and defaults to a value of 0. If you specify children with weights
greater than zero, the layout manager will allocate unused space to
each child in proportion to its weight.
Figure 11-13 shows an example of a LinearLayout containing four EditTexts. The first two have no weights assigned. EditText3 has a weight of 1 and EditText4 has a weight of 2. The effect is to make EditText4 twice as big as EditText3, while EditText1 and EditText2 just split whatever space the layout leaves over.
A TableLayout is just what you’d expect: it lays out the included Views in the form of a table (similar to an HTML table). We can create a table of TextViews to show how you would create that kind of screen for an application. Here’s an example TableLayout XML file:
<?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tblJobs" android:layout_width="fill_parent" android:layout_height="wrap_content" > <TableRow android:layout_width="fill_parent" android:layout_height="wrap_content"> <Button android:text="Cell 11" android:id="@+id/btnCel11" android:layout_width="20dip" android:layout_height="wrap_content" /> <TextView android:id="@+id/txtCell12" android:layout_width="20dip" android:layout_height="wrap_content" android:text="Cell 12" /> <TextView android:id="@+id/txtCell13" android:layout_width="20dip" android:layout_height="wrap_content" android:text="Cell 13" /> <TextView android:id="@+id/txtCell14" android:layout_width="20dip" android:layout_height="wrap_content" android:text="Cell 14" /> </TableRow> <TableRow android:layout_width="fill_parent" android:layout_height="wrap_content"> <Button android:text="Cell 21" android:id="@+id/btnCo21" android:layout_width="80dip" android:layout_height="wrap_content" /> <TextView android:id="@+id/txtCell22" android:layout_width="80dip" android:layout_height="wrap_content" android:text="Cell 22" /> <TextView android:id="@+id/txtCell23" android:layout_width="80dip" android:layout_height="wrap_content" android:text="Cell 23" /> <TextView android:id="@+id/txtCell24" android:layout_width="80dip" android:layout_height="wrap_content" android:text="Cell 24" /> </TableRow> </TableLayout>
Figure 11-14 shows the resulting layout on the emulator screen.
The structure of the XML file is pretty evident: the
TableLayout tags contain a list of TableRows that in turn contain
the Views you want to appear on each line of the table. Notice that
the layout_width
values are
different in the two rows—all the widths in the first row are
specified as 20dip
, whereas the
widths in the second row are specified as 28dip
—yet the columns line up on the
screen. To preserve the look of a table, Android makes each column
as wide as the widest cell in that column.
Of course, the cells are addressable from your Java code, and you can add rows programmatically to the table, if that’s what your application needs to do.
An AbsoluteLayout puts views on the screen wherever you tell it to. It doesn’t try to resize anything, and it doesn’t try to line anything up; it just puts things where it’s told. You might think that it would be an easy type of layout to use, since you don’t have to second-guess how the layout manager is going to rearrange things on your screen, but in practice the use of AbsoluteLayout is a bad idea for almost all applications. You usually want your application to run on as many Android devices as possible, and the strength of the Android layout manager is that it will automatically adapt your screen layout from device to device. AbsoluteLayout bypasses most of the layout manager, and while your application may look perfect on the device you used for development, the odds are very good that it will look terrible on other Android devices.
That warning aside, let’s take a look at an AbsoluteLayout XML file:
<?xml version="1.0" encoding="utf-8"?> <AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Upper Left" android:layout_x="0.0px" android:layout_y="0.0px" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Middle" android:layout_x="140.0px" android:layout_y="200.0px" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Lower Right" android:layout_x="240.0px" android:layout_y="400.0px" /> </AbsoluteLayout>
As with any dimension in a layout file, the
positions can be expressed in pixels (px
), device-independent pixels (dp
), scaled pixels (sp
), inches (in
), or millimeters (mm
), and the dimension has to be a
floating-point number. (For more about expressing sizes, see Dimensions in Android in Chapter 4.)
Figure 11-15 shows the resulting screen layout. Obviously, the position (0, 0) is the upper-left corner of the display, and the View is properly flush with the corner. The lower-right corner on the emulator is supposed to be (320, 480), but the View appears to be a little shy of that in both dimensions.
Just to caution against the use of AbsoluteLayout again, we
suggest you try changing the emulator skin to show the screen in landscape mode (enter
emulator
-skin
HVGA-L
from a command or terminal window before
you run the application), and you can see in Figure 11-16 that
the application no longer looks right.
We’ve used RelativeLayout, often in combination with LinearLayout, throughout the MJAndroid application. The advantage of RelativeLayout is that you can express the relative positioning of the Views in the screen, and the layout manager will do its best to fit them all on the screen in the proper relations. An example follows:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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/txtText1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Text1" android:gravity="top" android:layout_alignParentRight="true" /> <TextView android:id="@+id/txtText2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Text2" android:layout_below="@+id/txtText1" /> <Button android:id="@+id/btnButton1" android:layout_width="150dp" android:layout_height="wrap_content" android:text="Button1" android:layout_below="@+id/txtText2" /> <Button android:id="@+id/btnButton2" android:layout_width="150dp" android:layout_height="100dp" android:text="Button2" android:layout_toRightOf="@+id/btnButton1" android:layout_alignTop="@+id/btnButton1" /> </RelativeLayout>
Figure 11-17 shows what this looks like in portrait mode (the emulator default), and Figure 11-18 shows it in landscape mode. The layout manager has adjusted the arrangements in each case to match the layout hints we gave in the XML.
44.200.94.150