Android Logo MathCS.org - Android

User Interface Design with XML

java -> android -> designing with XML ...

In this segment we will (finally) use XML files to define resources and layout an interface to properly create a program. It is easier to do, allows you to preview your look quickly (without running your program), and separates data from logic. This is the preferred method to create Android programs.

The goal is to move all data-related parts of a program to a human-readable XML file. That includes all constants, in particular string constants, but also labels, widgets, and layouts. This approach allows you to quickly adjust or refine a program's layout as well as to change text and constants. It is particularly useful when you create an internationalized program that uses languages and formatting conventions different from US english.

To see how it works, let's recreate our temperature conversion program from before and hopefully improving it. As usual, we start by creating a new project for Android 1.5 or above, called, say, FirstXMLApp (which you should be able to do on your own by now). Eclipse will create the following files and folders for your project:

android_project_files

Of these files and folders we want to particularly focus on the resource file strings.xml and the layout file main.xml. Double-click on strings.xml. You can add, edit, and remove string resources, or view the raw XML file. To practice, remove the hello resource and add a string resource named title_string with value "Temperature Converter". As you can see, each resource has a name and a value. Internally resources are referenced by integers, but you can use the @ symbol to refer to a resource by name. To refer to our new title string resource, for example, you would (soon) use the reference:

   @string/title_string 

You might have noticed that your resource contains an error, introduced when you removed the string resource hello, which is referred to in the main.xml layout file and now invalid. Double-click main.xml to edit it. Eclipse lets you edit this layout file in graphical mode or in XML source mode. The more convenient graphical mode looks like this:

eclipse_layout_editor

You add views and layouts by dragging them from the left, or by highlighting an existing one on the list at the right and click the plus sign (to add) or the minus sign (to remove). You can also reorder the elements using the up/down arrows to properly nest layouts and views. You get a preview of your layout in the middle, and you can switch between landscape and portrait mode. Finally, you can inspect the properties of the element highlighted on the right in a property pane (not shown in the above image).

As a quick exercise, highlight the TextView element on the right and look for the property referencing the hello string. Then remove the text view inside the linear layout, since its reference to the hello resource string is invalid. As soon as you do, and save your changes, the project is again error-free.

Note: The layout preview is very helpful, but you should always verify your *true* look by loading your app on a real device(s) close in properties to your intended target device(s).

As an exercise, we'll recreate the layout of our previous temperature conversion app, which, as you may recall, looked something like this:

converter_sketch_1

xml_converter_outline

Tip: An easy way to create this layout is to first add all layouts, then all text views, then all edit texts. After that move them into place using the up/down arrows. Finally, give everything proper names (ids). At the end, add the two buttons and id them.

Now, create five string resources named title_string (you should have that already), celsius_label, fahrenheit_label, to_celsius_button, and to_fahrenheit_button with appropriate values. Switch back to the layout editor, highlight the TitleLabel and set its Text property (near the bottom) to @string/title_string. Note that a "browse" button (with three dots) appears when you click on the Text property from which you can pick instead of type the correct (string) resources. Similarly change the text that appears in the other two labels and on the buttons to string references.

A Word about Units: different Android units have different screen resolutions (number of pixels vertically and horizontally). Thus, text that is 12 pixels high might look perfectly readable on one device but could appear tiny (or huge) on another device. Thus, the preferred unit to specify size information either "dp" (density-independent pixels) or "sp" (scale-independent pixels), not "px" (traditional pixels), with "sp" being the best choice for size that appears to have the same size relative to screen density and user's font size preferences.

At this point our layout looks no better than our previous effort. In fact, it looks exactly the same as before. BUT, it is now easy to adjust the properties of the various views and layouts to quickly improve the look:

Now our layout is better than before, it was easier to do, it could easily be changed to another language, say German, and we can "preview on the go" without having written a single line of Java code:

xml_converter_layout

But we can easily do even better: one source of problem for our previous program was that a user could enter text that did not represent a number. We can now configure the edit texts to not allow that:

That does not change the way the field looks but now the field itself does not allow any input that is not a number (it still allows for empty input, though).

Exercise: Since the two words in front of the input fields have different widths, the input fields do not align properly. Change the layout so that they do, as shown below.
Hint: You could also experiment with a TableLayout, which has a Stretch column property you should set to 1. And a TableLayout should contain one or perhaps two TableRow layouts, as previously mentioned. Note that columns adjust automatically to the number of widgets in a TableRow.

xml_converter_layout_perfect

Now that our layout is perfect it is time to move to the programming portion by opening FirstXMLApp.java (remember the source code file).

We need handles to the two text fields (so we can move text in and out) and the two buttons (so we can attach on-click listeners). To get a variable to point to an XML resource we can use the inherited function findViewById(ID), where ID is a generated id that is part of the automatically generated R object. For example:

    Button toCelsiusButton = (Button) findViewById(R.id.ToCelsiusButton);

would define a Button named toCelsiusButton that is initialized by the view defined in the main XML layout file and named ToCelsiusButton, returned via findViewById and type-cast into a Button.

Warning: Before you can "find a view by ID", you must have loaded the layout components defined in the main XML file. In other words, before you can use findViewById you must have called setContentView(R.layout.main)

The (inner) handler classes are just as before, as are the converter methods they are using, but the number of fields can be reduced to two (why?) and the code in the onCreate method that previously had to do all the setup work is significantly easier. Here is the complete code for our new FirstXMLApp.java:

public class FirstXMLApp extends Activity 
{
	private EditText celsiusField = null;
	private EditText fahrenheitField = null;
	
	private class ToCelsiusHandler implements View.OnClickListener
	{
		public void onClick(View v)
		{
			convertToCelsius();
		}	
	}
	
	private class ToFahrenheitHandler implements View.OnClickListener
	{
		public void onClick(View v)
		{
			convertToFahrenheit();
		}	
	}

    	public void onCreate(Bundle savedInstanceState) 
    	{
        	super.onCreate(savedInstanceState);
	        setContentView(R.layout.main);
                
	        Button toCelsiusButton = (Button) findViewById(R.id.ToCelsiusButton);
	        Button toFahrenheitButton = (Button) findViewById(R.id.ToFahrenheitButton);

	        celsiusField = (EditText) findViewById(R.id.CelsiusField);
	        fahrenheitField = (EditText) findViewById(R.id.FahrenheitField);
        
	        toCelsiusButton.setOnClickListener(new ToCelsiusHandler());
	        toFahrenheitButton.setOnClickListener(new ToFahrenheitHandler());
    	}

	private void convertToCelsius()
	{
		try
		{
			String fahrenheitString = fahrenheitField.getText().toString();
			double f = Double.parseDouble(fahrenheitString);
			double c = 5.0/9.0 * (f - 32.0);
			celsiusField.setText(String.valueOf(c));
		}
		catch(Exception ex)
		{
			Toast.makeText(this, "Fahrenheit invalid", Toast.LENGTH_SHORT).show();
		}
	}

	private void convertToFahrenheit()
	{
		try
		{
			String celsiusString = celsiusField.getText().toString();
			double c = Double.parseDouble(celsiusString);
			double f = 9.0/5.0 * c + 32.0;
			fahrenheitField.setText(String.valueOf(f));
		}
		catch(Exception ex)
		{
			Toast.makeText(this, "Celsius invalid", Toast.LENGTH_SHORT).show();
		}
	}

}

This will run just as before but looks nicer and the code is easier to understand since all the setup work has been outsourced to XML. We did make one mistake, though: we flagged the edit fields as "number" in their XML properties, which means we can now only enter integers, no decimals. That is too restrictive - can you find a more appropriate value (or values) for the Input type to finish up this segment?