很久沒有寫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 newState The 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;
這三種回調順序:
- SCROLL_STATE_DRAGGING, 先是手指拖拽的狀態
- SCROLL_STATE_SETTLING,再是手指松開但是
RecyclerView
還在滑動 - SCROLL_STATE_IDLE, 最后是
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
進行滑動。