June 17, 2013

Android: The Swipe-Down-to-Refresh Pattern


The pull-to-refresh pattern, popularized by applications like Twitter and finally integrated as a native component in iOS 6, has long been frowned upon in Android land. Because the feature is generally considered an iOS pattern, apps utilizing it on Android are sometimes regarded as “bad ports” of their iPhone counterparts.

Instead of supporting pull-to-refresh, Android applications typically include a refresh button: either in the main UI or (more recently) in an app’s Action Bar. The latest version of the Gmail app, however, has started using another interesting method—one that is closer to pull-to-refresh, but integrated with Android’s Action Bar:


When the ListView is positioned at the beginning, starting an overscroll will—in addition to displaying the normal edge effect—change the ActionBar to display an action message and a horizontally-centered progress bar defining when the movement results in a refresh. You can reproduce that user interface element in only a few steps.

Hijacking the Action Bar

By default, the Action bar API doesn’t allow you to supply a custom view. Thus, we have to somehow find a way to supply our own custom layout while interoperating with the default Action bar. For that purpose, we will exploit two things: first, the fact that Action bar’s style (appearance) is readily available through the normal Android style/theming API and, second, a special mode which allows the action bar to be overlaid on top of normal content (as in the Google Maps app).

Thanks to these two facilities, we can virtually recreate the ActionBar look and feel by hand in a custom view and then let the system action bar draw over it with a transparent background (so that the animations applied to it are not noticeable). When we hide the action bar, we can re-purpose our “fake” action bar layout to display any custom view we want.

Following XML is the layout that recreates the action bar background:

		android:text="Swipe down to refresh"
		android:textSize="18sp" />

And then in our main Activity OnCreate method, we can setup the Action bar like this:

protected override void OnCreate (Bundle bundle)
	base.OnCreate (bundle);
	RequestWindowFeature (WindowFeatures.ActionBarOverlay);
	ActionBar.SetBackgroundDrawable (new ColorDrawable (Color.Transparent));
	SetContentView (Resource.Layout.Main);

The advantage of having the ActionBar in this special overlay mode is also that it doesn’t impact the main content layout. Indeed, if the bar hadn’t been in that mode, each show/hide call would result in a full layout pass of the visual tree, which is not what you want.

A Tale of Progress Bars

When the overscroll movement is detected, we are supposed to show a horizontally-aligned progress bar to give feedback to the user. In the spirit of reusing what’s already in the framework, we want to go with the standard ProgressBar widget.

It turns out that using only one progress bar is a bit awkward. Having it expand on both sides means that we would have to re-layout/offset it all the time and monitor the progress value in order to be even in all cases.

We will use two ProgressBar controls instead and sync them on the same value. Obviously, we need to somehow find a way to flip one of them since it needs to fill up in the other direction.

Again, this is very easy to do in Android by using static transformations. In our case, we will use the android:scaleX attribute to flip the X coordinates so that the left ProgressBar appears reversed.

Following is the layout of that part:

	<!-- Main content view -->
		android:entries="@array/planets_array" />
	<!-- Overlayed progress bars-->
			android:scaleY="1.0" />
			android:layout_marginTop="-7dp" />

For exactly the same reason as with the Action bar, we use a FrameLayout to overlay the two progress bars on top of the main content so that making them appear and disappear doesn’t cause an unnecessary relayout.

Using normal ProgressBar controls introduces a slight visual defect—by default, they have a background color that makes them look like this:


It is a little bit too intrusive and harder to tweak with animations since, visually, they still occupy the same screen space. However, thanks to the fact that on Android progress bars are implemented with Drawable rather than custon drawing, we can easily work around that issue. More precisely, the default Holo styled progress bar uses a layer drawable which, as the name implies, is a special kind of drawable that layers other drawables on top of each other.

Here is the definition of this layer drawable in the framework for the default progress bar:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background"
          android:drawable="@android:drawable/progress_bg_holo_dark" />
    <item android:id="@android:id/secondaryProgress">
        <scale android:scaleWidth="100%"
               android:drawable="@android:drawable/progress_secondary_holo_dark" />
    <item android:id="@android:id/progress">
        <scale android:scaleWidth="100%"
               android:drawable="@android:drawable/progress_primary_holo_dark" />

The layer we want to hide is the one identified with android:id/background. It’s not that straightforward, however, since LayerDrawable objects don’t allow layers to be removed at runtime.

What we can do is swap the drawable referenced by one of the layers to something else. Thanks to that approach, we can hide the background by changing its original 9-patch drawable to be a transparent color drawable like so:

foreach (var p in new ProgressBar[] { bar1, bar2 }) {
	var layer = p.ProgressDrawable as LayerDrawable;
	if (layer != null)
		layer.SetDrawableByLayerId (Android.Resource.Id.Background,
		                            new ColorDrawable (Color.Transparent));

The Result

I haven’t covered some of the last points of the implementation like animations and event subscription, but those should be mostly straightfoward once you read the code.

Here is a video of the pattern implemented by us:

And you can grab the code from GitHub: