Last week I released Fragment for Android. Fragment is made up of all sorts of custom Views, which I think sets it apart from many apps in the Play Store.
Some of these views have a similar pattern to views I’ve had to create for other apps, in which a scroll view has padding such that every item within it can be scrolled to the center of the view. On it’s surface this doesn’t seem complex, but when you consider the massive difference in screen sizes available on Android, things get a little more complicated.
Let’s start by defining our end goal. The control we want to build allows the user to scroll a view such that any of the contents can be moved to the center of the screen. Here’s an animation demonstrating what I mean.
This control, boiled down, is really just a HorizontalScrollView. Notice how, when the user scrolls to the end, there is a nice overscroll indicator. When the user flings the control, it moves accordingly. The control nicely decelerates just like all other Android ScrollViews, making it feel natural to the user. These are all things you get for free by subclassing a build in Android widget.
This view does, however, pose two challenges. First, the ScrollView needs to be observable, so we can update our value whenever the user scrolls the view. Second, we need enough padding on each side of the view so that the contents will scroll such that the edges align with the center of the screen. Let’s tackle these one at a time below.
An Observable ScrollView
The first challenge, creating an observable ScrollView (Horizontal in our case), is actually quite simple. The orientation of the ScrollView isn’t important, so this will work with vertical or horizontal scroll views.
This is a surprisingly simple problem, and I’m always surprised when I’m reminded that this feature isn’t built into the default Android ScrollView class. ListView has a listener interface to be notified with the scroll position changes, so why not ScrollView. Let’s just assume that the AOSP developers figured it was too easy to be required.
While the built in ScrollView classes don’t have an OnScrollChangedListener interface, they do have a protected
onScrollChanged method, so we can easily just subclass the ScrollView of our choosing and create our own interface. Here’s what mine looks like.
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
As you can see, the only thing that I’ve added here is the
OnScrollChangedListener interface, and called it when the ScrollView’s position changes. This is a super simple subclass that adds immense value to the built in ScrollView classes.
Here’s an example of how you might use this.
1 2 3 4 5 6 7 8
This simple solution gets us exactly what we want, and doesn’t take all that much effort to implement. Aside from the class name, I think it even fits very nicely in with the other Android API classes.
Where did I come up with this elegant solution, you ask? While I’ve pointed out several times that this is a very simple solution, the idea for how to approach the problem actually came from a piece of open source code that I saw a while back. This is why it pays to be involved in open source, even if it’s just diving through the code of others.
The other challenge that we identified when making this view was that the scrolling contents needs to be able to be in the center of the screen. The challenge here is that we don’t know the width of the screen at compile time, and can’t even get the width of the view until it is measured, so we need a dynamic way to add spacing around the scrolling content.
Below I will outline two approaches to this problem, but both solve the problem in the same fashion. The basic idea is to add invisible spacers before and after the scrolling content, sized appropriately so that that actual contents are able to scroll their left edge to the center of the scroll view.
As you can see in the diagram above, the ScrollView doesn’t need to do anything differently in relation to it’s scrolling, it just appears to allow items to scroll to the center to the user.
One way we can achieve this is by using the
ViewTreeObserver’s OnPreDrawListener. This is a super handy callback which is called before our views are drawn but after they have been measured. This allows us to modify our views (add spacers) once we know the size of the view. Here’s an example:
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
This is a nice approach because it can be added onto any ScrollView in the code, without modifying your existing layouts (assuming the child of the ScrollView is a LinearLayout appropriately oriented). Throw this in a view helper and it can be as simple as
One downside of this approach is that we actually throw away a measure-layout-draw cycle. Remember that our OnPreDrawListener is called at the end of this cycle so that we know the width of our ScrollView. That means that the first time around, this is really a measure-layout-add-spacers-measure-layout-draw cycle. At the end of the day, since this only happens once, it’s not generally a big deal, but it is something to note.
But what if there was a way to add our spacers earlier in the measure-layout-draw cycle?
As it would happen, there is. Since we want to modify the layout of the items, with a custom compound view we can make our adjustments during the layout cycle, meaning no wasted time. Here’s how we might accomplish that.
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
In this example, you can see that our custom compound view is really the container of an ObservableHorizontalScrollView, and that it sets the spacer width whenever the layout changes, during the initial layout pass.
This approach also makes our view easily reusable via our XML layouts, like so:
1 2 3 4
You now have a few different ways to create a custom view that will delight your users. Which one you choose is entirely up to you, but both can be extended to work with ListViews, or really any scrolling view to which you would like some extra space.
These techniques can be used for other effects, as well. For instance, in addition to using this for custom value selectors, I’ve used the same approach for things like parallax effects.
There’s just another couple of tools for your toolbelt. If you’ve done something similar to this, or have another approach, share it in the comments.