Note: People who bought my book dealing with Android development will probably notice this article looks like a section of the fourth chapter “UI & UX: major components for a great user experience and the Android logic”. Let’s be honest, this blog post is pretty much an enhanced translation of a section of my book. For those who don’t know about this book, written in French, the title is “DĂ©veloppez pour Android”. Released about a year ago, it deals with Android development in general and particularly focuses on making highly optimized Android applications. If you are interested in this book, you can buy/download it here.

I have recently seen many Android applications starting with what I call a splash screen. A splash screen is an image that takes the entire screen space and usually shows branding information about the application (logo of the company, logo of the application, etc.). This screen is generally visible for a small amount of time while the application is starting or loading some resources in the background. I am pretty sure you have already seen some of the popular Android applications below featuring a splash screen on startup:

Once again, from my point of view, the splash screen paradigm is a port of the iOS equivalent. Some of you may start thinking I hate iOS because it influences Android in a really bad way. I often consider lots of mistakes in Android applications are due to stupid ports of iOS apps to Android. However I don’t consider iOS as a crappy mobile platform. I honestly believe iOS is an awesome mobile OS. Issues are not related to OSes themselves but to how people incorrectly understand them and use them. The splash screen is one of the best example of how people are misleading OSes in general. Being widely used on iOS, the UI pattern is also actively discouraged by Apple! Indeed, if you look closely to the iOS documentation you will notice Apple encourages developers to use launch images. Launch images are very different from splash screens as they are images used to pretend an application launched rapidly. On iOS, the system displays the launch image instantly when the user starts your application and until the app is fully ready to use. As soon as your app is ready for use, it displays its first screen, replacing the launch placeholder image. As a result, supplying a launch image is the best way to improve user experience. The graphic below shows you the launch image on the right that is used in the built-in “Stock” iOS apps:

The iOS documentation clearly specifies launch images should not be used as an opportunity to display branding information. Unfortunately, rules are made to be by-passed and iOS folks understood that. I honestly can’t blame them! They have simply used a functionality offered by the framework and slightly modified launch images to use them as splash screens. What is very disappointing to my mind is the same pattern is currently happening on Android. I am strongly convinced splash screens are not appropriate on Android for several reasons I would like to share with you:

  • A splash screen prevents the user from using the application. As far as I know, it is not explicitly written in Android guidelines but Android has been primarily designed to help the user achieving and completing tasks rather than offering features. In order to do so, the entire system focuses on what users want to do and letting them do it as quickly as possible. This fastness is achieved by rendering the overall UI in a insanely fast fashion and never blocking the user. Some of the best examples of this are the notification area (I guess iOS users will always remember the so annoying and blocking popups - UIAlertViews - when receiving a text message pre iOS5), the advanced multitasking mechanism, the launcher widgets, the fast transition animations (I don’t have the exact numbers but the default transition between two Activity is twice as fast as the transition between two UIViewControllers put in a UINavigationController), etc. In a nutshell, being blocked is not something an Android user is used to nor wants to be confronted with.
  • Most of the time, it is not necessary A splash screen can be used to make resources available before an application starts. Personally I think it is not necessary in 98% of the cases. It may be useful for applications actively relying on heavy resources such as Google Earth, Sky Map, or games but this is not applicable to simple utility applications such as feed readers, social network apps, news readers, etc. You should not require a network connection at startup nor do heavy computations. Always keep in mind, Activity launching is blazingly fast on Android. The Android team at Google spent a lot of time - and I am sure they are still spending a lot of time working on it - ensuring applications and the Activity class launches very rapidly. As a result it is quite easy to have an application and its first Activity up and ready in less than 400ms. Also always keep in mind that an application displaying a splash screen is also completely up and running from a system perspective.
  • Displaying a launch image or splash screen in not part of the framework1. I don’t know the exact percentage of the Android documentation I have read at least once but I assure you the number is pretty high! I have never seen a method, class or xml resource explicitly dealing with launch images or splash screens. This obviously proves the Android team never considered the launch image trick as necessary or easy to implement.
  • Adding once-viewed resources increase the size of your APKs. In order to make a great splash screen, one may usually need to add several resources to the APK (bitmaps, layouts, 9-patchs, etc.). This may uselessly and drastically increase the size of your APK making it more difficult to download/install. It may also drive several of your users mad if they have a pretty memory-constrained device. I think the best way to demonstrate the horrible consequences of launch images is to give the example of a universal iOS app (a universal app is an app running both on iPad and iPhone). As I previously explained, iOS fakes fast application startup rapidly displaying a launch image. If you look closely at the iOS documentation, you will notice the iPhone requires two default images (only in portrait, one for each density - 320×480 and 640×960) while the iPad requires 4 images (one for each orientation and density - 768×1004, 1536×2008, 1024×748 and 2048×1496). Knowing iOS requires launch images encoded using the PNG compression algorithm, you will likely create an application that is almost 5Mo-sized (depending on how much your launch images are compressed) and only displays a splash screen!
  • Implementing a great splash screen is very difficult and tedious Developing on Android requires dealing with a lot of resolutions, densities, orientations, etc. In general, the built-in resource switching mechanism is enough. In the context of splash screen development in which absolute layouts are usually required, it is generally not sufficient. Moreover, you also have to deal with Android’s Activity lifecycle. I have seen many application falsely redisplaying a splash screen in low memory conditions, because the developers forgot to save the splash-screen-has-been-displayed flag in onSaveInstance(Bundle). The worst bug I am used to see is an app auto-magically restarting itself after the user got out of it. This is mostly because splash screen are implemented using a Runnable posted with a certain delay in a given Handler. Not removing it from the Handler’s queue in onPause() will let the application restart itself. Developers should rather focus on making their app launch time as short as possible rather than designing a well-working splash screen. Equally, designers should spend more time on making Android apps Androidy…
  • Users don’t care about branding at launch time. When a user is starting an application, he/she is expecting it to fully fulfill what it is intended for. Making your applications responsive and fast is part of that job. Having a splash screen or a long startup time is the best option to do the complete opposite of what the user wants. If you really need to brand your application (and I encourage you to do so to create visually unique looking apps), I strongly suggest you to style the ActionBar, add an ‘About’ screen, use the background as a display area, etc. Doing in-app branding is the best opportunity to spread your message to your users as it will remain visible on all application’s screens. Believe me, as a user, when I open the Facebook app, I already know I am using the Facebook app. There is no need to display a huge Facebook logo in the middle of my screen.
  • A splash screen indicates a single-entry point application. Just a few application use this technique but Android applications may have several entry points. The best examples of that are Maps, Google+, Contacts/Phone or, more recently, Facebook. This can be done very easily using the appropriate <intent-filter />s on your entry point activities in the Android manifest. I’m not saying this is something that is easy to apprehend for a user but it’s available from a framework perspective. This feature is not supported by iOS. Because of this, lots of people falsely consider an app has a single entry point and use it to add a splashscreen.
  • In a multitasking context, launch images have no meaning. In an ideal multitasked environment (ideal means en environment with access to unlimited resources), a splash screen is diplayed only once: the first and only time an application is started. Indeed, once an application is started, there is no need to display the splash screen again as the application will always remain in memory. Android has been primarily designed as a massively multitasked OS and therefore doesn’t stick to the splash screen paradigm at all.

Conclusion

When Apple launched the first iOS SDK, it may have made a great decision by enabling developers to fake a fast application startup via a launch image. Unfortunately, iOS evolution - iOS now runs on several densities and devices and supports multitaking - and the fact that developers completely hacked the way launch images are used made it a big mistake. Spending time on ensuring applications are opened rapidly would have been a better option. On the other hand, Android has been built to deal with an insanely large amount of devices. Please start taking this into account and continue fighting against the iOS enthousiasts who think the iOS way is the only way. iOS and Android are both awesome mobile OSes. They both have some particularities that need to be taken into account when developing applications. Understanding an iOS app is appropriate on iOS, and an Android app is appropriate on Android, etc. is the path to making awesome multiplatform applications.

1: Android has actually always had a built-in mechanism similar to launch images. I won’t talk about it as it could be the topic of an entire article. Moreover, writing about it in an article severely criticizing the splash screen paradigm would be the best way to make this blog post pointless.

Some of you (or should I say some of my followers - @cyrilmottier) may have noticed I have been tweeting several tips dealing with Android development lately. I received a lot of great feedback about those tips and I would like to thank everybody for their support. Some people also asked me to make a blog post gathering together all of the Android dev tips I have tweeted; so here it is.. Starting from now, I will try to update this article as soon as I tweet a new tip. Have fun with these tips and feel free to use the comment system at the bottom of this page if you have any question.

Note : This article has been written prior to the recent article on Techcrunch which states “pull-to-refresh” has been patented by Twitter. I agree with most of this article when it comes to iOS. My only disagreement is I don’t think this patent is the reason why Apple didn’t use it in their applications …

As an iOS & Android developer/user I spend a lot of time looking at and playing with mobile applications. Since 2007 (the year mobile really had a leap forward thanks to the iPhone), mobile UI patterns have been introduced, enhanced and reused by applications. The Android platform has always been a very productive environment for UI designers and recently has become even more so. This rapid growth and evolution is probably a direct consequence of the increasing number of Android developers and applications. Recently, I have noticed some emerging UI patterns. Among them is the well known “pull-to-refresh” pattern.

As far as I know, “pull-to-refresh” has been primarily developed on iOS. It consists of adding an additional cell at the top of your ListView that remains hidden most of the time. As suggested by its name, you can pull down your ListView to make the widget appear. When you pull enough, a smooth animation indicates you can release the ListView in order to launch the refresh process. The cell contents switches to an activity indicator for the duration of the refresh. When the refreshing has been completed, the list scrolls in order to hide the additional cell. The screenshots below show you the complete interaction model. Please note I have used the official Twitter application. However, it doesn’t mean I appreciate this application. To be honest, I think it is in my top 10 of the least Androidy or should I say the most iOSy on Android … From my point of view, Twitter on Android is exactly the kind of app you shouldn’t do (grouped UITableView, UINavigationBar, UITabBar, UISegmentedControl, iOS look ‘n feel, etc.).

To be honest, I love this UI pattern on iOS. I think it is extremely “natural”: most recent items are usually on top of the list so it’s natural to scroll further to the top to access to the most recent items. It totally fits iOS platform philosophy. When it comes to Android, lots of developers and companies have a tendency to reproduce this pattern on the Android version of their application. I consider it a huge mistake and here is my point of view:

  • The ‘pull-to-refresh’ UI pattern is mainly dedicated to power users. I honestly don’t think it is as intuitive as a simple but clear “Refresh” button. Lots of developers use this UI pattern without thinking about their actual audience and how technophile they are. Most of the time a “Refresh” button would have been way easier to implement from a developer perspective and to use from a user point of view. I believe this is one of the main reasons Apple never used it in any of their stock iOS applications.
  • A lot of people primarily consider ‘pull-to-refresh’ as a way to save space on screen. I can’t disagree with this argument! It obviously saves space as nothing is visible on screen at all! Hiding the essential refresh button from normal users will probably just leave room for another useless button. On iOS, the framework forces the developer to have a single button on the right of the UINavigationBar (ActionBar equivalent on iOS)1. Saving as much space as possible on iOS may be a good practice, it is not a big issue on Android. On Android, the system will automatically adapt the ActionBar appearance to ensure the maximum amount of actions is visible. You often have enough room to add two or three actions (regardless of the device you are running on) which is enough to maintain the main features on screen. For instance, in the Twitter application, it would have been possible to have a “Refresh” button next to the “New tweet” button.
  • The pull-to-refresh pattern is not easily visible to the user. The Android UI philosophy is to make the UI as clean and sober as possible. Unfortunately developers have a tendency to misunderstand this UI rule. They often over-engineer the UI by putting buttons everywhere, making their applications look like a cockpit! The actual rule is to make the UI as clear as possible by prioritizing and ensuring it remains accessible. Keep in mind that a mobile application is not intended to give the same level of functionality as its desktop/web equivalent. A mobile app has to be concise and has to provide an extremely well designed small subset of all of the product’s features. On iOS, people may see “pull-to-refresh” quite easily when the screen is displayed or when they are rapidly flinging the UITableView to its top. In that case, the list will bounce when reaching the top edge, letting the user see the “pull-to-refresh” widget. As far as I know, there is no equivalent on Android as all implementations are only visible from the very top of the ListView.
  • You have to be at the top of the ListView to refresh it. This may be easy on iOS as a click on the status bar automatically scrolls all UIScrollViews on screen to their top. Unfortunately this is not a default behavior on Android. As a result, a user who has reached the bottom of a ListView will have to scroll all the way up to the first item and then ask for a ‘Refresh’. Pretty boring, isn’t it?
  • It is not compliant with how Android represents scrollable contents. To my mind, this is the biggest issue when using “pull-to-refresh” in Android apps. On iOS, most scrollable containers inherit from UIScrollView which nicely ‘bounces’ when at least one of the edges is reached. As a result, creating a UI widget on top of a UIScrollView will let you benefit from the amazingly great bouncing mechanic that Apple implemented. It improves consistency and enforces its adoption by the users. Because of this, enhancing the ‘edge bouncing’ property with a “pull-to-refresh” widget is pretty natural to iOS users. Android users are not familiar with bouncing scrollable containers2. I am not saying Google didn’t make a mistake when they released the first version of Android not including such a nice behavior. It’s probably because the early Android devices had screens with lots of afterglow: having a bouncing animation would have resulted in having a blurry screen while the ListView would be springing back to its final position. I’m not happy with this either but I consider we now have to deal with it and stop misleading the user by changing how scrollable containers scroll at every new release. Google recently introduced the edge effect (see EdgeEffect for more information) so we have to stick to it. The pull-to-refresh is clearly not compatible with the edge effect (see screenshot below).

  • Most of the time the ‘pull-to-refresh’ is not necessary: I have seen many applications using the ‘pull-to-refresh’ pattern for items having a low “update rate”. As a consequence, implementing the “pull-to-refresh” pattern in those cases is almost useless. There is a little chance the list will need to be updated while the user is looking at it. Android has several mechanisms and callbacks you may use to refresh the contents when appropriate. Always prefer considering a smart and automatic refreshing mechanism. There is nothing better than simplifying the UI by anticipating what the user wants and doing it for him. For instance, you can look for new contents at every onResume calls, every X minutes, etc. You may also use components such as Service to refresh your contents in the background at fixed intervals of time (that has no equivalent on iOS) or the Cloud 2 Device Messaging (C2DM) service to push the new contents only when strictly necessary.
  • When visible, the “pull-to-refresh” widget is pretty intrusive compared to a regular “Refresh” button. The best example of this is probably the latest GMail application. When tapping on the “Refresh” button, it turns into an indeterminate ProgressBar while refreshing. You can continue reading your contents while still knowing whether or not the refresh is in progress.

Conclusion

I know some of you totally disagree with what I said in this article. As a huge fan of the ‘pull-to-refresh’ UI pattern on iOS I can’t blame you ^^. I just wanted to show you that stupidly porting UI widgets from one platform to another is never a good practice. Please always consider platform differences when developing an application on different OSes. If you are like me, a developer using an Android device a daily basis and surrounded by iOS users (designer, boss, etc.) then you will have to fight everyday to explain that Android is different. It may sound weird to say that to people who “think different” but I actually do it everyday. It’s an ongoing fight. When I develop an application on several platforms I always ensure they all have the same set of functionalities but I always redesign the UI where necessary. That’s the price to pay to get an Android app on Android, an iOS app on iOS, etc. and have non-disoriented users.

1: Technically speaking you can add several buttons to a UINavigationBar. For instance you may add a button on the left but this area is often reserved by the “back” button. You may also add a control in the center of the UINavigationBar but you will lose the UINavigationBar’s title. If you really want to add several buttons on the right of your UINavigationBar, you will often end up with tricks such as using a custom view (a UISegmentedControl for instance) in your UIBarButtonItem.

2: I know some of you may mention some manufacturers implementations. Personally, I love how the UIScrollView (and of course its UITableView descendant) bounces on iOS but most of the implementations I have seen on Android so far are not great. They either don’t feel natural or are not consistent with the rest of the system. For instance, Samsung implemented it, and the movement is quite OK. But is not consistent with the rest of the framework: neither View (which has a basic scrolling support) nor ScrollView (even worse!) bounce.

EDIT [02/24/12]: Fix a potential NullPointerException in the removeDelegate(TouchDelegate) method of TouchDelegate

Some of you may wonder why I have decided to publish a new article in the ListView Tips & Tricks series. Indeed, in the last article, I mentioned the fourth post was the latest of the series. Actually, at the time of the writing it was true but I recently came up with a new topic! As a result, I will stop saying such and such an article is the final of the series and I will never be wrong again!

I also would like to give you a quick recap of the topics we have already covered (please note it is not necessary - but highly encouraged - to read the previous tips in order to fully understand the article below):

Moreover, all of those articles are based on some snippets of code that have been all gathered in a single Android application. You can download/clone the source code of this application on GitHub using the following link:

http://github.com/cyrilmottier/ListViewTipsAndTricks

Note: This article is mainly dedicated to ListViews. The main reason behind this is enlarging touchable areas if often necessary in itemviews containing lots of controls (Button, CheckBox, etc.). Please remember the trick explained in this article doesn’t only apply to ListViews. It can be used everywhere in the system as long as you use Views … which, I guess, is often the case when developing an application^^.

In the previous article of this ListView Tips & Tricks series, we have discovered several way to enlarge touchables areas. The main purpose of those techniques is to ensure the user correctly and easily access to secondary actions (star an item, select an item, etc.). Ensuring your users don’t get frustrated because of their actions are not being recognized is very important. Chances are high that an angry user will rate your application with a bad comment or worse uninstall the application.

A great example of a easy-to-interact-with application is the new GMail application. Personally, I love using it because you can navigate through the sections seamlessly and flawlessly. The UI is clean and responds precisely, which has not always been the case… Even if the select and star controls are pretty tiny (graphically speaking), they are easy to check/uncheck. To sum up, the general design remains clean and simple while the size of the controls does not influence the correctness of user interactions. The screenshot below shows an itemview from the new GMail application:

More than being a reference to me, the GMail application is also highly featured on the new Android Design website - which, by the way, I highly recommend you to read. For instance, the Android design team decided to use the GMail application to give an explanation of how to create contextual icons on Android. Go to the ‘Small / Contextual Icons’ of the Iconography section to read the recommendations. The actual problem of this guideline is it only describes iconography. Knowing a touchable area has to be at least as big as a 30×30dp square and Google requires a 16×16dp icon brings us to a big question: How to reconciliate designers with ergonomists ?

My previous ListView Tips & Tricks article gave us some advices:

  • Adding padding to controllable Views
  • Adding a transparent safe-frame to all images used in controls
  • Enlarging view bounds using fill_parent or manually setting a dimension

All these techniques are working perfectly and are pretty familiar to developers. Unfortunately they also bring you several issues as these methods don’t let you have a precise control over the touchable areas. For instance, the first technique involves modifying the general layout of your itemviews (padding has an effect on how Views are laid out) and may not let you entirely fill vertically the parent. The second technique makes reusing the image fairly difficult. There’s a good chance the safe-frame won’t be necessary if the image is reused somewhere else. In other words, those techniques can be pretty hazardous to use and may not be your best option.

Fortunately, Android gives you an amazing way to enlarge touchable areas. The principle consists on forwarding MotionEvents (an object describing a touch) from a View’s rectangle area to another View. This can be done natively creating a TouchDelegate and attaching it to a View, the touches of which needing to be forwarded to another View (the delegate View). This class gives you a finer control over how MotionEvents are consumed. You can obviously have a look at the TouchDelegate documentation on the Android developer website. As usual, I will follow the “show me the code” path instead of procrastinating. We will simply develop a tiny application emulating GMail behavior. It will display a list of cheeses that can be independantly (un)selected and/or (un)starred. The screenshot below gives you an overview of the result we are targeting. The red rectangles define touchable areas that will (un)select the itemview. Blue rectangles describes the area allowing the user to (un)star the itemview:

On the screenshot below you can easily notice (especially on the left of the itemview) the touchable area overlaps the actual TextView bounds.

A custom View as itemview

Most of the time, UIs are based on XML layouts. This type of declaration if usually enough to create a UI. However, sometimes you may require a finer control over the View hierarchy: be notified the size of a View has changed, be notified a layout pass has been performed (starting from API 11, this is now possible from outside of a View using the OnLayoutChangeListener), etc. This is the main reason why I have decided to develop my own custom View for the purpose of this sample. The XML View hierarchy of our custom itemview is given below:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
 
    <ImageButton
        android:id="@+id/btn_select"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:src="@drawable/btn_check_off_normal"
        android:background="@null" />
 
    <TextView
        android:id="@+id/content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_weight="1"
        android:ellipsize="end"
        android:paddingLeft="6dp"
        android:paddingRight="6dp"
        android:singleLine="true"
        android:textAppearance="?android:attr/textAppearanceLarge" />
 
    <ImageButton
        android:id="@+id/btn_star"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:src="@drawable/btn_star_off_normal"
        android:background="@null" />
 
</merge>

The Java counterpart of our itemview is where all the magic happens as this is where TouchDelegates are set. The trick consists on listening to onLayout(boolean, int, int, int, int) calls and re-apply the correct TouchDelegate if the size of the itemview has changed (we suppose child Views cannot change their position/size if the parent keeps the same size)

package com.cyrilmottier.android.listviewtipsandtricks.widget;
 
import java.util.ArrayList;
 
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
 
import com.cyrilmottier.android.listviewtipsandtricks.R;
import com.cyrilmottier.android.listviewtipsandtricks.view.TouchDelegateGroup;
 
public class LargeTouchableAreasView extends LinearLayout {
 
    private static final int TOUCH_ADDITION = 20;
    private static final int COLOR_SELECT_AREA = Color.argb(50, 255, 0, 0);
    private static final int COLOR_STAR_AREA = Color.argb(50, 0, 0, 255);
 
    public interface OnLargeTouchableAreasListener {
        void onSelected(LargeTouchableAreasView view, boolean selected);
 
        void onStarred(LargeTouchableAreasView view, boolean starred);
    }
 
    private static class TouchDelegateRecord {
        public Rect rect;
        public int color;
 
        public TouchDelegateRecord(Rect _rect, int _color) {
            rect = _rect;
            color = _color;
        }
    }
 
    private final ArrayList<TouchDelegateRecord> mTouchDelegateRecords = new ArrayList<LargeTouchableAreasView.TouchDelegateRecord>();
    private final Paint mPaint = new Paint();
 
    private ImageButton mSelectButton;
    private ImageButton mStarButton;
    private TextView mTextView;
 
    private TouchDelegateGroup mTouchDelegateGroup;
    private OnLargeTouchableAreasListener mOnLargeTouchableAreasListener;
 
    private int mTouchAddition;
 
    private boolean mIsStarred;
    private boolean mIsSelected;
 
    private int mPreviousWidth = -1;
    private int mPreviousHeight = -1;
 
    public LargeTouchableAreasView(Context context) {
        super(context);
        init(context);
    }
 
    public LargeTouchableAreasView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
 
    private void init(Context context) {
 
        setOrientation(LinearLayout.HORIZONTAL);
        setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
 
        mTouchDelegateGroup = new TouchDelegateGroup(this);
        mPaint.setStyle(Style.FILL);
 
        final float density = context.getResources().getDisplayMetrics().density;
        mTouchAddition = (int) (density * TOUCH_ADDITION + 0.5f);
 
        LayoutInflater.from(context).inflate(R.layout.large_touchable_areas_view, this);
    }
 
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
 
        mSelectButton = (ImageButton) findViewById(R.id.btn_select);
        mSelectButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                setItemViewSelected(!mIsSelected);
                if (mOnLargeTouchableAreasListener != null) {
                    mOnLargeTouchableAreasListener.onSelected(LargeTouchableAreasView.this, mIsSelected);
                }
            }
        });
 
        mStarButton = (ImageButton) findViewById(R.id.btn_star);
        mStarButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                setItemViewStarred(!mIsStarred);
                if (mOnLargeTouchableAreasListener != null) {
                    mOnLargeTouchableAreasListener.onStarred(LargeTouchableAreasView.this, mIsStarred);
                }
            }
        });
 
        mTextView = (TextView) findViewById(R.id.content);
    }
 
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
 
        final int width = r - l;
        final int height = b - t;
 
        if (width != mPreviousWidth || height != mPreviousHeight) {
 
            mPreviousWidth = width;
            mPreviousHeight = height;
 
            mTouchDelegateGroup.clearTouchDelegates();
 
            //@formatter:off
            addTouchDelegate(
                    new Rect(0, 0, mSelectButton.getWidth() + mTouchAddition, height),
                    COLOR_SELECT_AREA,
                    mSelectButton);
 
            addTouchDelegate(
                    new Rect(width - mStarButton.getWidth() - mTouchAddition, 0, width, height),
                    COLOR_STAR_AREA,
                    mStarButton);
            //@formatter:on
 
            setTouchDelegate(mTouchDelegateGroup);
        }
    }
 
    private void addTouchDelegate(Rect rect, int color, View delegateView) {
        mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(rect, delegateView));
        mTouchDelegateRecords.add(new TouchDelegateRecord(rect, color));
    }
 
    @Override
    protected void dispatchDraw(Canvas canvas) {
        for (TouchDelegateRecord record : mTouchDelegateRecords) {
            mPaint.setColor(record.color);
            canvas.drawRect(record.rect, mPaint);
        }
        super.dispatchDraw(canvas);
    }
 
    public void setOnLargeTouchableAreasListener(OnLargeTouchableAreasListener listener) {
        mOnLargeTouchableAreasListener = listener;
    }
 
    public TextView getTextView() {
        return mTextView;
    }
 
    public void setItemViewSelected(boolean selected) {
        if (mIsSelected != selected) {
            mIsSelected = selected;
            mSelectButton.setImageResource(mIsSelected ? R.drawable.btn_check_on_normal : R.drawable.btn_check_off_normal);
        }
    }
 
    public void setItemViewStarred(boolean starred) {
        if (mIsStarred != starred) {
            mIsStarred = starred;
            mStarButton.setImageResource(mIsStarred ? R.drawable.btn_star_on_normal : R.drawable.btn_star_off_normal);
        }
    }
}

As explained in the fourth article of this series, I consider the CheckBox widget as badly implemented. To overcome all problems, I simply decided not to use it! The entire code emulates CheckBoxes using ImageButtons. Please note I made nothing to manage extra states (pressed, focused, etc.) because it was not the main purpose of this article. In production code, you should always ensure the appearance of a control changes depending on its current state.

One TouchDelegate, two TouchDelegates, three…

A View manages a single TouchDelegate. It other words, it means, by default, you can’t have several ‘delegation areas’ for a single View. To overcome this problem, I have created a very basic class called TouchDelegateGroup that is basically a TouchDelegate containing several TouchDelegates and forward MotionEvent to the correct one. I have to confess the implementation is pretty hacky (at least the constructor) but this is the only way to overcome the fact TouchDelegate is not an interface, has no no-arg constructor and does not manage null arguments. The code of the TouchDelegateGroup is given below:

package com.cyrilmottier.android.listviewtipsandtricks.view;
 
import java.util.ArrayList;
 
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
 
public class TouchDelegateGroup extends TouchDelegate {
 
    private static final Rect USELESS_HACKY_RECT = new Rect();
 
    private ArrayList<TouchDelegate> mTouchDelegates;
    private TouchDelegate mCurrentTouchDelegate;
 
    public TouchDelegateGroup(View uselessHackyView) {
        // I know this is pretty hacky. Unfortunately there is no other way to
        // create a TouchDelegate containing TouchDelegates since TouchDelegate
        // is not an interface ...
        super(USELESS_HACKY_RECT, uselessHackyView);
    }
 
    public void addTouchDelegate(TouchDelegate touchDelegate) {
        if (mTouchDelegates == null) {
            mTouchDelegates = new ArrayList<TouchDelegate>();
        }
        mTouchDelegates.add(touchDelegate);
    }
 
    public void removeTouchDelegate(TouchDelegate touchDelegate) {
        if (mTouchDelegates != null) {
            mTouchDelegates.remove(touchDelegate);
            if (mTouchDelegates.isEmpty()) {
                mTouchDelegates = null;
            }
        }
    }
 
    public void clearTouchDelegates() {
        if (mTouchDelegates != null) {
            mTouchDelegates.clear();
        }
        mCurrentTouchDelegate = null;
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent event) {
 
        TouchDelegate delegate = null;
 
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mTouchDelegates != null) {
                    for (TouchDelegate touchDelegate : mTouchDelegates) {
                        if (touchDelegate != null) {
                            if (touchDelegate.onTouchEvent(event)) {
                                mCurrentTouchDelegate = touchDelegate;
                                return true;
                            }
                        }
                    }
                }
                break;
 
            case MotionEvent.ACTION_MOVE:
                delegate = mCurrentTouchDelegate;
                break;
 
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                delegate = mCurrentTouchDelegate;
                mCurrentTouchDelegate = null;
                break;
        }
 
        return delegate == null ? false : delegate.onTouchEvent(event);
    }
 
}

The most simple ListActivity ever

The code of the ListActivity is given below. As you may have noticed, it is really similar to the code we have written in the fourth article of this serie. Normally there is nothing new for you in here:

package com.cyrilmottier.android.listviewtipsandtricks;
 
import static com.cyrilmottier.android.listviewtipsandtricks.data.Cheeses.CHEESES;
import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
 
import com.cyrilmottier.android.listviewtipsandtricks.widget.LargeTouchableAreasView;
import com.cyrilmottier.android.listviewtipsandtricks.widget.LargeTouchableAreasView.OnLargeTouchableAreasListener;
 
public class LargeTouchableAreasListActivity extends ListActivity {
 
    private static final String STAR_STATES = "listviewtipsandtricks:star_states";
    private static final String SELECTION_STATES = "listviewtipsandtricks:selection_states";
 
    private boolean[] mStarStates;
    private boolean[] mSelectionStates;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        LargeTouchableAreasAdapter adapter = new LargeTouchableAreasAdapter();
 
        if (savedInstanceState != null) {
            mStarStates = savedInstanceState.getBooleanArray(STAR_STATES);
            mSelectionStates = savedInstanceState.getBooleanArray(SELECTION_STATES);
        } else {
            mStarStates = new boolean[adapter.getCount()];
            mSelectionStates = new boolean[adapter.getCount()];
        }
 
        setListAdapter(adapter);
    }
 
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBooleanArray(SELECTION_STATES, mSelectionStates);
        outState.putBooleanArray(STAR_STATES, mStarStates);
    }
 
    private class LargeTouchableAreasAdapter extends BaseAdapter {
 
        @Override
        public int getCount() {
            return CHEESES.length;
        }
 
        @Override
        public String getItem(int position) {
            return CHEESES[position];
        }
 
        @Override
        public long getItemId(int position) {
            return position;
        }
 
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
 
            final LargeTouchableAreasView view;
 
            if (convertView == null) {
                view = (LargeTouchableAreasView) getLayoutInflater().inflate(R.layout.large_touchable_areas_item, parent, false);
                view.setOnLargeTouchableAreasListener(mOnLargeTouchableAreasListener);
            } else {
                view = (LargeTouchableAreasView) convertView;
            }
 
            view.setItemViewStarred(mStarStates[position]);
            view.setItemViewSelected(mSelectionStates[position]);
            view.getTextView().setText(getItem(position));
 
            return view;
        }
    }
 
    private OnLargeTouchableAreasListener mOnLargeTouchableAreasListener = new OnLargeTouchableAreasListener() {
 
        @Override
        public void onSelected(LargeTouchableAreasView view, boolean selected) {
            final int position = getListView().getPositionForView(view);
            if (position != ListView.INVALID_POSITION) {
                mSelectionStates[position] = selected;
            }
        }
 
        @Override
        public void onStarred(LargeTouchableAreasView view, boolean starred) {
            final int position = getListView().getPositionForView(view);
            if (position != ListView.INVALID_POSITION) {
                mStarStates[position] = starred;
            }
        }
    };
}

The R.layout.large_touchable_area_item is detailed below:

<?xml version="1.0" encoding="utf-8"?>
<com.cyrilmottier.android.listviewtipsandtricks.widget.LargeTouchableAreasView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="6dp" />

Conclusion

As we have seen, MotionEvent delegation is fairly simple when using a TouchDelegate. The most difficult part is to determine when and how to set the bounds of the rectangular area that will forward MotionEvent to the delegate View. Always consider the work worth it. Having controls that the user can hardly interact with is the best way to frustrate your users. Only a few developers know about the TouchDelegate class and even less use it. And you? Did you know about the TouchDelegate class?

As a versatile mobile software engineer, I consider myself as a pretty lucky guy. I’m working both on iOS and Android platforms. Let’s be honest: I’m spending a lot of my professional and personal time working on Android and trying to spread everything I know about it via this blog, some conferences, some open-source projects, etc. But I’m also very close to the iOS ecosystem and some iOS developers. Talking to them everyday help me remember iOS and Android are not so different eventually. They both have to learn from each other.

Most multi-skilled (and with no preconception) mobile developers will assure you developing on Android is as great and easy as developing on iOS. Almost everything that is possible on iOS can be done on Android and vice-versa. Depending on the problem you are solving, it can be easy to make it work on Android or iOS. For instance, I consider developing adaptive and responsive layouts (I’m talking about adaptive layouts and not fixed/absolute layouts created with Interface Builder1.) in iOS is a pain in the ass compared to Android. This usually results in having 80% of the iPhone applications running only on portrait orientation and having fixed UITableViewCell heights. On the other hand, animating objects was (prior to Android 3.0 which introduced property-based animations) quite boring and creating smooth custom Views can be complex on Android because of various reasons (garbage collector, asynchronous measure/layout, UI toolkit sometimes slow compared to iOS, etc.)

I’m often talking to developers or reading some articles on the Internet and I noticed lots of people are complaining about the Android SDK. Most of the time they argue the SDK does not let them do as many things as iOS does. They complain about the fact it is impossible to implement things on Android, that the Android SDK is too poor, that the Android SDK is not permissive enough etc. When it comes to things I love, I can be fairly impulsive so I get angry. It drives me mad because I’m sick of looking at blind people. They are often too lazy or stupid to look at the documentation or source code (RTFM!). Most of the time, they did it on iOS but couldn’t find an equivalent to this technique on Android. I strongly believe their problem is they just don’t get it! They simply have to “think different”. This mantra was the Apple slogan a few years ago but it looks like some iOS developers are relatively new and don’t get the original Apple way of thinking.

You probably wonder why I have decided to write this article. The reason is pretty simple and relies on the new version of Path. Path is a Facebook-like social network. The company recently released a second version of their application. I will be frank with you: the iOS version is astonishing. It’s laggy but in term of UI/UX, I consider it as a reference. It contains a lot of brand new UI patterns that has been designed with pixel perfection. When looking at the iOS app, I decided to update my app on Android and I got mad ^^. Yes, as I said before, I am mercurial when it comes to things I consider important. I really don’t understand why Path decided to publish such a crappy app on Android. Unskilled developers? Few resources? iOS porters? Android aversion/detestation? An aggregate of all those factors? I really don’t know but I consider (and that’s just my own point of view) publishing nothing is better than releasing an application working correctly only on hdpi devices (handling screen densities is so easy thanks to the Android framework - not apprehending the resources switching system indicates a total ignorance of Android itself). Finally, the application is a complete port of the iOS version. They did not create a design that is dedicated to Android - you can have a look at the new Android Design website and especially the ‘Pure Android’ page. To conclude, apart from those two points, Path respected my motto “Do less but do it insanely great”. The application is very poor compared to the iOS version but the available features are working correctly.

In order to show you Android is as capable as iOS I decided to create the UI widget you can see in the Path timeline. Path decided to prioritize the information on screen by displaying the current timestamp only when needed. As a result instead of adding often useless timestamps everywhere, they attached a information panel to the scroll indicator (this is the name given to scrollbars in iOS). This is only visible when the UITableView is scrolling. Android users can see this as a fast scroller (like the one in the latest People app) but you can’t directly interact on it. The screenshot below gives you an example of the result:

The iOS implementation

In order to look at the implementation, I started looking for open source projects and some websites describing the technique. I finally found a blog post describing how to attach an info panel to a scroll indicator. The problem is this technique is a complete hack. To sum up, it consists on getting the latest subview of the UIScrollView (for your information, in iOS, UITableView - ListView equivalent - inherits UIScrollView - ScrollView equivalent). It appears this subview is currently an UIImageView used to draw the vertical scroll indicator. Thanks to it, you can determine the bounds of the scroll indicator or directly add a layer to it.

The problem relies on the fact Apple could change the order of the subviews or completely change how scroll indicators are drawn. They could and they have the right to as this is not part of the public API. Let’s imagine, the future release of iOS (iOS 6?) draws scroll indicators manually (with no use of an UIImageView but directly using a UIImage or CGImage), the application would be completely broken and the info panel would be attached to something else, something unknown. When you have millions of users out there, you can’t afford using hacks like this one. If your application is broken, it could take weeks to fix the issue (be notified the application is broken, find a way to bypass the problem, implement the solution, wait for the AppStore review process, etc.).

I don’t know how Path developed it but I hope they did not use this hack and found a greater implementation. Here are the possibilities I can think of:

  • They have developed their own UIScrollView: I strongly believe that is not the Path’s option. Designing a UIScrollView is extremely complex and would require a lot of time to get inertia-scrolling, edge bouncing working exactly as the framework-based UIScrollView.
  • They found a way to get a reference to the scroll indicator. I don’t know the documentation by heart but I’m pretty sure that’s impossible. Getting a scroll indicator is not part of the API in iOS. The only possible thing you can do is to change the style of a scroll indicator using a predefined enumeration of styles. You know nothing about the actual implementation.
  • They compute the position of the info panel on their own: UIScrollView has contentOffset, contentInset and contentSize properties. Using those properties in addition to the height of the UIScrollView (via bounds.size.height) gives you the ability to determine the position and size of the scroll indicators. I sincerely hope they used this technique and I invite people related to the Path iOS app to write a comment that states they did not hacked iOS at the end of this post.

Here we are: we know it is possible to implement such a UI widget in iOS. So, why have they chosen not to implement it on Android? I don’t know and I think I will never know so we will just suppose they think that is not possible on Android. Because of this they simply removed the scrollbars from the timeline! Actually the framework provides everything you need to do such a thing starting from Android 2.0 (API level 5) (the current minimum requirement for Path Android is 2.1, API level 7)

Implementing a scroll panel on Android

Seeing such an awful version of Path on my Android device, I started thinking about a way to implement an info panel attached to the scrollbars on Android. In the following lines we will simply get an overview of all methods that could be useful to implement such a behavior

Getting the current scrollbar position and size

Where iOS uses a UIImageView as scroll indicator, Android draws scrollbars using a Drawable (or, to be more specific, a ScrollBarDrawable that is not part of the public API). Getting the bounds (using getBounds()) of the Drawable would have let us know the current position of the scrollbar within the View. Unfortunately, the View API has no getScrollBarDrawable() method. This reminds me the iOS SDK …

Of course, we could have hacked something more horrible than the first iOS implementation we talked about. We could use introspection/reflection on the View class to get a ScrollBarDrawable instance but once again we fall into the public VS private debate and we are not these kind of guys!

The actual technique consists on using computeVerticalScroll[Extent|Offset|Range](). These methods are used by the system to determine the current position and size to apply to the vertical scrollbar. The only problem we have is those methods are protected and therefore can only be accessed from a class that inherits View. As a result we will have to extend View (or ListView in our case) to get those values.

Note : Remember the Java language authorizes methods visibility extension. In other words, it means a protected method can be made public by a child class. In the previous example we could made public all computeVerticalScroll[Extend|Offset|Range]() methods so they can be used from everywhere.

You may also need getVerticalScrollbarPosition() and getVerticalScrollbarWidth() to get the position of the vertical scroll bar within the screen as well as its position within the screen (left or right).

Dismissing the info panel at the appropriate moment

If you look closely to the scrollbars, you will notice they appear instantly once the user starts scrolling. After a delay without user interaction, they fade away. This behavior is a direct consequence of a call to awakenScrollBars (int startDelay, boolean invalidate). Listening to this method will let you know when to display the scrollbar as well as the delay after which the scrollbars must fade away. Using a very simple Handler you can schedule a Runnable that will perform the dismissing animation on your info panel after the given startDelay. This method has been introduced in API level 5. This is the reason why this overall technique requires at least Android 2.0.

Getting the scrollbar fade duration

A consistent way of dismissing the info panel is to use an animation which duration is equal to the one used when View is fading the scrollbars away. You can get this duration thanks to the ViewConfiguration class. This class contains a lot of constants and scaled dimensions that may be used when developing custom View. To get the scrollbar fade duration you can use the getScrollBarFadeDuration() static method. This method is available starting from API level 5 which is not a problem as this is our minimum API requirement.

Knowing the ListView is scrolling

There are several ways to do it. The easiest way is to consider a call to awakenScrollBars as an indication the ListView has been scrolled. It’s okay but I prefer attaching a dedicated OnScrollListener to the ListView using the setOnScrollListener(OnScrollListener) method. This listener will have an onScroll(AbsListView, int, int, int) method that will be called every time the ListView scrolls. The only problem with ListView is you can’t attach several listeners at the same time. If you need to attach several OnScrollListeners (for instance one from your custom ListView and another one in your Activity’s code), a great option is to implement an observer pattern or a forward mechanism.

Conclusion

We now have all the data needed to create our scrolling info panel. The implementation uses no hack at all as all methods we are using are part of the public API. As a result, we are sure the code will run perfectly on future versions of the platform. I have created a demonstration application which screencast can be seen below (I’m sorry for the crappy screencast, but I don’t have the appropriate equipment to do this):

Some of you may ask “Why aren’t you sharing the code?”. Actually the current code I implemented is working like a charm but I don’t want to share it right now. I’m always taking care of the snippets of code I’m sharing and I don’t consider this as proper enough to be shared, especially to developers that may reuse it without thinking about the way it’s working ^^. Moreover, the current implementation only works for Android 3.0 because I was to lazy to animate Views on my own. I preferred using the new animation APIs. Just note, it would be relatively easy to do it starting from Android 2.0 by manually animating the Views.

In a nutshell, do always consider everything as ‘implementable’ on Android. I haven’t either talked about the Path ’swipable’ sidemenu (really similar to the one Facebook recently added to their application), but it’s also possible to implement it on Android (I consider the Facebook implementation as very disappointing). I can’t say more about this now but I will keep you informed soon … I assure you Android is a really great and capable OS. You just have to think differently, address issues differently from what you used to. Be open and open-minded! I assure you Android has almost no limit. Stop doing crappy work2, be a perfectionist user and developer, develop greatly, live your apps, create things people will admire, applications people will remember.

1: Interface Builder is the name of the WYSIWYG editor you can use to create UI on iOS. It layouts UI widgets in an absolute manner.
2: An approximative translation for an expression of mine : “ArrĂȘter de faire du petit boulot !”. It describes something that should not have been done, something unreasonable in term of quality, something that is not great enough to be a sold/shared as a product.