Android tips and tricks: carousel animations

August 15th, 2011

Continuing the series on the more interesting UI pieces of the new Android Market client, today i want to talk about the screenshot carousel and tighter control over animations. Here’s the genesis of the carousel in the previous version of the Market client:

This carousel allows the user to swipe left or right to quickly flip through the promoted applications. In addition, is no user action is taken within a predefined period of time (20 seconds), the carousel auto-advances to the next time – this hints at interactivity. The most interesting part of the promo carousel came from the requirement that no matter what the interaction is – auto-advance, user scroll or user fling – it should always end up with a promo image in the center, maintaining the visual connection to the information strip below the carousel (app name, rating and price).

The same approach was adopted and tweaked for the promo carousel in the tablet version of Market client that ships with Honeycomb tablets:

This carousel has the same underlying mechanics of auto-advance animations and snapping to a predefined position (horizontally centered), adding another moving piece – the “sidecar” with the application icon and price that moves in tandem with the fronted element.

The new version of Market client does not have a promo carousel. Instead, the landing page presents a grid of promo items from all available media verticals (apps, games, books, movies), and the main page for each vertical is a similar grid as well. However, the custom carousel still lives on in the details page – screenshots section:

While the auto-advance animations are disabled, the snap-to-edge behavior is there. If you scroll or fling the screenshots, the one closest to the left edge will “snap” to the left edge.

It’s time to talk about the implementation details:

  • A VelocityTracker is used to keep track of compound touch events in the overriden onTouchEvent(MotionEvent) method.
  • For the MotionEvent.ACTION_DOWN we store the x and y coordinates of the event.
  • For the MotionEvent.ACTION_MOVE we compute how much the finger “travelled”. The computation itself is pretty simple – take the deltas along both axes (using the coords of the current event and the last event), square and add them, and then add the square root to the variable that tracks the distance. When this distance exceeds the touch slop threshold – which you should obtain with ViewConfiguration.getScaledTouchSlop() API – we call ViewGroup.requestDisallowInterceptTouchEvent(true). This lets the parent (the entire vertically scrollable content) know that it should not handle any more move events. When you have a horizontally scrollable section in an otherwise vertically scrollable parent, you don’t want slightly diagonal swipes to be interpreted by both containers. Once the screenshots start scrolling, the vertical part of a diagonal swipe should be ignored – which is what the API call above effectively does. If you read the Javadocs of that method, you’ll notice that there is no need to call it again to reset the parent tracking state.

Before talking about the MotionEvent.ACTION_UP,  i want to pause and talk about math behind the scroll itself. A physically realistic scroll will have its velocity gradually declining until the scroll is complete. There are a number of ways to decrease the velocity, and the simplest is to maintain a constant deceleration factor, where the velocity is decremented by a constant amount on every animation “pulse”. If vo is the initial velocity, d is the deceleration amount and t is the time passed since the scroll began, then the velocity at time t is v0-d*t. From here, it’s simple to know how much time will it take for the scroll to end – v0/d. To know the distance covered at time t, take the integral – v0*t-d*t*t/2. Substituting the total scroll time into this formula will give you the total travel time given the initial velocity and the deceleration factor. The deceleration factor is computed based on device density, scroll friction and gravity – see the code in Scroller constructor for the specific details.

Now let’s take a look at the logic in MotionEvent.ACTION_UP:

  • VelocityTracker.computeCurrentVelocity is called to compute the gesture velocity along both axes.
  • As we’re only interested in horizontal movement, we then call VelocityTracker.getXVelocity(), followed by VelocityTracker.recycle() to clean all the state.
  • The absolute value of x velocity is compared to the system fling velocity threshold obtained from ViewConfiguration.getScaledMinimumFlingVelocity().
  • If our velocity is less than the threshold, it’s a scroll. This effectively means that we should scroll to the closest screenshot so that it snaps to the left edge. Once we determine the index of that screenshot (based on the current scroll offset), we know how much distance (on pixels) that scroll will take. Now that we have the total distance to scroll and the deceleration factor, we can compute the initial velocity that will give us a scrolling animation that ends snapping the matching screenshot to the left edge (see the formulas above).
  • If the velocity if more than the threshold, it’s a fling. Here is where it gets interesting. If we just start with this velocity, we will end up with misaligned screenshot edge. Instead, we tweak the initial velocity to ensure the edge-snap behavior. Suppose you’re currently viewing screenshot 4, and fling left so that the fling would end up at index 1.4 (somewhere between screenshots 1 and 2) if we were to use the velocity obtained from the tracker. Instead, we “extend” the fling so that it ends at index 1. In order to do this, we effectively increase the initial velocity, computing it in the manner very similar to the computation described in the previous bullet. Knowing how many pixels the fling needs to travel to end exactly at the left edge of screenshot 1, we work back to determine the tweaked initial velocity.

As described above, both scroll and fling are handled by determining the initial velocity required to create a scroll animation that snaps the target screenshot to the left edge of the screen. The animation itself is a linear interpolation of the time required to complete the scroll. At every “pulse”, we compute the scroll distance given how much time has passed since the beginning of the animation, and then lay out all the child image views. The linear interpolation of time results in quadratic interpolation of distance – an effectively realistic approximation of physical movement in the real world.

There’s one more case to handle – when the velocity is less than the system fling velocity threshold, it is either a scroll or a tap. The same variable that holds the total travelled distance since the last ACTION_DOWN is compared to the system touch slop obtained with ViewConfiguration.getScaledTouchSlop(). If we’re still within that margin, we treat the entire sequence of ACTION_DOWN+ACTION_MOVE*+ACTION_UP as a tap and show the full-screen view of the tapped screenshot.

The core AbsListView class is a more elaborate example of handling different touch events, modes and overscroll. This can serve as a complete source reference for handling custom animations that address specific requirements of your application.

Stay tuned for more.