Animate a scroll gesture

Try the Compose way
Jetpack Compose is the recommended UI toolkit for Android. Learn how to use touch and input in Compose.

In Android, scrolling is typically achieved by using the ScrollView class. Nest any standard layout that might extend beyond the bounds of its container in a ScrollView to provide a scrollable view managed by the framework. Implementing a custom scroller is only necessary for special scenarios. This document describes how to display a scrolling effect in response to touch gestures using scrollers.

Your app can use scrollers—Scroller or OverScroller—to collect the data needed to produce a scrolling animation in response to a touch event. They are similar, but OverScroller also includes methods for indicating to users when they reach the content edges after a pan or fling gesture.

  • Starting in Android 12 (API level 31), the visual elements stretch and bounce back on a drag event and fling and bounce back on a fling event.
  • On Android 11 (API level 30) and earlier, the boundaries display a "glow" effect after a drag or fling gesture to the edge.

The InteractiveChart sample in this document uses the EdgeEffect class to display these overscroll effects.

You can use a scroller to animate scrolling over time, using platform-standard scrolling physics such as friction, velocity, and other qualities. The scroller itself doesn't draw anything. Scrollers track scroll offsets for you over time, but they don't automatically apply those positions to your view. You must get and apply new coordinates at a rate that makes the scrolling animation look smooth.

Understand scrolling terminology

Scrolling is a word that can mean different things in Android, depending on the context.

Scrolling is the general process of moving the viewport—that is, the "window" of content you're looking at. When scrolling is in both the x- and y-axes, it's called panning. The InteractiveChart sample app in this document illustrates two different types of scrolling, dragging and flinging:

  • Dragging: this is the type of scrolling that occurs when a user drags their finger across the touchscreen. You can implement dragging by overriding onScroll() in GestureDetector.OnGestureListener. For more information about dragging, see Drag and scale.
  • Flinging: this is the type of scrolling that occurs when a user drags and lifts their finger quickly. After the user lifts their finger, you generally want to keep moving the viewport, but decelerate until the viewport stops moving. You can implement flinging by overriding onFling() in GestureDetector.OnGestureListener and using a scroller object.
  • Panning: scrolling simultaneously along both the x- and y-axes is called panning.

It's common to use scroller objects in conjunction with a fling gesture, but you can use them in any context where you want the UI to display scrolling in response to a touch event. For example, you can override onTouchEvent() to process touch events directly and produce a scrolling effect or a "snap-to-page" animation in response to those touch events.

Components that contain built-in scrolling implementations

The following Android components contain built-in support for scrolling and overscrolling behavior:

If your app needs to support scrolling and overscrolling inside a different component, complete the following steps:

  1. Create a custom touch-based scrolling implementation.
  2. To support devices that run Android 12 and later, implement the stretch overscroll effect.

Create a custom touch-based scrolling implementation

This section describes how to create your own scroller if your app uses a component that doesn't contain built-in support for scrolling and overscrolling.

The following snippet comes from the InteractiveChart sample. It uses a GestureDetector and overrides the GestureDetector.SimpleOnGestureListener method onFling(). It uses OverScroller to track the fling gesture. If the user reaches the content edges after they perform the fling gesture, the container indicates when the user reaches the end of the content. The indication depends on the version of Android that a device runs:

  • On Android 12 and later, the visual elements stretch and bounce back.
  • On Android 11 and earlier, the visual elements display a glow effect.

The first part of the following snippet shows the implementation of onFling():

Kotlin

// Viewport extremes. See currentViewport for a discussion of the viewport.
private val AXIS_X_MIN = -1f
private val AXIS_X_MAX = 1f
private val AXIS_Y_MIN = -1f
private val AXIS_Y_MAX = 1f

// The current viewport. This rectangle represents the visible chart
// domain and range. The viewport is the part of the app that the
// user manipulates via touch gestures.
private val currentViewport = RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX)

// The current destination rectangle—in pixel coordinates—into which
// the chart data must be drawn.
private lateinit var contentRect: Rect

private lateinit var scroller: OverScroller
private lateinit var scrollerStartViewport: RectF
...
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {

    override fun onDown(e: MotionEvent): Boolean {
        // Initiates the decay phase of any active edge effects.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects()
        }
        scrollerStartViewport.set(currentViewport)
        // Aborts any active scroll animations and invalidates.
        scroller.forceFinished(true)
        ViewCompat.postInvalidateOnAnimation(this@InteractiveLineGraphView)
        return true
    }
    ...
    override fun onFling(
            e1: MotionEvent,
            e2: MotionEvent,
            velocityX: Float,
            velocityY: Float
    ): Boolean {
        fling((-velocityX).toInt(), (-velocityY).toInt())
        return true
    }
}

private fun fling(velocityX: Int, velocityY: Int) {
    // Initiates the decay phase of any active edge effects.
    // On Android 12 and later, the edge effect (stretch) must
    // continue.
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects()
    }
    // Flings use math in pixels, as opposed to math based on the viewport.
    val surfaceSize: Point = computeScrollSurfaceSize()
    val (startX: Int, startY: Int) = scrollerStartViewport.run {
        set(currentViewport)
        (surfaceSize.x * (left - AXIS_X_MIN) / (AXIS_X_MAX - AXIS_X_MIN)).toInt() to
                (surfaceSize.y * (AXIS_Y_MAX - bottom) / (AXIS_Y_MAX - AXIS_Y_MIN)).toInt()
    }
    // Before flinging, stops the current animation.
    scroller.forceFinished(true)
    // Begins the animation.
    scroller.fling(
            // Current scroll position.
            startX,
            startY,
            velocityX,
            velocityY,
            /*
             * Minimum and maximum scroll positions. The minimum scroll
             * position is generally 0 and the maximum scroll position
             * is generally the content size less the screen size. So if the
             * content width is 1000 pixels and the screen width is 200
             * pixels, the maximum scroll offset is 800 pixels.
             */
            0, surfaceSize.x - contentRect.width(),
            0, surfaceSize.y - contentRect.height(),
            // The edges of the content. This comes into play when using
            // the EdgeEffect class to draw "glow" overlays.
            contentRect.width() / 2,
            contentRect.height() / 2
    )
    // Invalidates to trigger computeScroll().
    ViewCompat.postInvalidateOnAnimation(this)
}

Java

// Viewport extremes. See currentViewport for a discussion of the viewport.
private static final float AXIS_X_MIN = -1f;
private static final float AXIS_X_MAX = 1f;
private static final float AXIS_Y_MIN = -1f;
private static final float AXIS_Y_MAX = 1f;

// The current viewport. This rectangle represents the visible chart
// domain and range. The viewport is the part of the app that the
// user manipulates via touch gestures.
private RectF currentViewport =
  new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);

// The current destination rectangle—in pixel coordinates—into which
// the chart data must be drawn.
private final Rect contentRect = new Rect();

private final OverScroller scroller;
private final RectF scrollerStartViewport