??RecyclerView
作為一個(gè)列表View
,天生就可以滑動(dòng)萌丈。作為一個(gè)使用者克懊,我們可以不去了解它是怎么進(jìn)行滑動(dòng),但是我們作為一個(gè)學(xué)習(xí)源碼的人蒿讥,必須得知道RecyclerView
的滑動(dòng)機(jī)制挥等,所以友绝,我們今天來看看RecyclerView
滑動(dòng)部分的代碼。
??本文參考資料:
??同時(shí)肝劲,從RecyclerView
的類結(jié)構(gòu)上來看迁客,我們知道RecyclerView
實(shí)現(xiàn)了NestedScrollingChild
接口,所以RecyclerView
也是一個(gè)可以產(chǎn)生滑動(dòng)事件的View
辞槐。我相信大家都有用過CoordinatorLayout
和RecyclerView
這個(gè)組合掷漱,這其中原理的也是嵌套滑動(dòng)。本文在介紹普通滑動(dòng)中催蝗,可能會(huì)涉及到嵌套滑動(dòng)的知識(shí)切威,所以在閱讀本文時(shí)育特,需要大家掌握嵌套滑動(dòng)的機(jī)制丙号,具體可以參考我上面的文章:Android 源碼分析 - 嵌套滑動(dòng)機(jī)制的實(shí)現(xiàn)原理,此文專門從RecyclerView
的角度上來理解嵌套滑動(dòng)的機(jī)制缰冤。
??本文打算從如下幾個(gè)方面來分析RecyclerView
:
- 正常的
TouchEvent
- 嵌套滑動(dòng)(穿插著文章各個(gè)地方犬缨,不會(huì)專門的講解)
- 多指滑動(dòng)
- fling滑動(dòng)
1. 傳統(tǒng)事件
??現(xiàn)在,我們正式分析源碼棉浸,首先我們來看看onTouchEvent
方法怀薛,來看看它為我們做了那些事情:
@Override
public boolean onTouchEvent(MotionEvent e) {
// ······
if (dispatchOnItemTouch(e)) {
cancelTouch();
return true;
}
// ······
switch (action) {
case MotionEvent.ACTION_DOWN: {
// ······
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
// ······
} break;
case MotionEvent.ACTION_MOVE: {
// ······
} break;
case MotionEvent.ACTION_POINTER_UP: {
// ······
} break;
case MotionEvent.ACTION_UP: {
// ······
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}
// ······
return true;
}
??如上就是RecyclerView
的onTouchEvent
方法,我大量的簡(jiǎn)化了這個(gè)方法迷郑,先讓大家對(duì)它的結(jié)構(gòu)有一個(gè)了解枝恋。
??其中ACTION_DOWN
创倔、ACTION_MOVE
、ACTION_UP
和ACTION_CANCEL
這幾個(gè)事件焚碌,我相信各位同學(xué)都比較熟悉畦攘,這是View最基本的事件。
??可能有人對(duì)ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件比較陌生十电,這兩個(gè)事件就跟多指滑動(dòng)有關(guān)知押,也是本文重點(diǎn)分析之一。
??好了鹃骂,我們現(xiàn)在開始正式分析源碼台盯。在分析源碼之前,我先將上面的代碼做一個(gè)簡(jiǎn)單的概述畏线。
- 如果當(dāng)前的
mActiveOnItemTouchListener
需要消耗當(dāng)前事件静盅,那么優(yōu)先交給它處理。- 如果
mActiveOnItemTouchListener
不消耗當(dāng)前事件寝殴,那么就走正常的事件分發(fā)機(jī)制温亲。這里面有很多的細(xì)節(jié),稍后我會(huì)詳細(xì)的介紹杯矩。
??關(guān)于第一步栈虚,這里不用我來解釋,它就是一個(gè)Listener
的回調(diào)史隆,非常的簡(jiǎn)單魂务,我們重點(diǎn)的在于分析第二步。
(1). Down 事件
??我們先來看看這部分的代碼吧泌射。
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
??這里主要是做了兩件事粘姜。
- 記錄下Down事件的x、y坐標(biāo)熔酷。
- 調(diào)用
startNestedScroll
方法孤紧,詢問父View
是否處理事件。
??Down
事件還是比較簡(jiǎn)單拒秘,通常來說就一些初始化的事情号显。
??接下來,我們來看看重頭戲--move事件
(2). Move事件
??我們先來看看這部分的代碼:
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
??這部分代碼非常的簡(jiǎn)單躺酒,我將它分為如下幾步:
- 根據(jù)Move事件產(chǎn)生的x押蚤、y坐標(biāo)來計(jì)算dx、dy羹应。
- 調(diào)用
dispatchNestedPreScroll
詢問父View
是否優(yōu)先處理滑動(dòng)事件揽碘,如果要消耗,dx和dy分別會(huì)減去父View
消耗的那部分距離。- 然后根據(jù)情況來判斷
RecyclerView
是垂直滑動(dòng)還是水平滑動(dòng)雳刺,最終是調(diào)用scrollByInternal
方法來實(shí)現(xiàn)滑動(dòng)的效果的劫灶。- 調(diào)用
GapWorker
的postFromTraversal
來預(yù)取ViewHolder
。這個(gè)過程會(huì)走緩存機(jī)制部分的邏輯掖桦,同時(shí)也有可能會(huì)調(diào)用Adapter
的onBindViewHolder
方法來提前加載數(shù)據(jù)浑此。
??其中第一步和第二步都是比較簡(jiǎn)單的,這里就直接省略滞详。
??而scrollByInternal
方法也是非常的簡(jiǎn)單凛俱,在scrollByInternal
方法內(nèi)部,實(shí)際上是調(diào)用了LayoutManager
的scrollHorizontallyBy
方法或者scrollVerticallyBy
方法來實(shí)現(xiàn)的料饥。LayoutManager
這兩個(gè)方法實(shí)際上也沒有做什么比較騷的操作蒲犬,歸根結(jié)底,最終調(diào)用了就是調(diào)用了每個(gè)Child
的offsetTopAndBottom
或者offsetLeftAndRight
方法來實(shí)現(xiàn)的岸啡,這里就不一一的跟蹤代碼了原叮,大家了解就行了。在本文的后面巡蘸,我會(huì)照著RecyclerView
滑動(dòng)相關(guān)的代碼寫一個(gè)簡(jiǎn)單的Demo奋隶。
??在這里,我們就簡(jiǎn)單的分析一下GapWorker
是怎么進(jìn)行預(yù)取的悦荒。我們來看看postFromTraversal
方法:
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
if (recyclerView.isAttachedToWindow()) {
if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("attempting to post unregistered view!");
}
if (mPostTimeNs == 0) {
mPostTimeNs = recyclerView.getNanoTime();
recyclerView.post(this);
}
}
recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
??在postFromTraversal
方法內(nèi)部也沒有做多少事情唯欣,最核心在于調(diào)用了post
方法,向任務(wù)隊(duì)列里面添加了一個(gè)Runnable
搬味【城猓看來重點(diǎn)的分析還是GapWorker
的run
方法:
@Override
public void run() {
try {
TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
if (mRecyclerViews.isEmpty()) {
// abort - no work to do
return;
}
// Query most recent vsync so we can predict next one. Note that drawing time not yet
// valid in animation/input callbacks, so query it here to be safe.
final int size = mRecyclerViews.size();
long latestFrameVsyncMs = 0;
for (int i = 0; i < size; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
}
}
if (latestFrameVsyncMs == 0) {
// abort - either no views visible, or couldn't get last vsync for estimating next
return;
}
long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
prefetch(nextFrameNs);
// TODO: consider rescheduling self, if there's more work to do
} finally {
mPostTimeNs = 0;
TraceCompat.endSection();
}
}
??run
方法的邏輯也是非常簡(jiǎn)單,首先計(jì)算獲得下一幀的時(shí)間碰纬,然后調(diào)用prefetch
方法進(jìn)行預(yù)取ViewHolder
萍聊。
void prefetch(long deadlineNs) {
buildTaskList();
flushTasksWithDeadline(deadlineNs);
}
??prefetch
方法也簡(jiǎn)單,顯示調(diào)用buildTaskList
方法生成任務(wù)隊(duì)列悦析,然后調(diào)用flushTasksWithDeadline
來執(zhí)行task
,這其中會(huì)調(diào)用RecyclerView
的tryGetViewHolderForPositionByDeadline
方法來獲取一個(gè)ViewHolder
寿桨,這里就不一一分析了。
??不過需要提一句的是强戴,tryGetViewHolderForPositionByDeadline
方法是整個(gè)RecyclerView
緩存機(jī)制的核心亭螟,RecyclerView
緩存機(jī)制在這個(gè)方法被淋漓盡致的體現(xiàn)出來。關(guān)于這個(gè)方法酌泰,如果不出意外的話媒佣,在下一篇文章里面我們就可以接觸到匕累,在這里陵刹,先給大家賣一個(gè)關(guān)子??。
??最后就是Up事件和Cancel事件,這兩個(gè)事件更加的簡(jiǎn)單衰琐,都進(jìn)行一些清理的操作也糊,這里就不分析了。不過在Up事件里面羡宙,有一個(gè)特殊事件可能會(huì)產(chǎn)生--fling事件狸剃,待會(huì)我們會(huì)詳細(xì)的分析。
2. 多指滑動(dòng)
??大家千萬不會(huì)誤會(huì)這里多指滑動(dòng)的意思狗热,這里的多指滑動(dòng)不是指RecyclerView
能夠相應(yīng)多根手指的滑動(dòng)钞馁,而是指當(dāng)一個(gè)手指還沒釋放時(shí),此時(shí)另一個(gè)手指按下匿刮,此時(shí)RecyclerView
就不相應(yīng)上一個(gè)手指的手勢(shì)僧凰,而是相應(yīng)最近按下手指的手勢(shì)。
??我們來看看這部分的代碼:
case MotionEvent.ACTION_POINTER_DOWN: {
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
??當(dāng)另一個(gè)手指按下時(shí)熟丸,此時(shí)就會(huì)立即更新按下的坐標(biāo)训措,同時(shí)會(huì)更新mScrollPointerId
,表示后面只會(huì)響應(yīng)最近按下手指的手勢(shì)。
??其次光羞,我們來看看多指松開的情況:
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
private void onPointerUp(MotionEvent e) {
final int actionIndex = e.getActionIndex();
if (e.getPointerId(actionIndex) == mScrollPointerId) {
// Pick a new pointer to pick up the slack.
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = e.getPointerId(newIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
}
}
??在這里也沒有比較騷的操作绩鸣,就是普通的更新。這里就不詳細(xì)的解釋了纱兑。本文后面會(huì)有一個(gè)小Demo呀闻,讓大家看看根據(jù)RecyclerView
依葫蘆畫瓢做出來的效果。
??接下來潜慎,我們來最后一個(gè)滑動(dòng)总珠,也是本文最重點(diǎn)分析的滑動(dòng)--fling滑動(dòng)。為什么需要重點(diǎn)分析fling事件勘纯,因?yàn)樵谖覀兤匠W远xView
,fling
事件是最容易被忽視的局服。
3. fling滑動(dòng)
??我們先來看看fling
滑動(dòng)產(chǎn)生的地方,也是Up事件的地方:
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;
??從上面的代碼中驳遵,我們可以看出來淫奔,最終是調(diào)用fling
方法來是實(shí)現(xiàn)fling
效果的,我們來看看fling
方法:
public boolean fling(int velocityX, int velocityY) {
// ······
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;
}
??在fling
方法里面,顯示調(diào)用dispatchNestedPreFling
方法詢問父View
是否處理fling
事件,最后調(diào)用ViewFlinger
的fling
方法來實(shí)現(xiàn)fling
效果,所以真正的核心在于ViewFlinger
的fling
方法里面堤结,我們繼續(xù)來看:
public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
postOnAnimation();
}
??在ViewFlinger
的fling
方法里面,先是調(diào)用了OverScroller
的fling
來計(jì)算fling
相關(guān)的參數(shù)唆迁,包括fling
的距離和fling
的時(shí)間。這里就不深入的分析計(jì)算相關(guān)的代碼竞穷,因?yàn)檫@里面都是一些數(shù)學(xué)和物理的計(jì)算唐责。最后就是調(diào)用了postOnAnimation
方法。
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
removeCallbacks(this);
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}
??可能大家有可能看不懂上面的代碼瘾带,其實(shí)跟View
的post
差不多,所以最終還是得看ViewFlinger
的run
方法鼠哥。
??ViewFlinger
的run
方法比較長(zhǎng),這里我將它簡(jiǎn)化了一下:
public void run() {
// ······
// 第一步,更新滾動(dòng)信息朴恳,并且判斷當(dāng)前是否已經(jīng)滾動(dòng)完畢
// 為true表示未滾動(dòng)完畢
if (scroller.computeScrollOffset()) {
//······
if (mAdapter != null) {
// ······
// 滾動(dòng)特定距離
if (dx != 0) {
hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
overscrollX = dx - hresult;
}
if (dy != 0) {
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
// ······
}
// ······
// 如果滾動(dòng)完畢抄罕,就是調(diào)用finish方法;
// 如果沒有滾動(dòng)完畢于颖,就調(diào)用postOnAnimation方法繼續(xù)遞歸
if (scroller.isFinished() || (!fullyConsumedAny
&& !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
// setting state to idle will stop this.
setScrollState(SCROLL_STATE_IDLE);
if (ALLOW_THREAD_GAP_WORK) {
mPrefetchRegistry.clearPrefetchPositions();
}
stopNestedScroll(TYPE_NON_TOUCH);
} else {
postOnAnimation();
if (mGapWorker != null) {
mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
}
}
}
// ······
}
??整個(gè)fling
核心就在這里呆贿,通過上面的三步,最終就是實(shí)現(xiàn)了fling的效果森渐,上面的注意已經(jīng)非常的清晰了做入,這里就不繼續(xù)分析了。
??我們分析了RecyclerView
的fling
事件同衣,有什么幫助呢母蛛?在日常的開發(fā)中,如果需要fling
的效果乳怎,我們可以根據(jù)RecyclerView
實(shí)現(xiàn)方式來實(shí)現(xiàn)彩郊,是不是就覺得非常簡(jiǎn)單呢?對(duì)的蚪缀,這就是我們學(xué)習(xí)源碼的目的秫逝,不僅要理解其中的原理,還需要學(xué)以致用??询枚。
4. Demo展示
??這里的demo不是很高大上的東西违帆,就是照著RecyclerView
的代碼實(shí)現(xiàn)了一個(gè)多指滑動(dòng)View而已。我們來看看源碼:
public class MoveView extends View {
private int mLastTouchX;
private int mLastTouchY;
private int mTouchSlop;
private boolean mCanMove;
private int mScrollPointerId;
public MoveView(Context context) {
this(context, null);
}
public MoveView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int actionIndex = event.getActionIndex();
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
mScrollPointerId = event.getPointerId(0);
mLastTouchX = (int) (event.getX() + 0.5f);
mLastTouchY = (int) (event.getY() + 0.5f);
mCanMove = false;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mScrollPointerId = event.getPointerId(actionIndex);
mLastTouchX = (int) (event.getX(actionIndex) + 0.5f);
mLastTouchY = (int) (event.getY(actionIndex) + 0.5f);
break;
case MotionEvent.ACTION_MOVE:
final int index = event.findPointerIndex(mScrollPointerId);
int x = (int) (event.getX(index) + 0.5f);
int y = (int) (event.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if(!mCanMove) {
if (Math.abs(dy) >= mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
mCanMove = true;
}
if (Math.abs(dy) >= mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
mCanMove = true;
}
}
if (mCanMove) {
offsetTopAndBottom(-dy);
offsetLeftAndRight(-dx);
}
break;
case MotionEvent.ACTION_POINTER_UP:
onPointerUp(event);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
private void onPointerUp(MotionEvent e) {
final int actionIndex = e.getActionIndex();
if (e.getPointerId(actionIndex) == mScrollPointerId) {
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = e.getPointerId(newIndex);
mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
}
}
}
??相信經(jīng)過RecyclerView
源碼的學(xué)習(xí)金蜀,對(duì)上面代碼的理解也不是難事刷后,所以這里我就不需要再解釋了。具體的效果渊抄,大家可以拷貝Android studio里面去看看??尝胆。
4. 總結(jié)
??RecyclerView
的滑動(dòng)機(jī)制相比較來說,還是非常簡(jiǎn)單护桦,我也感覺沒有什么可以總結(jié)含衔。不過從RecyclerView
的源碼,我們可以學(xué)習(xí)兩點(diǎn):
- 多指滑動(dòng)二庵。我們可以根據(jù)
RecyclerView
的源碼贪染,來實(shí)現(xiàn)自己的多指滑動(dòng),這是一種參考催享,也是學(xué)以致用fling
滑動(dòng)杭隙。RecyclerView
實(shí)現(xiàn)了fling
效果,在日常開發(fā)過程中因妙,如果我們也需要實(shí)現(xiàn)這種效果痰憎,我們可以根據(jù)RecyclerView
的源碼來實(shí)現(xiàn)票髓。