LinearSmoothScrollerpublic abstract class LinearSmoothScroller extends RecyclerView.SmoothScroller {@link RecyclerView.SmoothScroller} implementation which uses
{@link android.view.animation.LinearInterpolator} until the target position becames a child of
the RecyclerView and then uses
{@link android.view.animation.DecelerateInterpolator} to slowly approach to target position. |
Fields Summary |
---|
private static final String | TAG | private static final boolean | DEBUG | private static final float | MILLISECONDS_PER_INCH | private static final int | TARGET_SEEK_SCROLL_DISTANCE_PX | public static final int | SNAP_TO_STARTAlign child view's left or top with parent view's left or top | public static final int | SNAP_TO_ENDAlign child view's right or bottom with parent view's right or bottom | 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
{@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START} | private static final float | TARGET_SEEK_EXTRA_SCROLL_RATIO | protected final android.view.animation.LinearInterpolator | mLinearInterpolator | protected final android.view.animation.DecelerateInterpolator | mDecelerateInterpolator | protected android.graphics.PointF | mTargetVector | private final float | MILLISECONDS_PER_PX | protected int | mInterimTargetDx | protected int | mInterimTargetDy |
Constructors Summary |
---|
public LinearSmoothScroller(android.content.Context context)
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
|
Methods Summary |
---|
public int | calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference)Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
{@link #calculateDyToMakeVisible(android.view.View, int)}
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;
| public int | calculateDxToMakeVisible(android.view.View view, int snapPreference)Calculates the horizontal scroll amount necessary to make the given view fully visible
inside the RecyclerView.
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (!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);
| public int | calculateDyToMakeVisible(android.view.View view, int snapPreference)Calculates the vertical scroll amount necessary to make the given view fully visible
inside the RecyclerView.
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (!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);
| protected float | calculateSpeedPerPixel(android.util.DisplayMetrics displayMetrics)Calculates the scroll speed.
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
| protected int | calculateTimeForDeceleration(int dx)Calculates the time for deceleration so that transition from LinearInterpolator to
DecelerateInterpolator looks smooth.
// 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);
| protected int | calculateTimeForScrolling(int dx)Calculates the time it should take to scroll the given distance (in pixels)
// 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) * MILLISECONDS_PER_PX);
| 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;
| public abstract android.graphics.PointF | computeScrollVectorForPosition(int targetPosition)
| 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.
return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
| 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.
return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
| protected void | onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action){@inheritDoc}
if (getChildCount() == 0) {
stop();
return;
}
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
| protected void | onStart(){@inheritDoc}
| protected void | onStop(){@inheritDoc}
mInterimTargetDx = mInterimTargetDy = 0;
mTargetVector = null;
| protected void | onTargetFound(android.view.View targetView, RecyclerView.State state, Action action){@inheritDoc}
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);
}
| protected void | updateActionForInterimTarget(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.
// find an interim target position
PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
Log.e(TAG, "To support smooth scrolling, you should override \n"
+ "LayoutManager#computeScrollVectorForPosition.\n"
+ "Falling back to instant scroll");
final int target = getTargetPosition();
stop();
instantScrollToPosition(target);
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);
|
|