Archive

Archive for the ‘ui’ Category

Why navigation drawers are bullshit

October 25, 2013 Leave a comment

I’ve been asked several times to implement the oh-so-popular navigation drawer for folder selection in AquaMail, replacing the old-fashioned drop-down list.

The technical part is easy, but the more I get into it, the more it seems like a bad idea. Here is why.

TL;DR – I’m happy for the young energetic designers who are making a career out of “inventing imaginative ways to improve mobile device navigation patterns”, but there are significant drawbacks which may outweigh the claimed benefits.

1 – I’m faced with the decision of whether I should keep the drop-down or not.

Having two entirely different UI elements that do same thing (let the user directly navigate between folders) is strange, putting it mildly.

Let’s look at the options:

- Remove the drop-down entirely and keep just the drawer.

This is bad for users who are not experts on Android UI patterns. Yes, the drawer indicator is there (the three tiny horizontal stripes to the left of the app’s icon), but… Sometimes I get emails asking how to access the menu, and there are users who barely know about the Back button. Nothing wrong with these people, they’re just not experts on technology or UI, and I’d rather not make the app more confusing to them.

- Remove the drop-down, make the folder name (where the drop-down is currently anchored) another way to open and close the drawer. The native Gmail app does this.

The issue is how to make it obvious that this is how things work? The drop-down is easy to see and understand because it has a little triangle bottom right, like all other drop-downs everywhere else.

Keeping the triangle makes the UI immediately inconsistent: it looks like a drop-down, but when tapped, it does something else (opens the side navigation drawer).

Removing the triangle is even worse (the native Gmail does this): now there is no visual clue that this is not just a piece of text, but an active UI element.

This last issue is now very common in Android and becomes more and more common. Personally, as a user, I find it confusing like hell. It used to be that buttons looked like buttons, lists looked like lists, etc. – now UI elements that “do stuff” no longer look like anything, they’re just areas the user is supposed to tap (or slide? which way? or long press?), and the only way to find out is to use mind-reading skills.

2 – The navigation drawer is anatomically wrong.

It forces the thumb to move sideways (left to right) which is awkward in the first place, and then, when it’s almost touching the palm, the thumb has to move vertically to select from the now open navigation drawer’s list.

Really, if you’re reading this, try it yourself and pay attention to how your thumb’s muscles tighten when it’s bent and closest to the index finger. It’s at this point that the navigation drawer requires the thumb to start moving up and down to pick from the list.

Furthermore, a drop-down list can extend the entire width of the screen (or close to it), so the thumb does not have to move left much at all. A navigation drawer, by convention, is anchored on the left, and is more narrow than the screen (the shadow effect). Reaching sideways to the left is hard, especially on large screen devices. Reaching up is easier: the phone can shifted in the hand, up and down; but this does not work for reaching to the left.

3 – The navigation drawer doesn’t achieve what it’s supposed to.

Ostensibly, the motivation (or one of) is better usability on large screen devices, the explanation being that a drop-down anchored in the action bar is difficult to reach.

However, a side drawer also has a list, of same items, and it’s natural to start them at the top (unless you have some UI element that can go first, just to take up space… see the Google+ app, it’s the profile photo).

Let’s see, we wanted to get away from a list that starts near the top, and we ended up with a list that starts near the top, which is not any easier to reach, and which puts additional stress on the thumb’s muscles.

4 – How do other app do it?

My favorite type of question (why isn’t Mr. Obama any good at Judo?)

The native Gmail app sort of manages to make its navigation drawer more usable by moving action bar icons to the top, which in turn pushes the drawer’s activation area (the current folder’s name) to the left.

But at what price?

Android 4.0 introduced a way to put action bar icons along the bottom on “narrow screen devices”, i.e. phones. This is a great feature, making frequently used actions easier to reach (better location; a larger number of icons showing at the same time).

Now Gmail’s way of implementing the drawer puts the icons back at the top. Let’s see, the number of icons is now reduced from 4 or 5 to just 2, the rest require tapping the “three dots” icon or the hardware Menu button. And these two icons are now located — ta-da! — near the top of the screen.

In summary, “one small but very visible step forward, two giant steps back” is what Gmail’s navigation bar treatment seems to me. They made 2 or 3 commands (tasks) less accessible, to improve accessibility of 1 command (task): switching folders.

Will I keep the already-almost-implemented navigation drawer?

Really don’t know yet. Maybe I’ll add a Prada logo instead.

Ye Old Fashioned Drop-Down Liste

Ye Old Fashioned Drop-Down Liste

Zee Modern Navigation Drawer

Zee Modern Navigation Drawer

Categories: android, ui

Another fast scroller helper for ListView

November 22, 2011 1 comment

Android’s ListView has a built-in way to implement fast (indexed) scrolling.

As useful as it is, it has two limitations that sometimes can be critical:

- A section name is assumed to be a single letter.
– If the list content changes over time, it’s difficult to update the section list on the go.

The fast scroller helper class in the framework is closely integrated with AbsListView, and so it does not seem possible to just replace it with another implementation, or change the way it behaves.

I needed one of these for my current project, and found a fast scroller that was posted on Google Code by Android’s Dianne Hackborn.

This code is from 2008, before there was a fast scroller implementation in the framework, and so its way of interacting with the list is different. This class is derived from FrameLayout and should be used in the layout XML file as the list view’s parent.

I combined this code with the fast scroller from the Ice Cream Sandwich sources. The result looks like this:

This class is used the same way as the FastScrollView linked to above – it should be inserted into the layout XML as the list view’s parent.

It has a method for updating the section index, which can also optionally reset the current drag tracking state (or not, specified as a parameter).

The section overlay’s size and its font size are specified as dimension resources. Long (over 1 character) section names are displayed correctly.

Finally, I also fixed some rendering issues on Android 3.0 when running with hardware acceleration enabled.

The code is too large to post here, so I uploaded it to Google Code. A sample / test project is included.

You can get it here: a wiki page is here and the code is here.

Categories: android, ui

AlertDialog, custom ListView items, and Honeycomb

October 15, 2011 1 comment

Here is another compatibility tip for Honeycomb and previous versions. This one has to with custom ListView item layouts inside alert dialogs.

Ok, so I have a few places in my app that display an AlertDialog with a ListView inside, backed up by a adapter that provides custom item layouts. Getting the text color inside those layouts is a little tricky.

Prior to Honeycomb, dialogs always use a dark background and light text, regardless of which theme (light or dark) is used by their parent activity. This is true for AlertDialogs and plain Dialogs. However, lists inside AlertDialogs use an inverted (light) background. This is usually enabled automatically, or you can enable it yourself by calling AlertDialog.setInverseBackgroundForced. Now TextViews inside item views returned by your adapter can use black text, or, preferably, just in case the color is customized by the device manufacturer, a theme attribute reference (one of textAppearanceMediumInverse, or textColorPrimaryInverse, etc.).

Here is an alert dialog with a list view, running on the Sony Ericsson Xperia Arc. The background is not quite white, it’s a bit greyish (purplish?), and the text is pure black.

The item layout looks like this (only text views are shown):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/pick_attachment_root"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content">

	<TextView
		android:textAppearance="?android:attr/textAppearanceMediumInverse"
		android:id="@+id/pick_attachment_main_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_toRightOf="@id/pick_attachment_icon"
		android:layout_toLeftOf="@id/pick_attachment_toggle"
		android:singleLine="true"
		android:ellipsize="middle"
		android:textStyle="bold" />

	<TextView
		android:textAppearance="?android:attr/textAppearanceInverse"
		android:id="@+id/pick_attachment_sub_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_toRightOf="@id/pick_attachment_icon"
		android:layout_toLeftOf="@id/pick_attachment_toggle"
		android:layout_below="@id/pick_attachment_main_title" />

</RelativeLayout>

On Honeycomb and later, if you use the native, Holographic, themes, dialogs can be light or dark, matching the parent activity’s theme. AlertDialogs with lists look the same as regular dialogs with text, so this is what the above layout looks like on an Sony Tablet S running Android 3.2:

The text views turned invisible, white on white. How did this happen? The dialog’s background is no longer inverted, as the dialog’s theme matches the parent activity’s, but our text appearance references still assume inverted background: for a light parent theme, it’s “whatever text color is appropriate over the inverse of a light background”, in other words, “whatever is appropriate for a black background”, which is white. Switching the parent activity to a dark theme produces black text on black background by the same logic.

Ok, so for Honecomb, we’re supposed to use non-inverted theme attribute references. How to do this without duplicating our layouts inside res/layout-v11? My solution is this:

First, define these styles inside res/values-v4/list_styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

	<!-- Text for listboxes, inverted for Andorid prior to 3.0 -->

	<style name="MyListTextAppearanceSmall">
		<item name="android:textAppearance">?android:attr/textAppearanceSmallInverse</item>
	</style>

	<style name="MyListTextAppearanceDefault">
		<item name="android:textAppearance">?android:attr/textAppearanceInverse</item>
	</style>

	<style name="MyListTextAppearanceMedium">
		<item name="android:textAppearance">?android:attr/textAppearanceMediumInverse</item>
	</style>

</resources>

Then use these styles inside the layouts returned by the adapter backing up the listview inside the AlertDialog (… that Jack built…). The layouts themselves don’t need to be duplicated, mine are in the “basic” res/layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/pick_attachment_root"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content">

	<TextView
		style="@style/MyListTextAppearanceMedium"
		android:id="@+id/pick_attachment_main_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_toRightOf="@id/pick_attachment_icon"
		android:layout_toLeftOf="@id/pick_attachment_toggle"
		android:singleLine="true"
		android:ellipsize="middle"
		android:textStyle="bold" />

	<TextView
		style="@style/MyListTextAppearanceDefault"
		android:id="@+id/pick_attachment_sub_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_toRightOf="@id/pick_attachment_icon"
		android:layout_toLeftOf="@id/pick_attachment_toggle"
		android:layout_below="@id/pick_attachment_main_title" />

</RelativeLayout>

Now all that’s left to do is to redefine these three styles inside res/values-v11/list_styles.xml and to use regular, not inverted, text appearance references:

<?xml version="1.0" encoding="utf-8"?>
<resources>

	<!-- Text for listboxes, non-inverted starting with Android 3.0 -->
	
	<style name="MyListTextAppearanceSmall">
		<item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
	</style>
	
	<style name="MyListTextAppearanceDefault">
		<item name="android:textAppearance">?android:attr/textAppearance</item>
	</style>

	<style name="MyListTextAppearanceMedium">
		<item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
	</style>

</resources>

The same dialog now looks like this:

And when the parent activity uses a theme dervied from @android:style/Theme.Holo, the dialog items will turn black, with light text.

Categories: android, ui

Custom themes for Honeycomb and backwards compatibility

October 7, 2011 5 comments

Here is a couple of things I learned while recenly updating my apps to have the native look on tablets running Android Honeycomb (3.*).

My app lets the user select a UI theme (there are two: light and dark), so I had something like this in my res/values/styles.xml:

<declare-styleable name="WifiTheme">
	<attr name="radar_text" format="color" />
	<attr name="radar_alpha" format="color" />
</declare-styleable>

<style name="WifiTheme.Light" parent="@android:style/Theme.Light">
	<item name="radar_text">#FF202020</item>
	<item name="radar_alpha">#80000000</item>
</style>

<style name="WifiTheme.Dark" parent="@android:style/Theme">
	<item name="radar_text">#FFFFFFFF</item>
	<item name="radar_alpha">#A0000000</item>
</style>

I would then apply one of these themes, from inside onCreate, to my activities.

Now, Honeycomb has new “Holographic” UI themes that look different from 2.* themes, and users expect an application running on a tablet to use those. The two new themes are @android:style/Theme.Holo.Light and @android:style/Theme.Holo.

It’s possible to duplicate the XML snippet above in res/values-11/styles.xml, replacing the parent themes, but that would mean duplicating all the values for those themes. The snippet above has just two extra attributes, but there could be many more of them.

Ok, so here is a trick I came up with. Just like with program code, what we need to get out of a tight spot is an extra level of abstraction.

First, I defined the following themes in res/values/theme_compat.xml:

<style name="ThemeCompat">
</style>

<style
	name="ThemeCompat.Dark"
	parent="@android:style/Theme">
</style>

<style
	name="ThemeCompat.Light"
	parent="@android:style/Theme.Light">
</style>

The first one is just to keep the Android style hierarchy system happy, the other two are where the real work is. You can think of them as defining a base class (in programming terms) for my custom themes. My custom theme declarations are now derived from the themes shown above, and look like this:

<style name="WifiTheme.Light" parent="@style/ThemeCompat.Light">
	<item name="radar_text">#FF202020</item>
	<item name="radar_alpha">#80000000</item>
</style>

<style name="WifiTheme.Dark" parent="@style/ThemeCompat.Dark">
	<item name="radar_text">#FFFFFFFF</item>
	<item name="radar_alpha">#A0000000</item>
</style>

And for when the application runs on Android 3.*, I have the following declarations in res/values-11/theme_compat.xml:

<style
	name="ThemeCompat.Dark"
	parent="@android:style/Theme.Holo">
</style>

<style
	name="ThemeCompat.Light"
	parent="@android:style/Theme.Holo.Light">
</style>

What this does is replaces the “base class” of my custom themes with those native to Honeycomb. Now my custom theme declarations don’t need to be duplicated, and are automatically derived at runtime from the most appropriate system themes.

Here is another thing. AlertDialogs with custom content views.

AlertDialogs can be sometimes tricky, because they have their own styling and can even impose it on their content. But I feel that using them in place of regular dialogs makes an application’s UI better. The light panel behind the buttons, the title bar with the icon, the separator line below that – those may seem insignificant little things, but I feel they really make an app look better. Some manufacturers (Samsung, Sony Ericsson, perhaps others) make changes to stock Andorid UI to add their own color and graphics accents, and so it becomes even more important to use AlertDialog to better blend in with the device’s UI.

And this is another thing where Honeycomb is different from previous Android versions. Prior to 3.0, AlertDialogs always use a dark background (unless overriden for lists), even if the parent activity’s theme is light. Starting with 3.0, alert dialogs have the same overall light/dark color scheme as the base activity.

If your alerts use custom content views, it becomes tricky, because those views need to be inflated using an appropriate Context, so they match the dialog’s color scheme.

It’s really easy to end up with white text on white background, unless you use the following method, new with API level 11: AlertDialogBuilder.getContext().

Since this method doesn’t exist prior to API level 11, it’s possible to use Java reflection to stay compatible with older Android versions. Turns out there is another way: by subclassing AlertDialog, and overriding onCreate(), it’s possible to call getContext() for the AlertDialog itself. That method exists since API level 1, and returns a correctly themed Context for both 3.* and prior versions of Android. A further shortcut is to call getLayoutInflater(), which also exists since API level 1.

With this in mind, here is a simple alert dialog:

public class DlgAbout extends AlertDialog {

	public DlgAbout(Context context) {
		super(context);
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {

		setTitle(R.string.about_title);
		setCancelable(true);

		LayoutInflater inflater = getLayoutInflater();

		View contentGroup = inflater.inflate(R.layout.about_content, null);
		setView(contentGroup);

		super.onCreate(savedInstanceState);
	}
}

The dialog’s layout can use views without any specific color customizations:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"

.....

<!-- Note the absence of android:textColor, etc -->
		<TextView
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:text="@string/about_1"
			android:gravity="center_horizontal" />
......

The code to use this dialog is below. Not really that much different from using AlertDialog.Builder, and is actually somewhat better because the code for the dialog is placed in its own class (above).

@Override
protected Dialog onCreateDialog(int id) {
	if (id == DIALOG_ID_ABOUT) {
		/*
		 * About the app
		 */
		return new DlgAbout(this);
	}
.........
}
Categories: android, ui

Implementing long-clickable preferences

August 29, 2011 10 comments

The built-in Preference class has a method to receive clicks, onClick, but no method to receive long clicks. In my current project, I actually have a need for this, and found a way to implement it.

PreferenceActivity is actually a ListActivity, with a special adapter behind the scenes. The usual (not long) clicks are processed by using the usual ListView mechanism, setOnItemClickListener. The code to set this up is in PreferenceScreen:

public final class PreferenceScreen extends PreferenceGroup implements AdapterView.OnItemClickListener.... {

    public void bind(ListView listView) {
        listView.setOnItemClickListener(this);
        listView.setAdapter(getRootAdapter());
        
        onAttachedToActivity();
    }

    public void onItemClick(AdapterView parent, View view, int position, long id) {
        Object item = getRootAdapter().getItem(position);
        if (!(item instanceof Preference)) return;
        
        final Preference preference = (Preference) item; 
        preference.performClick(this);
    }
}

It would be really easy to subclass PreferenceScreen and override bind to add a long-item-click listener to the list view, except this class is final. Because of this, I ended up adding the following code into my PreferenceActivity subclass:

public class BlahBlahActivity extends PreferenceActivity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {

		super.onCreate(savedInstanceState);

		addPreferencesFromResource(R.xml.account_options_prefs);

		ListView listView = getListView();
		listView.setOnItemLongClickListener(new OnItemLongClickListener() {
			@Override
			public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
				ListView listView = (ListView) parent;
				ListAdapter listAdapter = listView.getAdapter();
				Object obj = listAdapter.getItem(position);
				if (obj != null && obj instanceof View.OnLongClickListener) {
					View.OnLongClickListener longListener = (View.OnLongClickListener) obj;
					return longListener.onLongClick(view);
				}
				return false;
			}
		});
	}
}

Now I can have a Preference subclass that implements View.OnLongClickListener, which is automatically invoked for long clicks:

public class BlahBlahPreference extends CheckBoxPreference implements View.OnLongClickListener {

	@Override
	public boolean onLongClick(View v) {
		// Do something for long click
		return true;
	}
}
Categories: android, ui

Simple animated panels

April 26, 2011 Comments off

A common UI element in Android applications is an animated panel that provides access to certain actions. A panel like this should only be shown temporarily, perhaps triggered by other UI state changes initiated by the user.

You can see an example of this in GMail application: when you select one or more messages, a panel slides up from the bottom with buttons to act on those selected messages.

While using a PopupWindow is cetainly an option for this, it can be quite convenent to define the panel as a view group within the activity’s layout.

With a unified layout, the activity can obtain a reference to the panel’s view group with findViewById, and change its visibility state with View.setVisibility, either to View.VISIBLE or View.GONE.

Just changing the panel’s visibility isn’t by itself animated, especially when going from VISBLE to GONE: view animations are only shown if the view is visible. When hiding the panel, it’s necessary to first run the “slide out” animation, and only then set the visibility to GONE.

So – here is a simple class that makes it very easy to implement animated panels. It is constructed with a reference to the panel’s parent ViewGroup, and the type of animation (from right / from bottom), and has methods to animate the panel to slide in / out.

Here is a simple example of how this might look:

The class keeps track of the panel’s state, including currently running animations. When requested to hide the panel, it runs a “hide” animation first, and when the animation completes, but not sooner, changes the panel’s visibility to View.GONE.

public class PanelAnimation {

	public static final int WHERE_RIGHT = 0;
	public static final int WHERE_BOTTOM = 1;

	public PanelAnimation(ViewGroup viewGroup, int where) {
		mViewGroup = viewGroup;
		mContext = viewGroup.getContext();
		mWhere = where;
		mState = STATE_HIDDEN;
	}

	public boolean isShownOrShowing() {
		return mState == STATE_SHOWING || mState == STATE_SHOWN;
	}

	public void toggle() {
		if (mState == STATE_SHOWN) {
			hide();
		} else if (mState == STATE_HIDDEN) {
			show();
		}
	}

	public void showNow() {
		mState = STATE_SHOWN;
		mViewGroup.setVisibility(View.VISIBLE);
	}

	public void show() {
		if (mState != STATE_SHOWN) {
			mState = STATE_SHOWING;

			switch (mWhere) {
			default:
			case WHERE_RIGHT:
				mShowAnimation = AnimationUtils.makeInAnimation(mContext, false);
				break;
			case WHERE_BOTTOM:
				mShowAnimation = AnimationUtils.loadAnimation(mContext, R.anim.slide_in_up);
				break;
			}

			mShowAnimation.setAnimationListener(new AnimationListener() {
				@Override
				public void onAnimationStart(Animation animation) {
				}

				@Override
				public void onAnimationRepeat(Animation animation) {
				}

				@Override
				public void onAnimationEnd(Animation animation) {
					mState = STATE_SHOWN;
				}
			});
			mViewGroup.startAnimation(mShowAnimation);
			mViewGroup.setVisibility(View.VISIBLE);
		}
	}

	public void hide() {
		if (mState != STATE_HIDDEN) {
			mState = STATE_HIDING;

			switch (mWhere) {
			default:
			case WHERE_RIGHT:
				mHideAnimation = AnimationUtils.makeOutAnimation(mContext, true);
				break;
			case WHERE_BOTTOM:
				mHideAnimation = AnimationUtils.loadAnimation(mContext, R.anim.slide_out_down);
				break;
			}

			mHideAnimation.setAnimationListener(new AnimationListener() {
				@Override
				public void onAnimationStart(Animation animation) {
				}

				@Override
				public void onAnimationRepeat(Animation animation) {
				}

				@Override
				public void onAnimationEnd(Animation animation) {
					mState = STATE_HIDDEN;
					mViewGroup.setVisibility(View.GONE);
				}
			});
			mViewGroup.startAnimation(mHideAnimation);
		}
	}

	/**
	 * Attachment panel animation
	 */
	private static final int STATE_HIDDEN = 0;
	private static final int STATE_SHOWING = 1;
	private static final int STATE_SHOWN = 2;
	private static final int STATE_HIDING = 3;

	private int mState = STATE_HIDDEN;

	private ViewGroup mViewGroup;
	private Context mContext;
	private int mWhere;

	private Animation mShowAnimation;
	private Animation mHideAnimation;
}

This class uses two animations defined as resources, used when the panel is positioned at the bottom.

<?xml version="1.0" encoding="utf-8"?>
<!-- res/anim/slide_in_up.xml -->
<translate
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:interpolator="@android:anim/accelerate_interpolator"
	android:fromYDelta="100%"
	android:toYDelta="0%"
	android:duration="250" />
<?xml version="1.0" encoding="utf-8"?>
<!-- res/anim/slide_out_down.xml -->
<translate
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:interpolator="@android:anim/accelerate_interpolator"
	android:fromYDelta="0%"
	android:toYDelta="100%"
	android:duration="250" />

When the panel is on the right, this class uses standard Android animations (also used for activity animations).

To use this class, first add panel views to your activity’s layout .xml:

<?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">

	<!-- Normal content -->

	<ListView
		android:id="@+id/message_list"
		android:layout_width="fill_parent"
		android:layout_height="0dp"
		android:layout_weight="1" />

	<!-- Button panel -->

	<LinearLayout
		style="@style/ButtonPanel"
		android:id="@+id/message_list_ops_panel"
		android:visibility="gone">

		<TextView
			style="@style/ButtonLikeText"
			android:id="@+id/message_list_op_delete"
			android:text="@string/message_list_op_delete" />

		<TextView
			style="@style/ButtonLikeText"
			android:id="@+id/message_list_op_as_read"
			android:layout_marginLeft="4dp"
			android:text="@string/message_list_op_as_read" />

		<TextView
			style="@style/ButtonLikeText"
			android:id="@+id/message_list_op_as_unread"
			android:layout_marginLeft="4dp"
			android:text="@string/message_list_op_as_unread" />

	</LinearLayout>

</LinearLayout>

Now in Java, get a refrence to the ButtonPanel ViewGroup and construct PanelAnimation for it:

class SomeActivity {
	@Override
	public void onCreate(Bundle savedInstanceState) {
		setContentView(R.layout.some_layout_here);

		mMessageOpsPanel = (ViewGroup) findViewById(R.id.message_list_ops_panel);
		mMessageOpsPanelAnimation = new PanelAnimation(mMessageOpsPanel,
				PanelAnimation.WHERE_BOTTOM);

	}

	private ViewGroup mMessageOpsPanel;
	private PanelAnimation mMessageOpsPanelAnimation;
}

Finally, showing and hiding the panel can be done like this:

	private void onMessageOpsChange(boolean visible) {
		if (visible) {
			mMessageOpsPanelAnimation.show();
		} else {
			mMessageOpsPanelAnimation.hide();
		}
	}

And that’s really it.

Categories: android, ui

An EditText for entering IP addresses

February 27, 2011 10 comments

So I needed to create a custom EditText for entering IP addresses. I wanted it to do two things: one, display the numpad-style IME, and two, accept only digits and the dot character.

There is no pre-defined EditText input type for this, and although using android:digits="0123456789." comes pretty close, but doesn’t quite do it for me, since it doesn’t switch the IME into the numpad mode.

As it turns out, implementing special rules for IP address entry is not too difficult, it just takes a special KeyListener.

The docs say that KeyListener is an interface for converting text key events into edit operations on an Editable class.

There are a bunch of pre-defined KeyListener subclasses, which are accessible to SDK applications, and we’re going to use one of them as the base class for our IPAddressKeyListener.

Associating an EditText with our key listener is done like this:

	EditText edAddress = (EditText) findViewById(R.id.advanced_network_address);
	edAddress.setKeyListener(IPAddressKeyListener.getInstance());

Almost ready to show the code. Just a quick reminder on what this class gives us:

  • Switches the on-screen keyboard (the IME) to numpad mode
  • Only allows digits 0 through 9 and the dot character
  • The first character has to be a digit (not a dot)
  • At most, 3 dot characters are allowed

The code:

public class IPAddressKeyListener extends NumberKeyListener {

	private char[] mAccepted;
	private static IPAddressKeyListener sInstance;

	@Override
	protected char[] getAcceptedChars() {
		return mAccepted;
	}

	/**
	 * The characters that are used.
	 * 
	 * @see KeyEvent#getMatch
	 * @see #getAcceptedChars
	 */
	private static final char[] CHARACTERS =

	new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' };

	private IPAddressKeyListener() {
		mAccepted = CHARACTERS;
	}

	/**
	 * Returns a IPAddressKeyListener that accepts the digits 0 through 9, plus the dot
	 * character, subject to IP address rules: the first character has to be a digit, and
	 * no more than 3 dots are allowed.
	 */
	public static IPAddressKeyListener getInstance() {
		if (sInstance != null) return sInstance;

		sInstance = new IPAddressKeyListener();
		return sInstance;
	}

	/**
	 * Display a number-only soft keyboard.
	 */
	public int getInputType() {
		return InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL;
	}

	/**
	 * Filter out unacceptable dot characters.
	 */
	@Override
	public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
			int dend) {
		CharSequence out = super.filter(source, start, end, dest, dstart, dend);

		if (out != null) {
			source = out;
			start = 0;
			end = out.length();
		}

		int decimal = -1;
		int dlen = dest.length();

		// Prevent two dot characters in a row
		if (dstart > 0 && dest.charAt(dstart - 1) == '.') {
			decimal = dstart - 1;
		}
		if (dend < dlen && dest.charAt(dend) == '.') {
			decimal = dend;
		}

		// Up to three dot charcters, and no more
		if (decimal == -1) {
			int decimalCount = 0;
			for (int i = 0; i < dstart; i++) {
				char c = dest.charAt(i);

				if (c == '.') {
					decimalCount++;
					decimal = i;
				}
			}
			for (int i = dend; i < dlen; i++) {
				char c = dest.charAt(i);

				if (c == '.') {
					decimalCount++;
					decimal = i;
				}
			}

			if (decimalCount < 3) {
				decimal = -1;
			}
		}

		SpannableStringBuilder stripped = null;

		for (int i = end - 1; i >= start; i--) {
			char c = source.charAt(i);
			boolean strip = false;

			if (c == '.') {
				if (i == start && dstart == 0) {
					strip = true;
				} else if (decimal >= 0) {
					strip = true;
				} else {
					decimal = i;
				}
			}

			if (strip) {
				if (end == start + 1) {
					return ""; // Only one character, and it was stripped.
				}

				if (stripped == null) {
					stripped = new SpannableStringBuilder(source, start, end);
				}

				stripped.delete(i - start, i + 1 - start);
			}
		}

		if (stripped != null) {
			return stripped;
		} else if (out != null) {
			return out;
		} else {
			return null;
		}
	}
}

Some additional validation is still necessary (like checking that individual byte values are less than or equal to 255), but that can be done when the user leaves the edit control.

Categories: android, ui

Customizing AlertDialog, an update – removing the title

February 27, 2011 6 comments

A follow-up to an earlier post about customizing AlertDialog.

Jonas Petersson asked whether it’s possible to remove an alert dialog’s title. It is. Add a call to requestWindowFeature in your subclass’s onCreate, make sure it comes before the call to super.onCreate():

	@Override
	protected void onCreate(Bundle savedInstanceState) {

		setView...
		setButton...

		/*** This removes the title ***/
		requestWindowFeature(Window.FEATURE_NO_TITLE);

		super.onCreate(savedInstanceState);
	}

And this is what it looks like:

Categories: android, ui

Customizing AlertDialog

February 19, 2011 5 comments

Android has two ways to create dialogs: for simple ones, there is AlertDialog and its builder, for more complex there is Dialog.

AlertDialog looks very nice: the bottom panel (for buttons) is highlighted, the title bar has an icon, the font sizes are just right. But at the same time, AlertDialog can be too restrictive. You can replace the content view and configure the buttons, but you can’t change the logic of how these buttons work, and you can’t get to individual UI components to modify their behavior.

Or so I thought.

Whenever I needed a dialog with some logic to it (such as disabling buttons until the user provides correct input), I used to use plain Dialog. It did what I needed, but my dialogs looked different than AlertDialog, as you can see in the Android Development Guide.

The resources that Android uses for AlertDialog are: 1) private, and can’t be referenced directly from an application using “@android:drawable/imgname” and 2) can be customized by the hardware vendors (my Samsung Galaxy S uses a bluish scheme and a different title bar icon). For those two reasons, I had to find a way to customize AlertDialog without trying to copy those resources into my project and rebuild the way the UI looks with my own code.

Sidebar: I’m really not too happy with how some Andorid resources can’t be referenced from an application. Especially menu icons: for example, ic_menu_close_clear_cancel and ic_menu_add are accessible under “@drawable:android”, but ic_menu_refresh, or ic_menu_attachment are private and have to be copied into the project. The trouble, of course, is that hardware manufacturers can customize those, and your application will look inconsistent).

Ok, so it turns out, you can subclass AlertDialog, and get all the benefits with respect to its appearance, and still implement the behavior logic as needed.

public class MyPrettyDialog extends AlertDialog {

	private ListView mListView;
	private MyAdapter mAdapter;
	private Button mAcceptButton;

	public MyPrettyDialog(Context context) {
		super(context);
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		ListView listView = new ListView(mContext);
		listView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
				LayoutParams.WRAP_CONTENT, 1.0f));
		listView.setId(android.R.id.list);

		MyAdapter adapter = new MyAdapter();

		listView.setAdapter(adapter);
		listView.setOnItemClickListener(adapter);

		mListView = listView;
		mAdapter = adapter;

		setInverseBackgroundForced(true);
		setView(listView);
		setTitle(R.string.mytitle);
		setCancelable(false);

		setButton(DialogInterface.BUTTON_POSITIVE, "Positive",
				new OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						// This is replaced later
					}
				});

		setButton(DialogInterface.BUTTON_NEGATIVE, "Negative",
				new OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						// Do something
					}
				});

		super.onCreate(savedInstanceState);

		/*
		 * Replace the click listener
		 */
		mAcceptButton = getButton(BUTTON_ATTACH);
		mAcceptButton.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// Do something, the dialog is NOT closed automatically
			}
		});
	}
}

The most important part here is the overriden onCreate(). It is called automatically by the framework as nedded (just like for Activities). Creating this dialog is as simple as when using AlertDialog.Builder:

	@Override
	protected Dialog onCreateDialog(int id) {
		if (id == MY_PRETTY_DIALOG) {
			MyPrettyDialog dialog = new MyPrettyDialog(this);
			return dialog;
		}
		return super.onCreateDialog(id);
	}

The real value of the above code is that we can get references to UI widgets and keep them in the dialog (mListView, mAcceptButton).

With this, we can for example disable the “Accept” button until the user provides required data. Also, AlertDialog, by default, closes right after the user clicks any of the buttons, and you can’t avoid that. If you would like to do some validation (keeping the “Accept” button enabled, and explaining to the user what he needs to fill in and how), you can do that by replacing the click handler by the button with your own, as the code above does at the end of onCreate.

And this is what I actually ended up with – an AlertDialog with pretty involved processing logic, that still has the look the users expect.

Categories: android, ui

Large (tablet) screen impact on Android Application UI

January 29, 2011 Leave a comment

A few days Google released a perview of the next version of Andorid, 3.0 (Honeycomb), targeted at tablet devices.

Honeycomb contains new framework features that make it easier to create UIs that scale to large screen sizes. However, there are already tablets out there, like the Samsung Galaxy Tab, which run earlier versions of Android. Applications running on these earlier Android versions cannot take advantage of those new Honeycomb features (at least not yet). So, what effects does running on a large screen have on the UI of an Android application?

I’ve only done Galaxy Tab tests with the emulator, so my observations are somewhat limited, but still, here they are.

The Galaxy Tab reports itself as an HDPI device, even though its actual screen density is 170 dpi. This is to account for the larger display pixel count, and greater viewing distance.

Still, compared to a typical 4″ 480×800 hdpi phone screen, the Galaxy Tab can fit more on the screen, since it has more pixels: using a 72×72 icon as a measurement unit, a 480×800 screen can fit 6.6 accross and 11.11 down. The Galaxy Tab can fit 8.33 down and 14.22 across.

This makes images look smaller relative to the rest of the screen, even though they are larger in real world units, compared to a 4″ hdpi phone, and have same pixel size.

Screen metrics for a 4″ 480 by 800 screen

Pixels Inches Actual DPI Icon size, pixels Icon size, inches Icons per screen
480 2,06 233,01 72px 0,31″ 6,67
800 3,43 233,01 72px 0,31″ 11,11

Screen metrics for a 7″ 600 by 1024 screen

Pixels Inches Actual DPI Icon size, pixels Icon size, inches Icons per screen
600 3,54 169,49 72px 0,42″ 8,33
1024 6,04 169,49 72px 0,42″ 14,22

What this means is…

I don’t think you can expect an application running on the Tab (or another tablet) to look just like it looks on a phone, only larger. So far, this has been the case for phones of varying screen sizes and densities, but for a tablet device, this is no longer true.

And here are some screenshots of my application running on an HVGA device, on a WVGA800 device, and the Galaxy Tab, all resized to same height (320px):

HVGA - original size

The original 480 by 320 HVGA screen.

WVGA800 - resized

This is 800 by 480, resized to 533 by 320 (same height as the HVGA screenshot above).

Looks very, very much like the HVGA screenshot (although the text in spinners is not clipped, as they are a little larger).

Galaxy Tab - resized

This is 1024 by 600, resized to 546 by 320 (same height as the HVGA screenshot above). Doesn’t look like the screens above, does it?

The UI controls on the right still line up with list view on the left, but they are much smaller relative to the screen.

Measured in inches, the entire screen is larger, and the buttons (being the same size in pixels) are also larger than those on a 4″ WVGA800 device.

The Galaxy Tab can fit more information on the screen. The list could have about one and a half rows more.

Categories: android, ui
Follow

Get every new post delivered to your Inbox.

Join 97 other followers