I generally don’t talk a lot about iOS on this blog. I’ll be honest with you, it is not because I consider iOS as an evil platform. As an extremely curious person, I just hate that iOS is a closed-source platform. I would really love to look at the implementation of some parts of the system or framework sometimes. However, Apple’s iOS remains an incredibly awesome mobile platform to develop for and to use. I assure you the APIs are gorgeous. From the UI point of view, iOS also has tons of exciting features, one of which is the “tap status bar to scroll to top”.

The purpose of this article is to give you a clear explanation about the control offered by Android over scroll containers. I have intentionally used iOS to do the comparison because the philosophy behind scroll containers in iOS is relatively different than Android.

Once upon a time, there was iOS

Just like every mobile OS, iOS runs on devices with a rather limited display surface. In order to display as much content as possible, applications can use scroll containers such as UIScrollView ( ScrollView -equivalent), UITableView ( ListView -equivalent), UIWebView ( WebView -equivalent), etc. These containers let you scroll the content using gestures now considered elementary: the swipe gestures. When looking at your content, you may want to be brought back to the top of that content. While this is not something obvious on Android, the feature is available via a consistent and nice gesture on iOS: you simply have to tap the status bar. When doing so, the system will basically look for a UIScrollView in your app’s view hierarchy and scroll it back to the top if allowed to (i.e. if the UIScrollView has the scrollToTop property set to YES and the delegate allows it)).

Some might criticize the lack of discoverability of this feature and I totally agree. This is clearly something that is not natural to the user. It will generally be discovered by mistake. Once spotted, this is a power-feature reserved to power-users. However, always keep in mind that it is important to satisfy these guys : they generally push feedbacks and review your application way more rapidly than “regular” users.

So what about Android? I guess you are not aware of such a gesture on your devices. The reason is pretty obvious: Android doesn’t offer such a system-wide/global gesture! I don’t know the exact reason of this “lack”. Google considering it as not required? Apple having patented the gesture? The only thing I’m sure of is implementing scroll-to-top is almost impossible on Android because scroll containers in the SDK are a complete mess.

The Android scroll issue

This is not a mystery to anyone, globally speaking, I do love Android. However, I’m also pragmatic enough to notice some parts of the platform are not well designed or badly implemented. The scrollable containers APIs belong in this category. You don’t need to be an API designer to notice they are extremely confusing regarding scroll-related capabilities.

By default, the framework provides basic support for View s that wish to internally scroll their content and draw scrollbars. For instance, you can scrollTo(int, int) ). While this works perfectly with ScrollView it doesn’t with ListView nor WebView nor my beloved MapView . Another example of this confusion is the ViewTreeObserver.OnScrollListener that works perfectly on all kinds of scrollable content but doesn’t provide you with the container that scrolled. Once again, Google Maps Android API v2 MapView is an exception and won’t fire the callback when being scrolled or zoomed. Finally, there are some inconsistencies. For instance, AbsListView.OnScrollListener lets you listen to AbsListView scrolls but there is no View.OnScrollListener counterpart. If you want to listen to scrolls at the View level, you’ll need to override the onScrollChanged(int, int, int, int) method.

Put simply, Android offers several scroll containers, but no consistent way to formalize scrolling and notify the developer. Even if you can determine if a View is a scroll container by using View.setScrollContainer(boolean) 1, there is absolutely no way to develop a unified algorithm that would scroll your container to its top with a single call to View.scrollTo(0, 0) .

On the other side, iOS simplified the problem by making sure all scrollable containers are unified via a UIScrollView - the base class containing the “scrolling” and “scroll-to-top” implementation. The framework offers a bunch of scrollable containers: UITextView , UITableView , UIWebView , MKMapView , etc. that all inherit or encapsulate a UIScrollView . By factorizing the scrolling behavior, iOS ensure that the scrolling physics (velocity, friction, bouncing, etc.) are consistent throughout iOS apps and guarantee all scrollable content can be scrolled back to the top.

So, is Android a crappy framework? Well I don’t think so. The API mess is probably difficult to apprehend - especially for new developers - but this is also what makes Android’s ListView so powerful compared to iOS UITableView when displaying items with variable heights for instance. UITableView relying on UIScrollView , it has to know all of the list’s items. On the other hand, Android’s ListView only requires the height of the visible items.

To sum up, iOS' UIScrollView -based API simplifies development and enforces UIs consistency. On the other hand Android’s messy API requires more attention from the developer, but can kick iOS' ass in some special cases.

It appears that not being able to implement a global scroll-to-top gesture is not really a problem. Indeed, most issues can be solved at the application level using components that are generally way more specific to the data displayed by your app. It obviously requires more work than relying on the default system’s behavior and doesn’t provide a consistent and coherent gesture throughout the platform. But, why would I need a scroll-to-top gesture in the Contacts iOS app if it also offers an index on the right?

Back to top on Android: the ethereal problem

Ultimately, the scroll content issue Android suffers from at the API level has no impact on the UI. If you are complaining about the feature missing, you should probably notify the developer his/her app needs some enhancements. The framework includes out-of-the-box workarounds and components that prevent the user from flinging for eternity trying to reach the top of your scrollable container:

Avoid long scrollable content at all cost : The best way to avoid scrolling pains is to avoid large scroll containers. In general, avoid long ListView s at all costs. Failing to do so will drown the important information in the middle of an almost un-findable/searchable list.

Enable fly-wheel : Since API 11, Android offers a fly-wheel mode in Scroller and OverScroller (the base components used to implement scrolling behaviors). When activated, successive fling motions will keep on increasing scroll speed. As a result, the user can rapidly increase the speed of the scroll containers to go back to an edge. Prior API 11, the velocity was generally topped by ViewConfiguration.getScaledMaximumFlingVelocity() .

Enable fast scroll whenever possible: AbsListView can be scrolled extremely rapidly with a call to setFastScrollEnabled(true) . Used in addition to SectionIndexer this makes navigation though an ordered list of grouped items extremely pleasant and powerful. While fast scroll can be used with all kind of data, it is generally only appropriate with ordered and grouped data. The Contacts app for instance uses it brilliantly.

Contrary to iOS, you generally don’t need to implement a tap-on-something-to-scroll-to-top behavior on Android. However, there is one case where the previously described techniques don’t fit: the timeline. Most of the time, a timeline is a vertically scrolling area displaying events sorted by creation date. The closer you are to the top, the more recent the data are.

The best - or should I say the worst - example of this is Google+. Google+ for Android displays a timeline with all of the posts from your circles' members. Reading posts is usually done from top to bottom which basically means from the most recent to the oldest ones. Sometimes you want to scroll way back to the top to see if there is a new post. That sounds easy, right? Well good luck with that :s. Here are the two options I found:

Start flinging like crazy back to top . Unfortunately, it looks like they completely disabled the fly-wheel mode which makes scrolling a pain in the ass.

Exit the timeline and reopens it I don’t think I need to describe this technique. You’ll all have understood it is purely non-logical and hence not user-friendly.

In this rare case I think, the tap-on-something-to-scroll-to-top is the correct option.

Tweaking the Quick Return pattern

Android not letting us listen to taps on the status bar, the only option is to use a clickable area in your application: a tab, a regular TextView , etc. A few months ago, Roman Nurik and Nick Butcher described and formalized a pattern they called “Quick Return”. I highly suggest you take a look at Roman’s G+ post or at Juhani Lehtimäki’s blog article to learn more about this emerging UI pattern.

While this pattern is great to make some important controls of your UI reappear, it doesn’t exactly fit the scroll-to-top gesture. Indeed, using the Quick Return pattern in this case would involve having a button appearing once the user starts scrolling up. This could be really annoying or frustrating.

In order to fix the issue, I’ve decided to tweak the pattern. Because users usually scroll up rapidly when going back to top, I thought it was only necessary to display the button when the velocity is higher than a given threshold. The rest of the article will focus on implementing such a widget but you can download an APK of the project (API 12 min) here:

Download sample APK

Note: The code given below is a proof of concept. I have never used it in production and I already know it may behave weirdly (crash ?) when the underlying Adapter ’s data is modified. Please make sure to understand what you are doing when using/modifying the snippet of code below.

Scrolling to the top

Going back to the top in a ListView is rather complicated. Here are some of the methods you can use:

setSelection(int) : This method works like a charm by selecting the given position. As a result setSelection(0) can bring us back to the top. Unfortunately it has two mains disadvantages: the transition is not animated at all which is visually jarring and modifying the selected position in the middle of a fling animation doesn’t stop the animation.

smoothScrollToPosition(int) : Available since API 8, this methods sounds like a good match. Unfortunately, I have never made it work in my projects. I’ve found a lot of complains about it on the web and stopped using it.

smoothScrollToPositionFromTop(int, int) : Available since API 11, this method is a low-level counterpart of the previous method. The only different is it seems to work. Put simply, Android does not offer per-pixel scrolling in ListView prior API 11.

As you may have noticed, scrolling a ListView to its top in an animated way is rather difficult. Fortunately, some people in the Android team already did the job of creating an extension of ListView : the AutoScrollListView . Available in the Contacts app, the AutoScrollListView can be asked to scroll (smoothly or otherwise) to a position.

Measuring the velocity of a ListView

ListView doesn’t provide a method to get its current velocity. As a consequence, the only thing we can do is computing it. Measuring the velocity of a ListView is rather difficult. Indeed, measuring a velocity is usually done using the simple formula: v = Δd/Δt. Getting Δt is pretty elementary but that’s not the case for Δd on Android.

Contrary to iOS’s UITableView , ListView doesn’t give you a “current scroll Y”. The “measure items on demand” strategy used by ListView makes it hard to scroll at the pixel level and to measure its physical property (such as the velocity). However, even if you can’t determine the exact velocity of a ListView , you can approximate the value using an approximation of the travelled distance. Here is the approach I created:

At each scroll step n, keep the values of the View top d n and the position p n of the underlying data in the Adapter of the ListView ’s child at index 0

If the item’s at position p n+1 is still visible then Δd is equal to the difference between the new top and the previous top: d n+1 - d n .

If the position is not visible anymore, then we can approximate the distance by computing the average height of the visible items in the ListView and multiply this value by the difference between the current position and the old position.

The schema shows a list being scrolled up (i.e. the user is swiping from top to bottom). As explained previously, Δd = d 2 - d 1 .

While the technique works great and scrolling up, you may easily fall into a case where d 2 is not measurable because the view at index 0 in the previous measurement has been recycled. The trick consists of using the exact same technique twice: once for the child at index 0 (mostly used when scrolling up) and also for the child at index getChildCount() - 1 (mostly used when scrolling down).

Finally, if you are scrolling up or down extremely rapidly you may have none of the children on screen from one step to another. In this case we will use the “position is not visible anymore” approximation. This case can also occur if your application freezes the UI thread.

The code is provided below and consists on extending AutoScrollListView to approximate the velocity of the ListView and notifying an optional client:

VelocityListView.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 package com . cyrilmottier . android . scrolltotop . widget ; import android.content.Context ; import android.util.AttributeSet ; import android.view.animation.AnimationUtils ; import android.widget.AbsListView ; import android.widget.ListView ; /** * An extension of the framework's {@link ListView} that can determine an * approximate value of its current velocity on the Y-axis. * * @author Cyril Mottier */ public class VelocityListView extends AutoScrollListView { /** * A callback to be notified the velocity has changed. * * @author Cyril Mottier */ public interface OnVelocityListViewListener { void onVelocityChanged ( int velocity ); } private static final long INVALID_TIME = - 1 ; /** * This value is really necessary to avoid weird velocity values. Indeed, in * fly-wheel mode, onScroll is called twice per-frame which results in * having a delta divided by a value close to zero. onScroll is usually * being called 60 times per seconds (i.e. every 16ms) so 10ms is a good * threshold. */ private static final long MINIMUM_TIME_DELTA = 10L ; private final ForwardingOnScrollListener mForwardingOnScrollListener = new ForwardingOnScrollListener (); private OnVelocityListViewListener mOnVelocityListViewListener ; private long mTime = INVALID_TIME ; private int mVelocity ; private int mFirstVisiblePosition ; private int mFirstVisibleViewTop ; private int mLastVisiblePosition ; private int mLastVisibleViewTop ; public VelocityListView ( Context context ) { super ( context ); init (); } public VelocityListView ( Context context , AttributeSet attrs ) { super ( context , attrs ); init (); } public VelocityListView ( Context context , AttributeSet attrs , int defStyle ) { super ( context , attrs , defStyle ); init (); } private void init () { super . setOnScrollListener ( mForwardingOnScrollListener ); mForwardingOnScrollListener . selfListener = mOnScrollListener ; } @Override public void setOnScrollListener ( OnScrollListener l ) { mForwardingOnScrollListener . clientListener = l ; } public void setOnVelocityListener ( OnVelocityListViewListener l ) { mOnVelocityListViewListener = l ; } /** * Return an approximative value of the ListView's current velocity on the * Y-axis. A negative value indicates the ListView is currently being * scrolled towards the bottom (i.e items are moving from bottom to top) * while a positive value indicates it is currently being scrolled towards * the top (i.e. items are moving from top to bottom). * * @return An approximative value of the ListView's velocity on the Y-axis */ public int getVelocity () { return mVelocity ; } private void setVelocity ( int velocity ) { if ( mVelocity != velocity ) { mVelocity = velocity ; if ( mOnVelocityListViewListener != null ) { mOnVelocityListViewListener . onVelocityChanged ( velocity ); } } } /** * @author Cyril Mottier */ private static class ForwardingOnScrollListener implements OnScrollListener { private OnScrollListener selfListener ; private OnScrollListener clientListener ; @Override public void onScroll ( AbsListView view , int firstVisibleItem , int visibleItemCount , int totalItemCount ) { if ( selfListener != null ) { selfListener . onScroll ( view , firstVisibleItem , visibleItemCount , totalItemCount ); } if ( clientListener != null ) { clientListener . onScroll ( view , firstVisibleItem , visibleItemCount , totalItemCount ); } } @Override public void onScrollStateChanged ( AbsListView view , int scrollState ) { if ( selfListener != null ) { selfListener . onScrollStateChanged ( view , scrollState ); } if ( clientListener != null ) { clientListener . onScrollStateChanged ( view , scrollState ); } } } private OnScrollListener mOnScrollListener = new OnScrollListener () { @Override public void onScrollStateChanged ( AbsListView view , int scrollState ) { switch ( scrollState ) { case SCROLL_STATE_IDLE: mTime = INVALID_TIME ; setVelocity ( 0 ); break ; default : break ; } } @Override public void onScroll ( AbsListView view , int firstVisiblePosition , int visibleItemCount , int totalItemCount ) { final long now = AnimationUtils . currentAnimationTimeMillis (); final int lastVisiblePosition = firstVisiblePosition + visibleItemCount - 1 ; if ( mTime != INVALID_TIME ) { final long delta = now - mTime ; if ( now - mTime > MINIMUM_TIME_DELTA ) { int distance = 0 ; //@formatter:off if ( mFirstVisiblePosition >= firstVisiblePosition && mFirstVisiblePosition <= lastVisiblePosition ) { distance = getChildAt ( mFirstVisiblePosition - firstVisiblePosition ). getTop () - mFirstVisibleViewTop ; } else if ( mLastVisiblePosition >= firstVisiblePosition && mLastVisiblePosition <= lastVisiblePosition ) { distance = getChildAt ( mLastVisiblePosition - firstVisiblePosition ). getTop () - mLastVisibleViewTop ; //@formatter:on } else { // We're in a case were the item we were previously // referencing has moved out of the visible window. // Let's compute an approximative distance int heightSum = 0 ; for ( int i = 0 ; i < visibleItemCount ; i ++) { heightSum += getChildAt ( i ). getHeight (); } distance = heightSum / visibleItemCount * ( mFirstVisiblePosition - firstVisiblePosition ); } setVelocity (( int ) ( 1000 d * distance / delta )); } } mFirstVisiblePosition = firstVisiblePosition ; mFirstVisibleViewTop = getChildAt ( 0 ). getTop (); mLastVisiblePosition = lastVisiblePosition ; mLastVisibleViewTop = getChildAt ( visibleItemCount - 1 ). getTop (); mTime = now ; } }; }

The final code

Now we can be notified of a change in the velocity of our ListView , so we can animate in a scroll-to-top button only when going beyond a certain threshold. First of all, let’s create the layout of our Activity :

main_activity.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?xml version="1.0" encoding="utf-8"?> <merge xmlns:android= "http://schemas.android.com/apk/res/android" xmlns:tools= "http://schemas.android.com/tools" android:layout_width= "match_parent" android:layout_height= "match_parent" > <com.cyrilmottier.android.scrolltotop.widget.VelocityListView android:id= "@android:id/list" android:layout_width= "match_parent" android:layout_height= "match_parent" /> <Button android:id= "@+id/btn_scroll_to_top" android:layout_width= "match_parent" android:layout_height= "32dp" android:layout_gravity= "top" android:background= "@drawable/list_selector" android:gravity= "center" android:text= "@string/tap_to_scroll_to_top" android:textColor= "@android:color/white" android:textSize= "12sp" android:textStyle= "bold" android:translationY= "-32dp" /> </merge>

The Activity ’s code is now crystal clear:

MainActivity.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 package com . cyrilmottier . android . scrolltotop ; import android.animation.Animator ; import android.animation.Animator.AnimatorListener ; import android.animation.AnimatorListenerAdapter ; import android.app.Activity ; import android.os.Bundle ; import android.view.View ; import android.view.View.OnClickListener ; import android.view.ViewGroup ; import android.view.ViewPropertyAnimator ; import android.widget.BaseAdapter ; import android.widget.Button ; import android.widget.TextView ; import com.cyrilmottier.android.scrolltotop.widget.VelocityListView ; import com.cyrilmottier.android.scrolltotop.widget.VelocityListView.OnVelocityListViewListener ; public class MainActivity extends Activity { private static final int VELOCITY_ABSOLUTE_THRESHOLD = 5500 ; private static final int BIT_VISIBILITY = 0x01 ; private static final int BIT_ANIMATION = 0x02 ; private static final int SCROLL_TO_TOP_HIDDEN = 0 ; private static final int SCROLL_TO_TOP_HIDING = BIT_ANIMATION ; private static final int SCROLL_TO_TOP_SHOWN = BIT_VISIBILITY ; private static final int SCROLL_TO_TOP_SHOWING = BIT_ANIMATION | BIT_VISIBILITY ; private VelocityListView mListView ; private Button mScrollToTopButton ; private ViewPropertyAnimator mAnimator ; private int mVelocityAbsoluteThreshold ; private int mScrollToTopState = SCROLL_TO_TOP_HIDDEN ; @Override public void onCreate ( Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ); mVelocityAbsoluteThreshold = ( int ) ( VELOCITY_ABSOLUTE_THRESHOLD * getResources (). getDisplayMetrics (). density + 0.5f ); setContentView ( R . layout . main_activity ); mScrollToTopButton = ( Button ) findViewById ( R . id . btn_scroll_to_top ); mScrollToTopButton . setOnClickListener ( mOnClickListener ); mAnimator = mScrollToTopButton . animate (); mListView = ( VelocityListView ) findViewById ( android . R . id . list ); mListView . setAdapter ( new CheesesAdapter ()); mListView . setOnVelocityListener ( mOnVelocityListener ); } private OnClickListener mOnClickListener = new OnClickListener () { @Override public void onClick ( View v ) { mListView . requestPositionToScreen ( 0 , true ); } }; private OnVelocityListViewListener mOnVelocityListener = new OnVelocityListViewListener () { @Override public void onVelocityChanged ( int velocity ) { if ( velocity > 0 ) { if ( Math . abs ( velocity ) > mVelocityAbsoluteThreshold ) { if (( mScrollToTopState && BIT_VISIBILITY ) == 0 ) { mAnimator . translationY ( 0 ). setListener ( mOnShownListener ); mScrollToTopState = SCROLL_TO_TOP_SHOWING ; } } } else { if (( mScrollToTopState && BIT_VISIBILITY ) == BIT_VISIBILITY ) { mAnimator . translationY (- mScrollToTopButton . getHeight ()). setListener ( mOnHiddenListener ); mScrollToTopState = SCROLL_TO_TOP_HIDING ; } } } }; private final AnimatorListener mOnHiddenListener = new AnimatorListenerAdapter () { public void onAnimationEnd ( Animator animation ) { mScrollToTopState = SCROLL_TO_TOP_HIDDEN ; }; }; private final AnimatorListener mOnShownListener = new AnimatorListenerAdapter () { public void onAnimationEnd ( Animator animation ) { mScrollToTopState = SCROLL_TO_TOP_SHOWN ; }; }; public class CheesesAdapter 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 ) { if ( convertView == null ) { convertView = getLayoutInflater (). inflate ( R . layout . text_item , parent , false ); } (( TextView ) convertView ). setText ( getItem ( position )); return convertView ; } } public static final String CHEESES [] = { "Abbaye de Belloc" , "Abbaye du Mont des Cats" , // ... "Zanetti Grana Padano" , "Zanetti Parmigiano Reggiano" }; }

As described previously, the code above should be considered as a proof of concept rather than a ready-to-use widget. Because of this I have decided not to push it on GitHub but share it “as it” here. Please note the license attached to it is the Apache v2.

Download source

Conclusion

Android’s scroll containers are probably more difficult to understand than their iOS counterparts, but they also offer a larger set of features. While scrolling to the top is extremely easy to implement on iOS, it requires more work from developers on Android. However, always keep in mind that implementing an iOS-like scroll-to-top gesture is not necessary 95% of the time. The other 5% can freely tweak or reuse the code I shared here.

Thanks to @franklinharper and @moystard for reading drafts of this