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
andOverScroller
(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 byViewConfiguration.getScaledMaximumFlingVelocity()
.Enable fast scroll whenever possible:
AbsListView
can be scrolled extremely rapidly with a call tosetFastScrollEnabled(true)
. Used in addition toSectionIndexer
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:
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 resultsetSelection(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 inListView
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 dn and the position pn of the underlying data in theAdapter
of theListView
’s child at index 0If the item’s at position pn+1 is still visible then Δd is equal to the difference between the new top and the previous top: dn+1 - dn.
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 = d2 - d1.
While the technique works great and scrolling up, you may easily fall into a case where d2 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:
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 |
|
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
:
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 |
|
The Activity
’s code is now crystal clear:
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 |
|
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.
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
- This flag is currently used by Android to determine whether the window can resize or must pan when a soft IME is open.