ListView Tips & Tricks #5: Enlarged Touchable Areas
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:
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 30x30dp square and Google requires a 16x16dp 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:
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)
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:
packagecom.cyrilmottier.android.listviewtipsandtricks.view;importjava.util.ArrayList;importandroid.graphics.Rect;importandroid.view.MotionEvent;importandroid.view.TouchDelegate;importandroid.view.View;publicclassTouchDelegateGroupextendsTouchDelegate{privatestaticfinalRectUSELESS_HACKY_RECT=newRect();privateArrayList<touchdelegate>mTouchDelegates;privateTouchDelegatemCurrentTouchDelegate;publicTouchDelegateGroup(ViewuselessHackyView){// 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);}publicvoidaddTouchDelegate(TouchDelegatetouchDelegate){if(mTouchDelegates==null){mTouchDelegates=newArrayList<touchdelegate>();}mTouchDelegates.add(touchDelegate);}publicvoidremoveTouchDelegate(TouchDelegatetouchDelegate){if(mTouchDelegates!=null){mTouchDelegates.remove(touchDelegate);if(mTouchDelegates.isEmpty()){mTouchDelegates=null;}}}publicvoidclearTouchDelegates(){if(mTouchDelegates!=null){mTouchDelegates.clear();}mCurrentTouchDelegate=null;}@OverridepublicbooleanonTouchEvent(MotionEventevent){TouchDelegatedelegate=null;switch(event.getAction()){caseMotionEvent.ACTION_DOWN:if(mTouchDelegates!=null){for(TouchDelegatetouchDelegate:mTouchDelegates){if(touchDelegate!=null){if(touchDelegate.onTouchEvent(event)){mCurrentTouchDelegate=touchDelegate;returntrue;}}}}break;caseMotionEvent.ACTION_MOVE:delegate=mCurrentTouchDelegate;break;caseMotionEvent.ACTION_CANCEL:caseMotionEvent.ACTION_UP:delegate=mCurrentTouchDelegate;mCurrentTouchDelegate=null;break;}returndelegate==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:
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?