RecyclerView之Scroll和Fling
很久沒有寫Android控制元件了,正好最近專案有個自定義控制元件的需求,是基於RecylerView
整理了下,本文先看下Scroll
和Fling
的聯絡和區別
本文分析的原始碼是基於recyclerview-v7-26.1.0
1.Scroll
和狀態
Scroll
大家都知道,我們可以給RecyclerView
新增監聽,在RecyclerView
滾動的時候會回撥。
RecyclerView.addOnScrollListener(mScrollListener);
就是在RecyclerView
滾動的時候會回撥
/** * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event * has occurred on that RecyclerView. * <p> * @see RecyclerView#addOnScrollListener(OnScrollListener) * @see RecyclerView#clearOnChildAttachStateChangeListeners() * */ public abstract static class OnScrollListener { /** * Callback method to be invoked when RecyclerView's scroll state changes. * * @param recyclerView The RecyclerView whose scroll state has changed. * @param newStateThe updated scroll state. One of {@link #SCROLL_STATE_IDLE}, *{@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}. */ public void onScrollStateChanged(RecyclerView recyclerView, int newState){} /** * Callback method to be invoked when the RecyclerView has been scrolled. This will be * called after the scroll has completed. * <p> * This callback will also be called if visible item range changes after a layout * calculation. In that case, dx and dy will be 0. * * @param recyclerView The RecyclerView which scrolled. * @param dx The amount of horizontal scroll. * @param dy The amount of vertical scroll. */ public void onScrolled(RecyclerView recyclerView, int dx, int dy){} }
滾動有三種狀態:
/** * The RecyclerView is not currently scrolling. * @see #getScrollState() */ public static final int SCROLL_STATE_IDLE = 0; /** * The RecyclerView is currently being dragged by outside input such as user touch input. * @see #getScrollState() */ public static final int SCROLL_STATE_DRAGGING = 1; /** * The RecyclerView is currently animating to a final position while not under * outside control. * @see #getScrollState() */ public static final int SCROLL_STATE_SETTLING = 2;
這三種回撥順序:
RecyclerView RecyclerView
2.Fling
和Scroll
的關係
Fling
是怎樣一種狀態,手指在螢幕上滑動RecyclerView然後鬆手,RecyclerView中的內容會順著慣性繼續往手指滑動的方向繼續滾動直到停止,這個過程叫做Fling
。Fling
操作從手指離開螢幕瞬間被觸發,在滾動停止時結束。其實RecyclerView
在Fling
過程中會把state設定為SCROLL_STATE_SETTLING
。看下原始碼:
在Fling觸發的時候會回撥SnapHelper
中的onFling
@Override public boolean onFling(int velocityX, int velocityY) { LayoutManager layoutManager = mRecyclerView.getLayoutManager(); if (layoutManager == null) { return false; } RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); if (adapter == null) { return false; } int minFlingVelocity = mRecyclerView.getMinFlingVelocity(); return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && snapFromFling(layoutManager, velocityX, velocityY); } private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) { ... SmoothScroller smoothScroller = createScroller(layoutManager); if (smoothScroller == null) { return false; } int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY); if (targetPosition == RecyclerView.NO_POSITION) { return false; } smoothScroller.setTargetPosition(targetPosition); layoutManager.startSmoothScroll(smoothScroller); return true; }
在onFling
中呼叫snapFromFling
,findTargetSnapPosition
會根據滑動速率計算出要滑動到的位置,然後設定給smoothScroller
:
public void setTargetPosition(int targetPosition) { mTargetPosition = targetPosition; } /** * Returns the adapter position of the target item * * @return Adapter position of the target item or * {@link RecyclerView#NO_POSITION} if no target view is set. */ public int getTargetPosition() { return mTargetPosition; }
在snapHelper
中smoothScroller
預設是LinearSmoothScroller
,
public class LinearSmoothScroller extends RecyclerView.SmoothScroller
然後會呼叫smoothScroller
滾動到指定位置, 開始滾動之前會呼叫start
方法, 會設定mTargetView
就是我們上面設定的要滾到的位置:
/** * Starts a smooth scroll for the given target position. * <p>In each animation step, {@link RecyclerView} will check * for the target view and call either * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until * SmoothScroller is stopped.</p> * * <p>Note that if RecyclerView finds the target view, it will automatically stop the * SmoothScroller. This <b>does not</b> mean that scroll will stop, it only means it will * stop calling SmoothScroller in each animation step.</p> */ void start(RecyclerView recyclerView, LayoutManager layoutManager) { mRecyclerView = recyclerView; mLayoutManager = layoutManager; if (mTargetPosition == RecyclerView.NO_POSITION) { throw new IllegalArgumentException("Invalid target position"); } mRecyclerView.mState.mTargetPosition = mTargetPosition; mRunning = true; mPendingInitialRun = true; mTargetView = findViewByPosition(getTargetPosition()); onStart(); mRecyclerView.mViewFlinger.postOnAnimation(); }
在RecyclerView.SmoothScroller
滑動是會呼叫onAnimation
,在滾動過程中如果發現getChildPosition(mTargetView) == mTargetPosition
,也就是目標位置已經layout出來了,那麼就會回撥onTargetFound
.
public abstract static class SmoothScroller { private void onAnimation(int dx, int dy) { final RecyclerView recyclerView = mRecyclerView; if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) { stop(); } mPendingInitialRun = false; if (mTargetView != null) { // verify target position if (getChildPosition(mTargetView) == mTargetPosition) { onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction); mRecyclingAction.runIfNecessary(recyclerView); stop(); } else { Log.e(TAG, "Passed over target position while smooth scrolling."); mTargetView = null; } } ... } } public int getChildPosition(View view) { return mRecyclerView.getChildLayoutPosition(view); }
沒有懸念,繞了一大圈終於回到SnapHelper
中onTargetFound
,在onTargetFound
中會呼叫calculateDistanceToFinalSnap
:
return new LinearSmoothScroller(mRecyclerView.getContext()) { @Override protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView); final int dx = snapDistances[0]; final int dy = snapDistances[1]; final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); if (time > 0) { action.update(dx, dy, time, mDecelerateInterpolator); } } @Override protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; } };
計算到目標位置的座標然後更新給RecyclerView.Action
,然後在上面的onAnimation
會呼叫mRecyclingAction.runIfNecessary(recyclerView);
,然後會呼叫smoothScrollBy
:
void runIfNecessary(RecyclerView recyclerView) { if (mJumpToPosition >= 0) { final int position = mJumpToPosition; mJumpToPosition = NO_POSITION; recyclerView.jumpToPositionForSmoothScroller(position); mChanged = false; return; } if (mChanged) { validate(); if (mInterpolator == null) { if (mDuration == UNDEFINED_DURATION) { recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy); } else { recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration); } } else { recyclerView.mViewFlinger.smoothScrollBy( mDx, mDy, mDuration, mInterpolator); } mConsecutiveUpdates++; if (mConsecutiveUpdates > 10) { // A new action is being set in every animation step. This looks like a bad // implementation. Inform developer. Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure" + " you are not changing it unless necessary"); } mChanged = false; } else { mConsecutiveUpdates = 0; } }
在smoothScrollBy
中就會設定狀態SCROLL_STATE_SETTLING
:
public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) { if (mInterpolator != interpolator) { mInterpolator = interpolator; mScroller = new OverScroller(getContext(), interpolator); } setScrollState(SCROLL_STATE_SETTLING); mLastFlingX = mLastFlingY = 0; mScroller.startScroll(0, 0, dx, dy, duration); if (Build.VERSION.SDK_INT < 23) { // b/64931938 before API 23, startScroll() does not reset getCurX()/getCurY() // to start values, which causes fillRemainingScrollValues() put in obsolete values // for LayoutManager.onLayoutChildren(). mScroller.computeScrollOffset(); } postOnAnimation(); }
上面繞了一圈,總結一下呼叫棧就是:
SnapHelper onFling ---> snapFromFling
上面得到最終位置targetPosition
,把位置給RecyclerView.SmoothScroller
, 然後就開始滑動了:
RecyclerView.SmoothScroller start --> onAnimation
在滑動過程中如果targetPosition
對應的targetView
已經layout出來了,就會回撥SnapHelper
,然後計算得到到當前位置到targetView
的距離dx,dy
SnapHelper onTargetFound ---> calculateDistanceToFinalSnap
然後把距離dx,dy
更新給RecyclerView.Action
:
RecyclerView.Action update --> runIfNecessary --> recyclerView.mViewFlinger.smoothScrollBy
最後呼叫RecyclerView.ViewFlinger
, 然後又回到onAnimation
class ViewFlinger implements Runnable public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) { if (mInterpolator != interpolator) { mInterpolator = interpolator; mScroller = new OverScroller(getContext(), interpolator); } setScrollState(SCROLL_STATE_SETTLING); mLastFlingX = mLastFlingY = 0; mScroller.startScroll(0, 0, dx, dy, duration); postOnAnimation(); }
3.觸發fling
操作
接著我們再看看Fling操作在RecyclerView
中是什麼時候觸發的。
先看下RecyclerView
的onTouch
方法:
@Override public boolean onTouchEvent(MotionEvent e) { case MotionEvent.ACTION_UP: { mVelocityTracker.addMovement(vtev); eventAddedToVelocityTracker = true; mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0; final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0; if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } resetTouch(); } break; }
在up觸發的時候會呼叫native方法去獲得X和Y方向的速率:
/** * Compute the current velocity based on the points that have been * collected.Only call this when you actually want to retrieve velocity * information, as it is relatively expensive.You can then retrieve * the velocity with {@link #getXVelocity()} and * {@link #getYVelocity()}. * * @param units The units you would like the velocity in.A value of 1 * provides pixels per millisecond, 1000 provides pixels per second, etc. * @param maxVelocity The maximum velocity that can be computed by this method. * This value must be declared in the same unit as the units parameter. This value * must be positive. */ public void computeCurrentVelocity(int units, float maxVelocity) { nativeComputeCurrentVelocity(mPtr, units, maxVelocity); }
如果有其中一個不為0就會呼叫fling
操作:
public boolean fling(int velocityX, int velocityY) { if (mLayout == null) { Log.e(TAG, "Cannot fling without a LayoutManager set. " + "Call setLayoutManager with a non-null argument."); return false; } if (mLayoutFrozen) { return false; } final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); final boolean canScrollVertical = mLayout.canScrollVertically(); if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) { velocityX = 0; } if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) { velocityY = 0; } if (velocityX == 0 && velocityY == 0) { // If we don't have any velocity, return false return false; } if (!dispatchNestedPreFling(velocityX, velocityY)) { final boolean canScroll = canScrollHorizontal || canScrollVertical; dispatchNestedFling(velocityX, velocityY, canScroll); if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) { return true; } if (canScroll) { int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontal) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertical) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH); velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); mViewFlinger.fling(velocityX, velocityY); return true; } } return false; }
這裡有個mMinFlingVelocity
欄位,如果x和y方向的速率小於這個欄位,就不會觸發fling操作,直接return false,然後就會把RecyclerView
的狀態設定為SCROLL_STATE_IDLE
。mMinFlingVelocity
在RecyclerView
構造的時候會從ViewConfiguration
中獲取
final ViewConfiguration vc = ViewConfiguration.get(context); mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
在ViewConfiguration
建構函式中:
/** * Minimum velocity to initiate a fling, as measured in dips per second */ private static final int MINIMUM_FLING_VELOCITY = 50; /** * Maximum velocity to initiate a fling, as measured in dips per second */ private static final int MAXIMUM_FLING_VELOCITY = 8000; public ViewConfiguration() { mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY; mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; }
所以如果滑動速率小於50的話就不會觸發fling操作。
從上面分析知道,fling操作是在手指離開的時候觸發然後直到滑動停止這中間的一段操作。在滑動過程中其實state狀態是SCROLL_STATE_SETTLING
,然後通過RecyclerView.ViewFlinger
進行滑動。