Last week we announced the rollout of a completely redesigned Android Market client, and it’s time to dig a little deeper into some of the more interesting UI pieces. As i mentioned in my Google+ post, the team is committed to a level playing field, and the entire UI is done using only published platform APIs. To kick off this series, let’s look at the combined permissions / purchase page:
Let’s see what happens when the specific application requests a lot of permissions and they don’t fully fit on the screen. As you start scrolling the content, the action bar and the item snippet remain anchored while the rest of the content scrolls:
As the “Accept & buy” button gets to the bottom edge of the item snippet, it snaps to it and does not scroll any more:
Let’s take a look at the implementation. The main page is a vertical linear layout with three child views: action bar, item snippet and a scroll view. This scroll view has two children (see below for why this is not strictly correct): the “main” content and a button row:
You can see the bounds of these two views highlighted in yellow in the screenshots above. While the “Accept & buy” button appears to be part of the scrollable content, in fact it is its sibling. The scroll view content has a dummy child view between the pricing content and the tab strip. This view has the same height as the “Accept & buy” button (plus the few dips for the thick green separator) – creating the perfect illusion of unified content.
Keeping the layout of these two siblling views in sync is the real meat. The parts are:
- As ScrollView can have only one child, the two child views highlighted above are placed within one more container. Let’s call it PurchaseLayout.
- The view containing the button and the thick green separator (which snap together to the bottom edge of the item snippet) is placed as the second child of the PurchaseLayout. This ensures that it is painted on top of the main content and is not obscured during the scrolling.
- As ScrollView does not have an API to attach a scroll listener, a custom extension of ScrollView tracks calls to the onScrollChanged method and notifies a custom listener with the new top scroll position.
- As the user scrolls the content, the listener is notified. The listener implementation uses the current top scroll position to determine the vertical position of the “Accept & buy” button. As long as the purchasing part of the page is visible, the buy button is “anchored” to its bottom edge. Once the purchasing part is completely scrolled away, the buy button is “anchored” to the bottom edge of the item snippet. In this case it is effectively the current top scroll position as the buy button is inside the scroll view.
There are two points that deserve a special mention:
- A simpler option would be to have the buy button be a sibling of the scroll view. This has one big problem – as this snapping content (with dark background and thick green separator) is painted on top of the scroll view, it hides the relevant part of the scroll bar. It is usable, but quite ugly.
- When the content is scrolled and we’ve computed the new vertical position for the buy button, we need to re-layout the content of the scroll view to maintain the unified scrolling visuals. This is usually done with a call to View.requestLayout(). This method, however, schedules an asynchronous layout pass. While it is acceptable for one-off layouts, calling it during fast scrolls can cause very visible vertical tear-offs. We saw that multiple consecutive requestLayout() calls were coalesced – and the buy button was lagging behind the fast moving scrolling content. The solution is to use the synchronous View.offsetTopAndBottom() to ensure smoothly synchronized scrolling no matter how fast the content is scrolled.
With vertical space being a rare commodity on landscape orientation, this page behaves slightly different. Only the action bar stays anchored, with the item snippet “joining” the rest of the scrollable content. The “Accept & buy” button snaps to the bottom edge of the action bar – which did not require any code changes since the actual vertical position is the current top scroll position.
Stay tuned for more.