Android UX Tricks: Nested Scrolling without Nested Scrolling

Brendan
VSCO Stories
Published in
5 min readFeb 7, 2017

For a recent UX revamp of the VSCO Android app, our design team proposed having a camera sneak preview available in the part of the app where users edit photos. The design requires a container of scrollable content that you should be able to scroll up to cover the camera sneak preview, and then continue to scroll the contents of that list in one fluid motion. This component should also support continuous flings. This post documents the process by which we arrived at our solution for handling nested flinging and nested scrolling.

The first thought for how to address nested scrolling and flings was to look at a relevant sample app on CoordinatorLayout from Google I/O. To my chagrin, the CoordinatorLayout examples support nested scrolling, but do not support nested flings. Without nested flinging, the behavior of the layouts feels a bit unnatural.

The CoordinatorLayout examples also rely on Google support library views such as AppBarLayout. These views can have a high ROI if your design does not stray far from a sample android app, but their configuration can be unwieldy if you are trying to customize heavily.

It is often easier to look at the source of these layouts, find the code relevant to the desired behavior, and compose that code into your own custom View, rather than spending time wrestling with some combination of configuring those views and subclassing them to get just the right effect.

Even without nested fling handling, we wanted to see how the CoordinatorLayout examples were handling continuous scrolling between nested scroll containers. Browsing through the example source, two aptly named interfaces stand out: NestedScrollingParent and NestedScrollingChild.

As a refresher, if you think of the view hierarchy as a tree, the default behavior of touch events is to traverse the view hierarchy from the root to the leaf node that corresponds to the touch event’s coordinates. Each intermediary node has a chance to intercept or listen to the touch event along the way. To handle nested scrolling, we need to provide a way for the child to dispatch the scroll event back to a parent node, or a parent of a parent. The NestedScrollingParent and NestedScrollingChild interfaces define how to pass touch/scroll events from parent to child back to parent, without getting caught in an endless loop.

If you are working on a larger app and have not moved to Buck or Bazel yet, your build times might be long (1 minute clean build on average for us). You can prevent build times from slowing down your workflow, especially when testing UX tricks, by making a new app as a sandbox. To test out nested scrolling, we put together a simple app that would mirror the new studio design using blue and white tiles to mirror what would be the grid of images and a red rectangle to mirror the camera preview.

NestedScrollView from the support library implements both NestedScrollingParent and NestedScrollingChild, so if you want to familiarize yourself with how the interfaces interact with each other, you can simply subclass NestedScrollView and nest a NestedScrollView within itself, throw in some log statements for each callback, and observe. You can get a quick start by playing with this simple app we threw together. To get the outcome in the red-blue-white sample video, you only need to specify logic in two of the NestedScrollingParent methods:

At first glance, the interface methods look like they might help us implement nested flinging. Unfortunately, the methods giving us fling information are only called at the initiation of a fling. My inner high school physics student was eager to make use of VelocityTracker or trying to fork RecyclerView to derive fling velocity and acceleration at the moment a RecyclerView’s edge is hit, and then initiate a new fling on the parent view. Forking RecyclerView is probably not reasonable for a mid-sized company or smaller. The class is 8000+ lines long and will likely be iterated on in the future, and subclassing RecyclerView is difficult as a lot of the classes used within it are not accessible due to package scoping.

UX tricks usually have a simpler answer that will make you want to face-palm when you figure it out. After playing around with the NestedScrollingParent and NestedScrollingChild interfaces, we went scouring the web again to see if anyone else had tackled this problem with a novel approach. There are two open source libraries that offered demos showing off nested flinging:

The source code for these libraries was at first befuddling. In the layout files, there were not two scrollable ViewGroup’s, just one. And then a lightbulb went off. You can give the illusion of nested scrolling by putting a transparent item at the top of a RecyclerView, and placing the view that should be above the RecyclerView below that transparent item. And that’s exactly what these libraries do. In order to update the header view, though, you want to know about the scroll position of the RecyclerView. By default, RecyclerView does not give us a current scrollY or scrollX value. To get around that, these libraries attempt to compute scrollX and scrollY by keeping track of every onScroll event and keeping a counter of scroll dx and dy.

Position of the recyclerView is off due to inaccurate scrollY

We tried to use these libraries but ended up seeing incorrect values returned for computed scrollY and scrollX. Judging from our limited testing, and a lengthy backlog of unresolved GitHub issues, it appears their method of computing the scrollY of a Recycler View is not reliable.

We thought the general idea was a good one, if we could reliably track scrolling of the RecyclerView. And then, we had a thought. What if we just put in a 1px transparent item as the second item in the adapter, and exposed a method for returning its raw y position. For our purposes, this would give us enough tracking to do any necessary effects with a header above the RecyclerView.

In our live app, and the video above, we do just this. Since we want to support item transition animations, and a header that shows/hides based on the user’s scroll behavior, we place a View with a white background behind the RecyclerView. While the y position of the sentinel view within the RecyclerView is greater than zero, we update the camera preview y position; when it is less than zero, we do nothing and default to normal RecyclerView scrolling.

This post is based on a talk I gave at the Easy Bay Developer Meetup a few weeks ago. You can view the slides from the talk on SlideShare. If you’re interested in working on the Android team at VSCO, feel free to reach out to me brendanw@vsco.co. We are hiring.

The relevant portions of code are below:

--

--