public class

LinearSmoothScroller

extends RecyclerView.SmoothScroller

 java.lang.Object

androidx.recyclerview.widget.RecyclerView.SmoothScroller

↳androidx.recyclerview.widget.LinearSmoothScroller

Gradle dependencies

compile group: 'androidx.recyclerview', name: 'recyclerview', version: '1.4.0-beta01'

  • groupId: androidx.recyclerview
  • artifactId: recyclerview
  • version: 1.4.0-beta01

Artifact androidx.recyclerview:recyclerview:1.4.0-beta01 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.recyclerview:recyclerview com.android.support:recyclerview-v7

Androidx class mapping:

androidx.recyclerview.widget.LinearSmoothScroller android.support.v7.widget.LinearSmoothScroller

Overview

RecyclerView.SmoothScroller implementation which uses a until the target position becomes a child of the RecyclerView and then uses a to slowly approach to target position.

If the RecyclerView.LayoutManager you are using does not implement the RecyclerView.SmoothScroller.ScrollVectorProvider interface, then you must override the RecyclerView.SmoothScroller.computeScrollVectorForPosition(int) method. All the LayoutManagers bundled with the support library implement this interface.

Summary

Fields
protected final DecelerateInterpolatormDecelerateInterpolator

protected intmInterimTargetDx

protected intmInterimTargetDy

protected final LinearInterpolatormLinearInterpolator

protected PointFmTargetVector

public static final intSNAP_TO_ANY

Decides if the child should be snapped from start or end, depending on where it currently is in relation to its parent.

public static final intSNAP_TO_END

Align child view's right or bottom with parent view's right or bottom

public static final intSNAP_TO_START

Align child view's left or top with parent view's left or top

Constructors
publicLinearSmoothScroller(Context context)

Methods
public intcalculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference)

Helper method for LinearSmoothScroller and LinearSmoothScroller

public intcalculateDxToMakeVisible(View view, int snapPreference)

Calculates the horizontal scroll amount necessary to make the given view fully visible inside the RecyclerView.

public intcalculateDyToMakeVisible(View view, int snapPreference)

Calculates the vertical scroll amount necessary to make the given view fully visible inside the RecyclerView.

protected floatcalculateSpeedPerPixel(DisplayMetrics displayMetrics)

Calculates the scroll speed.

protected intcalculateTimeForDeceleration(int dx)

Calculates the time for deceleration so that transition from LinearInterpolator to DecelerateInterpolator looks smooth.

protected intcalculateTimeForScrolling(int dx)

Calculates the time it should take to scroll the given distance (in pixels)

protected intgetHorizontalSnapPreference()

When scrolling towards a child view, this method defines whether we should align the left or the right edge of the child with the parent RecyclerView.

protected intgetVerticalSnapPreference()

When scrolling towards a child view, this method defines whether we should align the top or the bottom edge of the child with the parent RecyclerView.

protected voidonSeekTargetStep(int dx, int dy, RecyclerView.State state, RecyclerView.SmoothScroller.Action action)

protected voidonStart()

protected voidonStop()

protected voidonTargetFound(View targetView, RecyclerView.State state, RecyclerView.SmoothScroller.Action action)

protected voidupdateActionForInterimTarget(RecyclerView.SmoothScroller.Action action)

When the target scroll position is not a child of the RecyclerView, this method calculates a direction vector towards that child and triggers a smooth scroll.

from RecyclerView.SmoothScrollercomputeScrollVectorForPosition, findViewByPosition, getChildCount, getChildPosition, getLayoutManager, getTargetPosition, instantScrollToPosition, isPendingInitialRun, isRunning, normalize, onChildAttachedToWindow, setTargetPosition, stop
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final int SNAP_TO_START

Align child view's left or top with parent view's left or top

See also: LinearSmoothScroller.calculateDtToFit(int, int, int, int, int), LinearSmoothScroller, LinearSmoothScroller

public static final int SNAP_TO_END

Align child view's right or bottom with parent view's right or bottom

See also: LinearSmoothScroller.calculateDtToFit(int, int, int, int, int), LinearSmoothScroller, LinearSmoothScroller

public static final int SNAP_TO_ANY

Decides if the child should be snapped from start or end, depending on where it currently is in relation to its parent.

For instance, if the view is virtually on the left of RecyclerView, using SNAP_TO_ANY is the same as using SNAP_TO_START

See also: LinearSmoothScroller.calculateDtToFit(int, int, int, int, int), LinearSmoothScroller, LinearSmoothScroller

protected final LinearInterpolator mLinearInterpolator

protected final DecelerateInterpolator mDecelerateInterpolator

protected PointF mTargetVector

protected int mInterimTargetDx

protected int mInterimTargetDy

Constructors

public LinearSmoothScroller(Context context)

Methods

protected void onStart()

protected void onTargetFound(View targetView, RecyclerView.State state, RecyclerView.SmoothScroller.Action action)

protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, RecyclerView.SmoothScroller.Action action)

protected void onStop()

protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics)

Calculates the scroll speed.

By default, LinearSmoothScroller assumes this method always returns the same value and caches the result of calling it.

Parameters:

displayMetrics: DisplayMetrics to be used for real dimension calculations

Returns:

The time (in ms) it should take for each pixel. For instance, if returned value is 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.

protected int calculateTimeForDeceleration(int dx)

Calculates the time for deceleration so that transition from LinearInterpolator to DecelerateInterpolator looks smooth.

Parameters:

dx: Distance to scroll

Returns:

Time for DecelerateInterpolator to smoothly traverse the distance when transitioning from LinearInterpolation

protected int calculateTimeForScrolling(int dx)

Calculates the time it should take to scroll the given distance (in pixels)

Parameters:

dx: Distance in pixels that we want to scroll

Returns:

Time in milliseconds

See also: LinearSmoothScroller

protected int getHorizontalSnapPreference()

When scrolling towards a child view, this method defines whether we should align the left or the right edge of the child with the parent RecyclerView.

Returns:

SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector

See also: LinearSmoothScroller.SNAP_TO_START, LinearSmoothScroller.SNAP_TO_END, LinearSmoothScroller.SNAP_TO_ANY

protected int getVerticalSnapPreference()

When scrolling towards a child view, this method defines whether we should align the top or the bottom edge of the child with the parent RecyclerView.

Returns:

SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector

See also: LinearSmoothScroller.SNAP_TO_START, LinearSmoothScroller.SNAP_TO_END, LinearSmoothScroller.SNAP_TO_ANY

protected void updateActionForInterimTarget(RecyclerView.SmoothScroller.Action action)

When the target scroll position is not a child of the RecyclerView, this method calculates a direction vector towards that child and triggers a smooth scroll.

See also: RecyclerView.SmoothScroller.computeScrollVectorForPosition(int)

public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference)

Helper method for LinearSmoothScroller and LinearSmoothScroller

public int calculateDyToMakeVisible(View view, int snapPreference)

Calculates the vertical scroll amount necessary to make the given view fully visible inside the RecyclerView.

Parameters:

view: The view which we want to make fully visible
snapPreference: The edge which the view should snap to when entering the visible area. One of LinearSmoothScroller.SNAP_TO_START, LinearSmoothScroller.SNAP_TO_END or LinearSmoothScroller.SNAP_TO_ANY.

Returns:

The vertical scroll amount necessary to make the view visible with the given snap preference.

public int calculateDxToMakeVisible(View view, int snapPreference)

Calculates the horizontal scroll amount necessary to make the given view fully visible inside the RecyclerView.

Parameters:

view: The view which we want to make fully visible
snapPreference: The edge which the view should snap to when entering the visible area. One of LinearSmoothScroller.SNAP_TO_START, LinearSmoothScroller.SNAP_TO_END or LinearSmoothScroller.SNAP_TO_END

Returns:

The vertical scroll amount necessary to make the view visible with the given snap preference.

Source

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.recyclerview.widget;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PointF;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;

/**
 * {@link RecyclerView.SmoothScroller} implementation which uses a {@link LinearInterpolator} until
 * the target position becomes a child of the RecyclerView and then uses a
 * {@link DecelerateInterpolator} to slowly approach to target position.
 * <p>
 * If the {@link RecyclerView.LayoutManager} you are using does not implement the
 * {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface, then you must override the
 * {@link #computeScrollVectorForPosition(int)} method. All the LayoutManagers bundled with
 * the support library implement this interface.
 */
public class LinearSmoothScroller extends RecyclerView.SmoothScroller {

    private static final boolean DEBUG = false;

    private static final float MILLISECONDS_PER_INCH = 25f;

    private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;

    /**
     * Align child view's left or top with parent view's left or top
     *
     * @see #calculateDtToFit(int, int, int, int, int)
     * @see #calculateDxToMakeVisible(android.view.View, int)
     * @see #calculateDyToMakeVisible(android.view.View, int)
     */
    public static final int SNAP_TO_START = -1;

    /**
     * Align child view's right or bottom with parent view's right or bottom
     *
     * @see #calculateDtToFit(int, int, int, int, int)
     * @see #calculateDxToMakeVisible(android.view.View, int)
     * @see #calculateDyToMakeVisible(android.view.View, int)
     */
    public static final int SNAP_TO_END = 1;

    /**
     * <p>Decides if the child should be snapped from start or end, depending on where it
     * currently is in relation to its parent.</p>
     * <p>For instance, if the view is virtually on the left of RecyclerView, using
     * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
     *
     * @see #calculateDtToFit(int, int, int, int, int)
     * @see #calculateDxToMakeVisible(android.view.View, int)
     * @see #calculateDyToMakeVisible(android.view.View, int)
     */
    public static final int SNAP_TO_ANY = 0;

    // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
    // view is not laid out until interim target position is reached, we can detect the case before
    // scrolling slows down and reschedule another interim target scroll
    private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;

    protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();

    protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();

    @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
    protected PointF mTargetVector;

    private final DisplayMetrics mDisplayMetrics;
    private boolean mHasCalculatedMillisPerPixel = false;
    private float mMillisPerPixel;

    // Temporary variables to keep track of the interim scroll target. These values do not
    // point to a real item position, rather point to an estimated location pixels.
    protected int mInterimTargetDx = 0, mInterimTargetDy = 0;

    @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
    public LinearSmoothScroller(Context context) {
        mDisplayMetrics = context.getResources().getDisplayMetrics();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onStart() {

    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
    protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
        final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
        final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
        final int distance = (int) Math.sqrt(dx * dx + dy * dy);
        final int time = calculateTimeForDeceleration(distance);
        if (time > 0) {
            action.update(-dx, -dy, time, mDecelerateInterpolator);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
    protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
        // TODO(b/72745539): Is there ever a time when onSeekTargetStep should be called when
        // getChildCount returns 0?  Should this logic be extracted out of this method such that
        // this method is not called if getChildCount() returns 0?
        if (getChildCount() == 0) {
            stop();
            return;
        }
        //noinspection PointlessBooleanExpression
        if (DEBUG && mTargetVector != null
                && (mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0)) {
            throw new IllegalStateException("Scroll happened in the opposite direction"
                    + " of the target. Some calculations are wrong");
        }
        mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
        mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);

        if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
            updateActionForInterimTarget(action);
        } // everything is valid, keep going

    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onStop() {
        mInterimTargetDx = mInterimTargetDy = 0;
        mTargetVector = null;
    }

    /**
     * Calculates the scroll speed.
     *
     * <p>By default, LinearSmoothScroller assumes this method always returns the same value and
     * caches the result of calling it.
     *
     * @param displayMetrics DisplayMetrics to be used for real dimension calculations
     * @return The time (in ms) it should take for each pixel. For instance, if returned value is
     * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
     */
    @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
        return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
    }

    private float getSpeedPerPixel() {
        if (!mHasCalculatedMillisPerPixel) {
            mMillisPerPixel = calculateSpeedPerPixel(mDisplayMetrics);
            mHasCalculatedMillisPerPixel = true;
        }
        return mMillisPerPixel;
    }

    /**
     * <p>Calculates the time for deceleration so that transition from LinearInterpolator to
     * DecelerateInterpolator looks smooth.</p>
     *
     * @param dx Distance to scroll
     * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
     * from LinearInterpolation
     */
    protected int calculateTimeForDeceleration(int dx) {
        // we want to cover same area with the linear interpolator for the first 10% of the
        // interpolation. After that, deceleration will take control.
        // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
        // which gives 0.100028 when x = .3356
        // this is why we divide linear scrolling time with .3356
        return  (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
    }

    /**
     * Calculates the time it should take to scroll the given distance (in pixels)
     *
     * @param dx Distance in pixels that we want to scroll
     * @return Time in milliseconds
     * @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
     */
    protected int calculateTimeForScrolling(int dx) {
        // In a case where dx is very small, rounding may return 0 although dx > 0.
        // To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
        // time.
        return (int) Math.ceil(Math.abs(dx) * getSpeedPerPixel());
    }

    /**
     * When scrolling towards a child view, this method defines whether we should align the left
     * or the right edge of the child with the parent RecyclerView.
     *
     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
     * @see #SNAP_TO_START
     * @see #SNAP_TO_END
     * @see #SNAP_TO_ANY
     */
    protected int getHorizontalSnapPreference() {
        return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
                mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
    }

    /**
     * When scrolling towards a child view, this method defines whether we should align the top
     * or the bottom edge of the child with the parent RecyclerView.
     *
     * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
     * @see #SNAP_TO_START
     * @see #SNAP_TO_END
     * @see #SNAP_TO_ANY
     */
    protected int getVerticalSnapPreference() {
        return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
                mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
    }

    /**
     * When the target scroll position is not a child of the RecyclerView, this method calculates
     * a direction vector towards that child and triggers a smooth scroll.
     *
     * @see #computeScrollVectorForPosition(int)
     */
    @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
    protected void updateActionForInterimTarget(Action action) {
        // find an interim target position
        PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
        if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
            final int target = getTargetPosition();
            action.jumpTo(target);
            stop();
            return;
        }
        normalize(scrollVector);
        mTargetVector = scrollVector;

        mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
        mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
        final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
        // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
        // interim target. Since we track the distance travelled in onSeekTargetStep callback, it
        // won't actually scroll more than what we need.
        action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
    }

    private int clampApplyScroll(int tmpDt, int dt) {
        final int before = tmpDt;
        tmpDt -= dt;
        if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
            return 0;
        }
        return tmpDt;
    }

    /**
     * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
     * {@link #calculateDyToMakeVisible(android.view.View, int)}
     */
    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
            snapPreference) {
        switch (snapPreference) {
            case SNAP_TO_START:
                return boxStart - viewStart;
            case SNAP_TO_END:
                return boxEnd - viewEnd;
            case SNAP_TO_ANY:
                final int dtStart = boxStart - viewStart;
                if (dtStart > 0) {
                    return dtStart;
                }
                final int dtEnd = boxEnd - viewEnd;
                if (dtEnd < 0) {
                    return dtEnd;
                }
                break;
            default:
                throw new IllegalArgumentException("snap preference should be one of the"
                        + " constants defined in SmoothScroller, starting with SNAP_");
        }
        return 0;
    }

    /**
     * Calculates the vertical scroll amount necessary to make the given view fully visible
     * inside the RecyclerView.
     *
     * @param view           The view which we want to make fully visible
     * @param snapPreference The edge which the view should snap to when entering the visible
     *                       area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
     *                       {@link #SNAP_TO_ANY}.
     * @return The vertical scroll amount necessary to make the view visible with the given
     * snap preference.
     */
    @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
    public int calculateDyToMakeVisible(View view, int snapPreference) {
        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
        if (layoutManager == null || !layoutManager.canScrollVertically()) {
            return 0;
        }
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
        final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
        final int start = layoutManager.getPaddingTop();
        final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
        return calculateDtToFit(top, bottom, start, end, snapPreference);
    }

    /**
     * Calculates the horizontal scroll amount necessary to make the given view fully visible
     * inside the RecyclerView.
     *
     * @param view           The view which we want to make fully visible
     * @param snapPreference The edge which the view should snap to when entering the visible
     *                       area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
     *                       {@link #SNAP_TO_END}
     * @return The vertical scroll amount necessary to make the view visible with the given
     * snap preference.
     */
    @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
    public int calculateDxToMakeVisible(View view, int snapPreference) {
        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
        if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
            return 0;
        }
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
        final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
        final int start = layoutManager.getPaddingLeft();
        final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
        return calculateDtToFit(left, right, start, end, snapPreference);
    }
}