Saturday, March 12, 2011

ActionBar before Honeycomb

The Android Honeycomb UI now prominently features an "action bar" -- a bar at the top of the screen where an application can put its icon, application name and menus, either in the form of icon or as text or as a popup.

If you have no idea what I'm talking about, I recommend you start by having a look at Reto's excellent article on the subject: Android App Surgery: Earthquake Redux (Honeycomb Tablet Edition).

That article however doesn't say much about 2 things: how complicated is it to use the new action bar, and how to maintain compatibility with 99.8% of pre-Honeycomb platforms.

I won't kill you with suspense: the answer to the first question is that it's fairly trivial. As an Android developer, your first reaction should always be to run the ApiDemos sample matching the latest platform of the SDK.

In this case you'll find 4 examples that best explain what you can do here:

  • You can activate the action bar using a window feature or a theme (preferred).
  • You can select what the bar displays: icon, text, custom view, search field, menu items, popup menu.
For testing, I decided to update my Mandelbrot Map application for Android. Here's what it looks like on a phone running Gingerbread and below on a Xoom running Honeycomb, with no modification whatsoever.

In this application, I remove the title bar since it doesn't add much value and instead I replace it by a text view with the zoom parameters, number of iterations and coordinates in the Mandelbrot set. Note that the application doesn't hide the status bar -- this is not a "total immersion" game, instead it's the kind of modest utility one my run whilst waiting for an appointment or something to happen, so we want to keep notifications and time visible.

Even on the Xoom, out of the box it looks pretty good. The only drawback is the "old style" menu at the bottom, which isn't very pretty. But it's still very functional and that was an amazing zero amount of work so far -- the application already correctly declared it supported large screen sizes in its manifest.



So the first action is to get rid of the "old style" menu. To do this, all you have to do set android:targetSdkVersion to 11 in your manifest -- this tells the system you know your application can deal with API 11, so you'll get the new theme:

<manifest ...>
<uses-sdk
    android:targetSdkVersion="11"
    android:minSdkVersion="4" />
...

This will remove the "old" menu icon in the bottom bar. However rhe application will then suddenly offer no way for users to access the menu! How did that happen?!

That's because in this application I was overriding the default theme to use a theme with no title bar.
What we need is to change it for Honeycomb, yet we want to keep it as it was for previous versions. We can easily do that by defining the activity theme in a resource file and customizing it for version 11.

First set a theme on your activity -- this is a much better way than setting the theme or the window features programmatically as it helps the system initialize the screen to look right from the very start:

<activity
  android:name=".tiles.TileActivity"
  android:theme="@style/ThemeMandelWindow" />

Now we'll define the theme twice, once in res/values and once in res/values-v11:
In res/values/styles.xml:
<resources>
  <style name="ThemeMandelWindow" parent="android:Theme.NoTitleBar">
  </style>
</resources>

In res/values-v11/styles.xml:
<resources>
  <style name="ThemeMandelWindow" parent="android:Theme">
  <item name="android:windowActionBar">true</item>
  </style>
  </resources>

Where does that "windowActionBar=true" come from? It's all explained in ApiDemos in the app/ActionBarMechanics.java source. I use a resource style because the behavior is static. Had I wanted a dynamic behavior (e.g. depending on an intent or in response to a user action), I could have used this in the Activity.onCreate():
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
For most applications, you can stop here. You'd have an action bar with the default theme color, an icon and a label, and a menu drop-down if you have menu items in your activity.

For the Mandelbrot application however, I have this text view at top that displays some information. As an exercise, I'm going to display it in the action bar as a custom view. I also want to change the action bar background color.

First, the layout. I want that text view in all versions with API 10 or lower, and no text view in the version for API 11 or higher.  I will however want that text view as a custom widget for API 11 and in this case it's just simpler to define it once using a custom XML, then include it in the main layout for API 10.

First we'll need an XML just for the custom text view, in res/layout/infoview.xml:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/text"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:lines="1"
  android:text="Mandelbrot Map"
  android:textAppearance="@style/TextCaption"
  android:gravity="center"
  android:background="@color/dark_blue"/>

Now in res/layout/tiles.xml we include this; this is for APIs 10 and lower:
<RelativeLayout
  android:layout_width="fill_parent"
  android:layout_height="fill_parent" >
  <include layout="@layout/infotext"
  android:layout_alignParentTop="true"
  android:layout_alignParentRight="true"
  android:layout_alignParentLeft="true" />
  <com.alfray.mandelbrot2.tiles.TileView
    android:id="@+id/tile_view" ... />
</RelativeLayout>

And we can have a version for API 11 without the include in res/layout-v11/tiles.xml:
<RelativeLayout
  android:layout_width="fill_parent"
  android:layout_height="fill_parent" >
  <com.alfray.mandelbrot2.tiles.TileView
    android:id="@+id/tile_view" ... />
</RelativeLayout>

Finally in the activity we just need to change the action bar to insert the custom view:
// Grab the text view. This will be null in API 11 and higher.
TextView textView = (TextView) findViewById(R.id.text);
if (Build.VERSION.SDK_INT >= 11) {
  // Instantiate a new custom view based on the infotext layout file
  textView = (TextView) getLayoutInflater().inflate(R.layout.infotext, null);

  // Get the action bar
  ActionBar bar = getActionBar();
  android.app.ActionBar.LayoutParams lp =
  new ActionBar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
  lp.gravity = Gravity.CENTER;
  // Set the custom view and tell the action var to display it.
  bar.setCustomView(textView, lp);
  bar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, ActionBar.DISPLAY_SHOW_CUSTOM);
  // Set the background of the action bar
  Resources r = getResources();
  bar.setBackgroundDrawable(new ColorDrawable(r.getColor(R.color.dark_blue)));
}
textView.setText("initial values");

Now here's the end result:


That was quite trivial and the application is one step towards having a UI more consistent with the rest of an Honeycomb tablet. Now the next step would be to create some actions that are displayed directly in the action bar and take them off the options menu, but that's left as exercise to the reader.

1 comment:

  1. Thanks a lot ! I just couldn't figure out where that f***ing action bar ! It drove me crazy all night !
    Great post :)

    ReplyDelete