Anyone who has used a modern computer or mobile device is familiar with many of the common elements that make up user interfaces: buttons, check boxes, lists, sliders, and a number of other types of controls. Each of these elements is represented by a Java class, and these classes form a rich and quite large class hierarchy.
The basic building block for Android GUI elements is the android.view.View
class. All other GUI elements are subclasses of View
. These views take up a portion of the screen and are rendered in a variety of different ways to convey information to the user, and they also respond to various kinds of user interaction, such as clicking (or tapping), dragging, keyboard input, and so forth.
There are two main categories of views. The first category, widgets, typically extend View
or another subclass of View
and are used to provide the user with an area to interact with the app, such as by clicking a button. Another category of views, called view groups, act as invisible "containers" that organize other views (or even other view groups, creating deeply nested structrues). These view groups often provide functionality to automatically lay out the views they contain – called their children – in a specific arrangement on the screen.
In the latest version of the Android API at the time of this writing (API level 16, "Jelly Bean"), there are 78 direct and indirect subclasses of View
in the platform. Obviously we don't have room to talk about all of them! So below we're going to discuss some of the most common types of widgets that you may want to use in your apps.
The most common type of action-based widget in the Android API is android.widget.Button
. This widget can contain a text label or an image and it will fire an event when the user clicks it. See Responding to Events for a discussion of how you can handle these events to carry out actions.
Sometimes you want a button that you can toggle on or off to indicate whether an option is enabled or disabled. Android provides a few of these:
Check boxes (android.widget.CheckBox
) are rendered as a button with a checkmark image and a text label describing the purpose of the button. When the user clicks a check box, it toggles its state from on to off, or from off to on.
Toggle buttons (android.widget.ToggleButton
) are functionally the same as check boxes, but they are rendered as a rectangular button that includes an area that "lights up" when the button is checked. Toggle buttons can also use different text labels when they are on or off.
Radio buttons (android.widget.RadioButton
) are similar to check boxes, but with the added feature that you can create a set of them so that only one can be checked at any time. In other words, checking one of the buttons will turn off the other ones in the same group, so they can be used to represent a mutually exclusive set of options.
To indicate which radio buttons should be treated as a single set of options, you must place them inside an android.widget.RadioGroup
.
Each of these types of buttons extends a common superclass, android.widget.CompoundButton
. Because of this, they all share common methods for accessing or modifying that state. The android.widget.CompoundButton#isChecked()
method returns true
if the button is checked or false
if it is not. Likewise, the android.widget.CompoundButton#setChecked(boolean)
method lets you change the state of the button from within your code.
Also, like regular buttons, these "stateful" buttons fire events when clicked if you need to be able to respond to that immediately with an action.
When you need the user of your app to be able to enter some text, the android.widget.EditText
widget exists for this purpose.
This one class provides a wide variety of input mechanisms for entering different kinds of textual data. It can be configured to support single-line entry or multi-line entry, and a variety of input types allow you to restrict the format of the data being entered. For example, you can create EditText
widgets that support arbitrary text, or that support only numeric data, or telephone numbers, to list a few. For devices that have on-screen keyboards, the keyboard can reconfigure itself to display only keys that are relevant for that particular input type when a widget is selected.
The text currently entered in the widget can be requested by calling the android.widget.EditText#getText()
method.
getText()
method for EditText
widgets does not return a String
but rather an instance of android.text.Editable
, which represents an editable buffer (because String
objects cannot be changed once created). In order to get the text as a string that you can use, call getText().toString()
.
Likewise, calling android.widget.TextView#setText(CharSequence)
lets you change the text in the widget from within your code. (CharSequence
is an interface that both String
and Editable
implement.)
Sometimes you just need to put a small informational piece of text on the screen without providing any interaction with it – for example, to show the result of a computation or to label another widget to indicate its purpose. The android.widget.TextView
class is used for this.
Android applications can be composed of four types of application components:
A single screen that provides a user interface.
A component that runs in the background to complete a long-running computation without disrupting the user.
A component that manages shared data that can be made available to other applications.
A component that responds to system-wide broadcast notifications, such as the battery life falling below a threshold, or the phone being locked.
Each of these application components is an entry point for either the user, the operating system, or other applications to access your app. For example, activities allow the user to interact with your app, while content providers allow other apps to access data created by your app and its users.
Most simple applications, such as those you will do in this class, will consist entirely of activities that represent the interface that the user will interact with.
In the Android API, all activities extend the android.app.Activity
class, which provides a common set of functionality for the GUI features of an application. Apps built using Sofia should instead extend the sofia.app.Screen
class, which is a subclass of Activity
that includes additional behavior. (The name of the class reinforces the notion that it represents and controls a single screen of the application's user interface.)
Each screen should present the user with some sort of interface that he or she can interact with. Most applications will extend the Screen
class (or one of the other specialized subclasses that Sofia provides) and write the functionality for that screen of their app in that class.
Imagine that we are writing a very basic temperature converter app to convert between Celsius and Fahrenheit scales. We would only need one screen for this simple app, which would contain some TextView
informational labels, some EditText
fields to enter the temperatures, and perhaps another Button
to reset the fields in the app.
The first step of creating this app is to create a new subclass of Screen
in the application's main package, cs2114.demos.temperatures
. Let's call this class TemperatureScreen
. The simplest possible class that we can write is an empty one:
package cs2114.demos.temperatures; import sofia.app.Screen; public class TemperatureScreen extends Screen { }
We could actually run this program right now. If we did, we would see a blank screen. This makes sense, since we haven't written anything that would put something more meaningful there. So how do we tell Android what should go on the screen? How does it know what buttons, text fields, lists, and other widgets to use?
Fortunately, using Sofia, you don't have to write a single line of code to associate a GUI with your screen class. Recall that the res/layout
folder of your project is where you put XML layout files that describe the look of your application. Using the Android GUI editor built in to Eclipse, you can drag and drop widgets onto a layout and see a very close approximation of how your app will look. You can see one possible example for this app in the image to the right.
When you extend Screen
, Sofia will automatically find the layout for that screen if you give it a filename that matches the name of your class, converted to all lowercase, followed by the .xml
extension.
TemperatureScreen
, we want to create a layout file named temperaturescreen.xml
in the project's res/layout
folder. You can use XML directly from the example below, or you can create your own version by designing it yourself.
<?xml version="1.0" encoding="utf-8"?> <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"> <TextView android:id="@+id/fahrenheitLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginLeft="14dp" android:layout_marginTop="14dp" android:text="Degrees Fahrenheit:" android:textAppearance="?android:attr/textAppearanceMedium" /> <EditText android:id="@+id/fahrenheitField" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/fahrenheitLabel" android:layout_alignParentRight="true" android:layout_below="@+id/fahrenheitLabel" android:layout_marginRight="16dp" android:ems="10" android:inputType="numberDecimal|numberSigned" /> <TextView android:id="@+id/celsiusLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/fahrenheitField" android:layout_below="@+id/fahrenheitField" android:layout_marginTop="14dp" android:text="Degrees Celsius:" android:textAppearance="?android:attr/textAppearanceMedium" /> <EditText android:id="@+id/celsiusField" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/celsiusLabel" android:layout_alignRight="@+id/fahrenheitField" android:layout_below="@+id/celsiusLabel" android:ems="10" android:inputType="numberDecimal|numberSigned" /> <Button android:id="@+id/clear" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignRight="@+id/celsiusField" android:layout_below="@+id/celsiusField" android:layout_marginTop="14dp" android:text="Clear Fields" /> </RelativeLayout>
The identifiers that we assign to views in a layout (using the android:id
attribute or right-clicking the view and choosing Edit ID... from the menu) is very important, because this is how we access those views inside our code.
@+id/fahrenheitField
. For the purposes of understanding the raw XML, you can ignore the @+id/
part. The identifier of that widget is fahrenheitField
, which is what you would type into the Edit ID... dialog in the GUI editor.
The RelativeLayout
at the root of the layout file is a type of view group that uses a constraints system to position views relative to other views. In the layout above, for example, the widget with the identifier fahrenheitField
is positioned so that it is below and aligned with the left edge of the widget fahrenheitLabel
. The remaining widgets are positioned in a similar fashion. You can see these constraints as you drag and drop widgets onto your layout, as the editor draws arrows and locks things into place.
With this layout file in place with the correct name, we can run the app again. Sofia will find the layout automatically and display it when the screen is loaded on the device. Of course, we haven't written any code to make the app do anything when the user interacts with it, so matter how much you click the button or type text into the fields, nothing meaningful will happen. That comes next.
There are two main ways that we can bridge the gap between the GUI layout and the Java code that we write in our app:
Obtain a reference to the actual Java object that represents the widget. We need to do this if we want to be able to call methods on the widget – for example, to get the text currently entered in an EditText
or to find out if a CheckBox
is checked.
Listen for events generated by the widget. When the user clicks a button, or presses Enter after typing into a text field, or selects an item from a list, or interacts with the app in a number of other ways, the Android runtime fires an event. We want to respond to those events in order to perform an action in response to that user interaction.
In order to access properties and information about a widget at runtime, we need to have a reference to the actual Java object that represents that widget. Sofia makes this very easy. For each widget in your layout that you want a reference to, declare a private field in your screen class whose name is exactly the same as the widget ID and whose type is compatible with the type of the widget in the layout. When your screen class is initialized, these fields will be automatically filled in with references to those widgets.
Consider the layout above for the temperature converter app. Of the five widgets it contains, the program would need references to the two EditText
widgets in order to pull the value that the user typed and to update the other widget with the converted temperature.
To obtain references to those widgets, simply declare the following fields:
public class TemperatureScreen extends Screen { private EditText fahrenheitField; private EditText celsiusField; }
Since these fields are automatically filled in, you can immediately make a call like fahrenheitField.getText().toString()
elsewhere in your code to retrieve the text that was typed into the field.
When the user interacts with a view or a widget in your app, that view or widget generates an event. Your app needs to contain event handlers to respond to those events. For example, when a button is clicked or when the user types something into a field, you want to have the app call a method to perform an appropriate action.
Remember that your screen class represents the controller in your application. This is where most of your event handling code should be located, since the controller intercepts events from views and translates them into appropriate calls on the model. (In this simple example, we don't really have a model, however. All of the state that we need is stored in the text fields.)
So how do we write a method to handle an event from a widget? Sofia makes this easy as well. We just have to write a public void
method in our screen class with a very specific name and it will be automatically called when a widget generates an event. The name of this method is based on the ID of the widget and the type of event that we want to handle. There is no need to set any properties in the layout XML file or manually connect any listeners to the widgets.
For example, if you want to have your app do something when the button with the ID clear
is clicked, you can write the following method:
public void clearClicked() { // Do something to respond to the button click. }
Similarly, if you want an action to occur when the user has typed something into a text field with the ID fahrenheitField
and pressed Enter or clicked the "Done" button, you can write a method like this:
public void fahrenheitFieldEditingDone() { // Do something to respond to the editing operation. }
Sometimes you need a reference to the widget that generated the event as well. If you don't want to declare a field that will hold that reference (maybe you only need the reference inside the event handler and nowhere else), then you can also write your method to take a single parameter that has the same type as your widget. You could rewrite the two examples above like this (notice that the parameter does not have to have the same name as the widget ID):
public void clearClicked(Button button) { // Do something to respond to the button click. } public void fahrenheitFieldEditingDone(EditText field) { // Do something to respond to the editing operation. }
Sofia currently supports handling the following kinds of events in this way (replace id
with the ID of the view and ViewType
with the type of the view):
Action to handle | Supported by these views/widgets | Write a method like this |
---|---|---|
Clicking a view | Mainly Button s, but any subclass of View that is not also a subclass of AdapterView and where isClickable() returns true |
public void idClicked() orpublic void idClicked(ViewType widget) |
Completing an edit in a text field | EditText |
public void idEditingDone() orpublic void idEditingDone(EditText widget) |
Clicking an item in a list | ListView |
public void idItemClicked(Object item) orpublic void idItemClicked(Object item, int position) |
Selecting an item in a spinner | Spinner |
public void idItemSelected(Object item) orpublic void idItemSelected(Object item, int position) |
Let's use this feature to write a method that will be called when the use enters a temperature in the Fahrenheit field:
public void fahrenheitFieldEditingDone() { double degreesF = Double.parseDouble(fahrenheitField.getText().toString()); double degreesC = (degreesF - 32) * 5 / 9; celsiusField.setText(Double.toString(degreesC)); }
In the method above, we ask the fahrenheitField
for its current text value and convert it to a floating-point value. Then we convert it to degrees Celsius and finally update the text value of celsiusField
to contain the converted value.
Doing the same for the Celsius field is almost identical:
public void celsiusFieldEditingDone() { double degreesC = Double.parseDouble(celsiusField.getText().toString()); double degreesF = degreesC * 9 / 5 + 32; fahrenheitField.setText(Double.toString(degreesF)); }
If we want to clear the fields when that button is clicked (maybe you want the temperatures you're converting to be secret!), then handling the clear
button's click event is as easy as writing another method:
public void clearClicked() { celsiusField.setText(""); fahrenheitField.setText(""); }
For quick reference, here is the entire Java source for the TemperatureScreen
class. If you combine this with the layout XML file above, you will have your first completely functional Android/Sofia app!
package cs2114.demos.temperatures; import sofia.app.*; import android.widget.EditText; public class TemperatureScreen extends Screen { private EditText fahrenheitField; private EditText celsiusField; /** * This method is called when the user presses Enter in the * "fahrenheitField" widget. */ public void fahrenheitFieldEditingDone() { double degreesF = Double.parseDouble(fahrenheitField.getText().toString()); double degreesC = (degreesF - 32) * 5 / 9; celsiusField.setText(Double.toString(degreesC)); } /** * This method is called when the user presses Enter in the * "celsiusField" widget. */ public void celsiusFieldEditingDone() { double degreesC = Double.parseDouble(celsiusField.getText().toString()); double degreesF = degreesC * 9 / 5 + 32; fahrenheitField.setText(Double.toString(degreesF)); } /** * This method is called when the user clicks the "clear" button. */ public void clearClicked() { celsiusField.setText(""); fahrenheitField.setText(""); } }
As you know from doing a good bit of Java programming already, most objects are initialized by constructors: special methods that are called when new objects are created so that the fields in those objects can be set to hold starting values.
For activities and screens, however, we do not initialize them inside their constructors. There are a couple reasons for this:
The Java object is created before the screen has been integrated into the rest of the application's context. When the screen's constructor is called, it hasn't been linked up with other resources or widgets yet.
Since memory is limited on mobile devices, activities can be disposed when they're not being used in order to free up resources. This might leave the same Screen
object in memory but re-initialize it multiple times to restore its layout once it's brought back into view.
Therefore, instead of writing constructors for classes that subclass Screen
, you should write a public void
method named initialize()
instead. This method will be called whenever the screen is about to be presented to the user for the first time, or when it is about to appear again after it has been off screen long enough that it has been removed from memory. By the time initialize()
is called, your screen's layout has been inflated and any widget references are automatically filled in so that you can refer to them to prepare the initial state of your application.
public class TemperatureScreen extends Screen { public void initialize() { // Do initialization here, not in the constructor. } }
initialize()
method takes no parameters. There are situations that will be discussed later where you may want to write other versions of this method that take different parameters.
In versions of Android up to 2.3, applications can have an options menu that is hidden until the user presses the Menu key on his or her device. The options menu can be used to hold actions or settings that you want the user to be able to access quickly but that are not important enough to occupy screen space at all times.
Starting with Android 3.0, Google extended the notion of the options menu into an action bar. The action bar is a thin toolbar at the top or bottom of the screen and menu items can be optionally placed there instead of in a hidden menu.
Menus are defined using XML files that you store in the res/menu
folder of your project. An example of this file is shown below.
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/red" android:title="Red" android:icon="@+drawable/menu_red"/> <item android:id="@+id/green" android:title="Green" android:icon="@+drawable/menu_green"/> <item android:id="@+id/blue" android:title="Blue" android:icon="@+drawable/menu_blue"/> <item android:id="@+id/clear" android:title="Clear" android:icon="@+drawable/menu_clear"/> </menu>
Fortunately, as with GUI layout files, the Android plug-ins for Eclipse provide a more convenient user interface to edit these resources.
By default, when you extend Screen
(or one of its subclasses), there will be no menu associated with that screen – pressing the Menu button on your device will do nothing. The easiest way to associate a menu with a screen is to do the following two tasks:
Create a menu resource in res/menu
that is named with the lowercased name of your screen class (similar to creating layouts). For example, the TemperatureScreen
class would store its menu in res/menu/temperaturescreen.xml
.
Add the @OptionsMenu
annotation to the top of your screen class:
@OptionsMenu public class TemperatureScreen extends Screen
Then, pressing the Menu button will make the menu appear on the screen. Note that both steps are required to make the menu work. Unlike a screen layout, just putting the properly named file in the resource directory is not enough.
If you want to use a menu resource with a name different than that of the Screen
, then you can pass the name of the resource to the annotation:
@OptionsMenu("my_menu") // loads res/menu/my_menu.xml public class TemperatureScreen extends Screen
Menus generate events when an item in the menu is clicked. In fact, menu items generate the same Clicked
events that buttons and similar widgets do. So, if you have a menu item with the ID clear
, the following method would be called when it is clicked:
public void clearClicked() { // Called when either a button or menu item with the ID // "clear" is clicked }
The advantage of this is that if a screen has a button with a particular ID and a menu item with the same ID, selecting either one will invoke the same action. (However, you probably wouldn't want to design a GUI that has the same action in two places!) If you find that you absolutely must distinguish between these two events, you can overload the method with a parameter that indicates the source of the event:
public void clearClicked(Button button) { // Called when a button with the ID "clear" is clicked } public void clearClicked(MenuItem menuItem) { // Called when a menu item with the ID "clear" is clicked }