Monday, April 28, 2008

Custom views

Now that I entertained myself (and hopefully the readers of the blog as well) with custom adapters and animations, I set out to an even more adventurous exercise. In Android, entirely new view classes could be created and used in conjunction with XML layout files, similarly to built-in views. Views have their own lifecycle. Intererested readers should consult the documentation of android.view.View class. Very shortly: views may handle the event when their inflation is finished, when they are being measured by the layout manager, when they are being laid out, when their size change, when they gain or lose focus, when they are attached to and detached from a window and when the window they are attached to changes visibility. This is pretty complex so I chose an easier way to introduce custom views by extending a built-in view.

The scenario which absolutely requires custom views is the following: we have a list of alarm events and some of them requires the user's acknowledgement. There is a flashing exclamation mark beside such alarms. Like this (except that the exclamation mark is flashing:)


You can download the example program from here.

We will support the list with a custom adapter, as we have seen previously. A single row for this adapter looks like this (located under res/layout/alarm_row.xml, XML mangling because of blog engine limitation).

[?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="wrap_content"
android:orientation="horizontal"]

[TextView android:id="@+id/alarmtext"
android:textSize="16px"
android:layout_width="280px"
android:layout_height="wrap_content"/]

[aexp.customview.AlarmingView
android:id="@+id/alarming"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/]
[/LinearLayout]

TextView is our old friend but the second view in the row is our own. If you check the aexp.customview.AlarmingView class, you will see that it is a descendant of ImageView. We still have to define three constructors (one for the creation from code, 2 others for inflating from XML) but the rest of the view lifecycle handling is inherited from ImageView. The flashing effect is achieved by periodically updating the drawable resource displayed by the underlying ImageView.

There is one point worth highlighting. There is a single private class handling the periodic flashing (coded as singleton for all AlarmingView instances to save resources and achieve synchronous flashing) but that class does not use the java.lang.Thread mechanism. If you code it that way (I did first :-( ), the application is shut down with an error message that views are not thread-safe and can be manipulated only from the thread that created them. Hence, we create the private class instance when the first AnimatedView is created and therefore the Handler instance in the private class will also be created in the context of the thread that creates the AnimatedView in the first place. Then we request this Handler instance to update the state of our views periodically.

Tuesday, April 22, 2008

Animated views

I have to admit, I am aesthetically challenged. Creating a nice GUI is not my game and I respected those who could design such things. Because of this limitation of mine, I generally look down on glittering UIs with contempt. But even I was moved - even if only a bit - by Android's animation capabilities.

Android is able to animate any View object and each and every View has a startAnimation() method that launches animation. Animations exist as independent objects that can be applied to the Views. Animation objects can even be populated from XML resources that live in the res/anim directory.

You can download the example program from here.

Let's look at the animation file below (again, XML mangling due to the limitations of the blog engine). This file can be found as res/anim/magnify.xml in the example program bundle.

[?xml version="1.0" encoding="utf-8"?]
[scale xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXScale="0.5"
android:toXScale="2.0"
android:fromYScale="0.5"
android:toYScale="2.0"
android:pivotX="0%"
android:pivotY="50%"
android:startOffset="0"
android:duration="400"
android:fillBefore="true" /]

This is a scaling animation. The attributes mean the followoing:
  • The content of the view is actually shrinken first (fromXScale, fromYScale=0.5) to half before it starts to grow to double its standard size and 4 times of the initial size (0.5->2.0). This is done both in X and Y axis.
  • The pivot point from which the object grows is 0% in the X axis. This means that the leftmost points of the object will stay in place and the object grows rightward to double size. Meanwhile, the pivot point on the Y axis is the middle of the object (50%), this means that the object will grow up and down from its center line.
  • The animation starts immediately (startOffset=0) and finishes in 400 msec.
Once we have an animation object, we can apply it to any view. In the example program, I created a simple list and applied the animation to the TextView list elements whenever one list element is selected (either by moving the selection with the arrow keys or clicking at list elements). Two points are worth noting:
  • Just because we animate, the list layout is not recalculated. Hence the generous top and bottom paddings around the list elements. There is enough space provided for the TextView to grow.
  • In the list row layout (res/layout/row.xml), the layout_width is set to fill_parent. This is seemingly random choice but actually, the program does not work well with wrap_content as layout_width. Whenever a list element is selected, its color changes to highlight. If the width is wrap_content, then the size of the TextView (hence the higlighted screen area) is just the length of the text. When the animation starts, the text grows past this size and the end of the text becomes unreadable. This is very important therefore to allow the list row to occupy the entire available width because then the entire row will be highlighted and the animation cannot grow larger than the highlighted area.
At last, two pictures about the beginning and the end of the animation.


Monday, April 21, 2008

Custom widget adapter based on XML layout

Lex from anddev.org pointed out an annoying property of my custom Weather adapter example: the view row the Weather objects are transformed to are encoded in Java instead of being defined in XML layout resource. This makes the weather display style harder to modify and every application using the WeatherAdapter has the same weather screen layout.

You can download the example program from here.

I created a new version of WeatherAdapter that takes an additional parameter in its constructor, an ID of a composite view (a LinearLayout in our case) that has three children views having the IDs "city", "temperature" and "sky" respectively. The code is flexible enough to handle the case when one or more of these child views are missing, in this case the field will simply not set from the Weather object. Check the res/layout/weather_row.xml file for an example.

The new version is indeed more flexible which is demonstrated by the bit more complicated display of weather rows. Thanks for the feedback, Lex, it really made this example program more valuable.


Friday, April 18, 2008

Custom widget adapters

In an earlier post, I wrote about the SimpleAdapter and how SimpleAdapter allows significant flexibility when laying out list items. SimpleAdapter is great but then I became curious what it takes to write an adapter.

Adapters are simple devices. On one side of the adapter is a data structure like a Java object storing data. SimpleAdapter handles Java objects that can be meaningfully translated into Strings by invoking the objects' toString() method (every Java object supports that but for quite many of them, the toString() format is not meaningful for the end user). On the other side of the adapter, there is a View that the data structure was transformed into. That View is displayed to the user. As we use Adapters to supports list views, the Adapter handles lists of Java objects (that are eventually transformed into a list of Views).

Android's built-in adapters are sufficiently versatile but it is often handy to create a custom adapter. Let's look at the following example.



You can download the example code from here.

The example is a simple weather display. A weather entry consists of 3 data items: name of the city (String), temperature in the city in degrees (integer) and an icon showing whether the sky is sunny, overcast or it is raining. The first two entries could be handled using SimpleAdapter but the third cannot: SimpleAdapter does not handle icons. Therefore we create our own adapter, called WeatherAdapter that takes list of Weather objects storing the weather info and turns that list into Views that can be rendered as list rows.

The most important part of the trick happens in a private class in WeatherAdapter.java called WeatherAdapterView. WeatherAdapter does nothing more than manages a list of WeatherAdapterViews. WeatherAdapterView is the View the Weather data object is mapped to. It is itself a composite View, composed by a LinearLayout. The LinearLayout is set up programmatically (as opposed to an XML layout) and contains two TextViews and one ImageView. The ImageView encapsulates the icon. It is worth checking how the icon images are referenced in Weather.java: as the icons are in the res/drawable directory, R.java has integer IDs for them. The getSkyResource() method in the Weather class just returns this resource ID based on the sky member variable of the Weather class.