February 26, 2013

Android tricks: flash bars

By

The artwork and underlying idea in this blog post is based on Romain Nurik‘s work. He is a great source to follow for insight into Android application design.

The new GMail app that comes with Android 4.0 is full of interesting UI tricks. If you have ever been on dodgy data connection, you probably have already encountered this one that I call the flash bar:

flash-bar

There is a little text block for it at the bottom of the Confirming & Acknowledging Android design page. This bar is used to warn of a non-fatal error in an unobstrusive manner and offer a quick way to retry the operation that failed. If the user doesn’t actually care about it, the bar will disappear after a small amount of time.

For these cases, it efficiently replaces a normal AlertDialog—which is much more costly in term of user interaction. As such, it’s an excellent pattern to handle things like the kind of simple network errors that happen often and are generally non-lethal.

As you can see in the screenshot, flash bars are very similar to normal Toast messages. The problem with toast messages is that they can never gain focus. Consequently, toast notifications can’t use any interactive UI elements (like buttons). That’s why the bar needs to be a custom XML layout snippet.

First of all, download the following zip containing the 9-patch images and the style definition for the flashbar. Put its content in your application’s Resources directory (merging the styles.xml file with what you already have).

Next, you will need to tune up a bit the XML layout of the Activity that will contain a flash bar. Essentially, it should look like this:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="match_parent"
	android:layout_height="match_parent">
	<LinearLayout
		android:orientation="vertical"
		android:layout_width="fill_parent"
		android:layout_height="fill_parent">
		<!-- Rest of your normal XML layout definition -->
	</LinearLayout>
	<LinearLayout
		android:id="@+id/flashbar"
		style="@style/FlashBar">
		<TextView
			android:id="@+id/flashbar_message"
			style="@style/FlashBarMessage"
			android:text="Some error string" />
		<Button
			android:id="@+id/flashbar_button"
			style="@style/FlashBarButton"
			android:text="Retry" />
	</LinearLayout>
</FrameLayout>

The overlay trick is achieved by using multiple children in a container FrameLayout that stacks them from bottom to top on the screen. If you have an existing layout where you want to add a flash bar, the first child LinearLayout can be the one that was enclosing your layout initially.

Now, the code manipulating that bar follows:

using System;
using System.Threading.Tasks;
using Android.Animation;
using Android.OS;
using Android.Text;
using Android.Views;
using Android.Widget;
public class FlashBarController : AnimatorListenerAdapter
{
	View barView;
	TextView messageView;
	ViewPropertyAnimator barAnimator;
	Handler hideHandler = new Handler();
	string message;
	Action hideRunnable;
	Action flashBarCallback;
	const int DefaultHideTime = 5000;
	public FlashBarController (View flashBarView)
	{
		barView = flashBarView;
		barAnimator = barView.Animate ();
		messageView = barView.FindViewById<TextView> (Resource.Id.flashbar_message);
		var flashBarBtn = barView.FindViewById<Button> (Resource.Id.flashbar_button);
		flashBarBtn.Click += delegate {
			HideBar (false);
			if (flashBarCallback != null)
				flashBarCallback ();
		};
		hideRunnable = () => HideBar(false);
		HideBar (true);
	}
	public void ShowBarUntil (Func<bool> flashBarCallback, bool immediate = false, string withMessage = null, int withMessageId = -1)
	{
		Action callback = () => {
			var t = Task.Factory.StartNew (flashBarCallback);
			t.ContinueWith (_ => {
				if (t.Exception != null || !t.Result)
					hideHandler.Post (() => ShowBarUntil (flashBarCallback, immediate, withMessage, withMessageId));
			});
		};
		ShowBar (callback, immediate, withMessage, withMessageId);
	}
	public void ShowBar (Action flashBarCallback, bool immediate = false, string withMessage = null, int withMessageId = -1)
	{
		if (withMessage != null) {
			this.message = withMessage;
			messageView.Text = message;
		}
		if (withMessageId != -1) {
			this.message = barView.Resources.GetString (withMessageId);
			messageView.Text = message;
		}
		this.flashBarCallback = flashBarCallback;
		hideHandler.RemoveCallbacks (hideRunnable);
		hideHandler.PostDelayed (hideRunnable, DefaultHideTime);
		barView.Visibility = ViewStates.Visible;
		if (immediate) {
			barView.Alpha = 1;
		} else {
			barAnimator.Cancel();
			barAnimator.Alpha (1);
			barAnimator.SetDuration (barView.Resources.GetInteger (Android.Resource.Integer.ConfigShortAnimTime));
			barAnimator.SetListener (null);
		}
	}
	public void HideBar (bool immediate = false)
	{
		hideHandler.RemoveCallbacks (hideRunnable);
		if (immediate) {
			barView.Visibility = ViewStates.Gone;
			barView.Alpha = 0;
		} else {
			barAnimator.Cancel();
			barAnimator.Alpha (0);
			barAnimator.SetDuration (barView.Resources.GetInteger (Android.Resource.Integer.ConfigShortAnimTime));
			barAnimator.SetListener (this);
		}
	}
	public override void OnAnimationEnd (Animator animation)
	{
		barView.Visibility = ViewStates.Gone;
		message = null;
	}
}

You generally instantiate this class after inflating your Activity layout by passing it the LinearLayout of the flash bar as a View, like so:

var view = inflater.Inflate (Resource.Layout.MyMainLayout, container, false);
// ...
var flashBarView = view.FindViewById (Resource.Id.flashbar);
flashBarCtrl = new FlashBarController (flashBarView);

Afterwards, you can use either ShowBar or ShowBarUntil to display the flash bar. Each of these methods accepts a boolean parameter to disable animation. You can set the text of the bar either by string id or directly by passing a string instance. The callback is the action to be called when the user tap the bar button.

Here are two examples using the above methods:

// ShowBarUntil
// If the callback returns false or throws an exception, the bar will be shown again
flashBarCtrl.ShowBarUntil (() => {
	DoNetworkCall ();
	DoSomeOtherStuff ();
	return true;
}, withMessageId: Resource.String.flashbar_tab_error);
// ShowBar
flashBarCtrl.ShowBar (() => RecursivelyCallTheExecutingMethod (withThe, sameParameters),
                      withMessage: "Problem while contacting server");
TwitterFacebookGoogle+LinkedInEmail