不喜勿噴,有錯請留言(以下源碼均來自recyclerView-27.1.1版本)
在之前的layout篇中,我們分析了layout的對ItemView進行創建,填充,測量和佈局,以及錨點的意義等,接下來就要從scroll角度分析,但是談及scroll就不得不提事件分發,故衍生出這一篇划水篇
RecyclerView的scroll操作
RecyclerView本身的scroll操作有多個,既然是滑動操作自然離不開我們的觸摸事件處理,那我也決定先從觸摸事件開始分析處理,循序漸進的的進入scroll,從而分析理解scroll角度裏的layoutManager
首先我們RecyclerView中提供的scroll方法有很多scrollTo/scrollBy/smoothScrollToPosition/scrollToPosition以及jumpToPositionForSmoothScroller,但是這些方法的實現幾乎都是依賴於layoutmanager,我逐個將其方法貼出
這裏除了scrollTo方法體空實現,內部官方給了`RecyclerView does not support scrolling to an absolute position. Use scrollToPosition instead` 即使用scrollToPosition替代的說明
其他均是layoutManager實現
public void scrollToPosition(int position) {
...省略部分代碼...
mLayout.scrollToPosition(position);調用layoutManager的scrollToPosition操作
awakenScrollBars();
}
public void smoothScrollToPosition(int position) {
...省略部分代碼...
mLayout.smoothScrollToPosition(this, mState, position);調用layoutManager的smoothScrollToPosition操作
}
public void scrollBy(int x, int y) {
...省略部分代碼...
if (canScrollHorizontal || canScrollVertical) {
雖然此處執行的是scrollByInternal方法,文章中會分析該方法,最終執行的是layoutManager的scrollHorizontallyBy/scrollVerticallyBy方法
scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null);
}
}
void jumpToPositionForSmoothScroller(int position) {
...省略部分代碼...
mLayout.scrollToPosition(position);調用layoutManager的scrollToPosition操作
awakenScrollBars();
}
onTouchEvent
首先由於onTouchEvent的方法較長,這裏進行拆開分析,首先我們先進入onTouchEvent的時候就做了一些判斷,我們先來看看這些條件
1.這第一個條件是佈局是否被禁用,若被禁用時,則不做處理直接返回
if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
return false;
}
2.當佈局未被禁用時,先看item是否有touch監聽,如果有touch事件的監聽處理,則將事件傳遞給item並重置觸摸事件的標記
if (dispatchOnItemTouch(e)) {
cancelTouch();
return true;
}
3.若佈局layoutManager爲null,那就沒發玩了,就直接返回 不處理
if (mLayout == null) {
return false;
}
4.檢查layout都支持那些方面的滾動,是否支持橫向,是否支持縱向
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
當判斷完這幾個條件後,就可以進行處理對觸摸的不同事件的處理等操作
-
準備工作
1.創建一個輔助跟蹤觸摸事件速度的工具類 if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } boolean eventAddedToVelocityTracker = false; 2.從原有的motionEvent拷貝一份 final MotionEvent vtev = MotionEvent.obtain(e); 3.獲取當前motinoEvent的類型,和關聯的指針索引信息 final int action = e.getActionMasked(); final int actionIndex = e.getActionIndex(); if (action == MotionEvent.ACTION_DOWN) { mNestedOffsets[0] = mNestedOffsets[1] = 0; } 4.設置事件偏移量位置 vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
-
事件狀態處理
-
ACTION_DOWN 和 ACTION_POINTER_DOWN:
由於它們兩個比較接近,所以放在一塊分析
1.設置pointer
2.初始化觸摸位點
3.設置方向滾動指示(ACTION_DOWN獨有)
-
ACTION_POINTER_UP
POINTER_UP事件相對簡單,重新選擇新的指針,重置touchX和touchY
private void onPointerUp(MotionEvent e) { final int actionIndex = e.getActionIndex(); if (e.getPointerId(actionIndex) == mScrollPointerId) { 選擇一個新的指針來彌補不足並重置touchX和touchY 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); } }
-
ACTION_UP
1.通過VelocityTracker將用戶的移動添加到跟蹤器
2.獲取x軸與y軸的距離
3.執行fling操作並重置scrollstate(由於這一步比較重要,我們再貼下其的位置,以免大家看不明白)
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); }
4.重置touch期間的一些工具和參數恢復初始化狀態等待下一次touchEvent使用
-
ACTION_CANCEL
從方法名上我們很容易知道這是一個取消觸摸的方法
-
ACTION_MOVE
1.通過findPointerIndex找到其index,若index小於0,則跳過不處理
2.通過xy的進一法,對x和y進行記錄
3.如果有嵌套滾動,則分發嵌套滾動至嵌套進程上
若滾動狀態不是SCROLL_STATE_DRAGGING
1.如果可以橫向滾動,且x軸距離屬於合理範圍,則對x進行方向上的計算,並打開startScroll開關
2.如果可以縱向滾動,且y軸距離屬於合理範圍,則對y進行方向上的計算,並打開startScroll開關
3.如果startScroll開啓,那麼設置scrollState狀態爲SCROLL_STATE_DRAGGING
若滾動狀態爲SCROLL_STATE_DRAGGING
1.srollByInternal操作,也將是我們layoutmanager滾動的開始(此方法我們留在篇尾重點分析)
2.Gapworker進行預取操作
-
-
事件處理簡要分析
可能有很多人對這塊的事件處理有些許疑問
-
爲什麼要監聽ACTION_POINTER_DOWN和ACTION_POINTER_UP
當我們用兩根手指觸摸屏幕時,只會根據第一根手指的滑動,當鬆開第一根手指時,如果我們不堅挺ACTION_POINTER的事件,會導致屏幕突然滾動一大段,就是緣於第二根手指移動事件的x,y會和第一根手指移動時留下的x和y比較導致屏幕滾動
-
scrollByInternal分析
既然這個方法關聯了我們layoutManager的滾動啓動,那麼我們就花篇幅分析,爲我們下一篇的layoutManager做準備,我們一步步分析,由於scrollByInternal方法過長,我們先分析其前半部分
首先先定義了針對x,y兩個方向上的變量,緊接着就進入consumePendingUpdateOperations方法中,對於consumePendingUpdateOperations方法從方法命名上我們能感覺出來該方法主要作用處理延遲更新操作
void consumePendingUpdateOperations() {
非第一次layoutComplete 或者 layout後數據發生改變
if (!mFirstLayoutComplete || mDataSetHasChangedAfterLayout) {
TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
dispatchLayout();執行layoutstep操作
TraceCompat.endSection();
return;
}
if (!mAdapterHelper.hasPendingUpdates()) {
return;
}
如果僅一個條目改變,檢測任何可見項是否受影響,如果沒有,就忽略該更改
if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && !mAdapterHelper
.hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE
| AdapterHelper.UpdateOp.MOVE)) {
TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG);
startInterceptRequestLayout();
onEnterLayoutOrScroll();鎖定滾動
mAdapterHelper.preProcess();
if (!mLayoutWasDefered) {
if (hasUpdatedView()) {
dispatchLayout();執行layoutstep操作
} else {
不需要滾動,清除狀態
mAdapterHelper.consumePostponedUpdates();
}
}
stopInterceptRequestLayout(true);
onExitLayoutOrScroll();釋放滾動
TraceCompat.endSection();
} else if (mAdapterHelper.hasPendingUpdates()) {
TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
}
}
處理完延遲更新操作後,若adapter不爲null
1.攔截requestLayout操作,然後鎖定滾動狀態(主要是爲了推遲layout操作)
2.fillRemianingScrllValues 根據滾動狀態,藉助scroller查看可滾動的值還有多少
3.接着根據x和y不爲0時,對layoutmanager進行scroll處理
4.最後釋放滾動狀態,釋放攔截requestLayout操作
5.如果decoration不爲null則通知invalidate掉decoration
這是scrollByInternal方法上半部分的處理,接下來我們看其下半部分的方法
-
這一部分首先是執行了dispatchNestedScroll方法,這個方法內部是調用了NestedScrollingChildHelper的dispatchNestedScroll的方法,這個方法是對嵌套的處理,如果父級使用了任何嵌套滾動,則爲true,其方法體會將滾動的x,y向父級處理
-
接下來 對滾動狀態,若不是View.OVER_SCROLL_NEVER就進行considerReleasingGlowsOnScroll,首先對於OverScrollMode的結果有三種第一種是OVER_SCROLL_ALWAYS(若視圖允許滾動,始終允許過度滾動此視圖)也是默認情況,第二種是OVER_SCROLL_IF_CONTENT_SCROLLS(若視圖允許滾動,則當視圖大於容器時,允許過度滾動),第三種就是OVER_SCROLL_NEVER(拒絕滾動此視圖);可能有人對這裏有些迷糊,我來大白話一下,細心的朋友發現Android2.3以後滾動的邊界時,recylcerView會有一個光暈提示到底,其實這個就是對光暈的操作,如下:
-
View.OVER_SCROLL_ALWAYS
滑動到邊界繼續滑動總是會出現提示的光暈
-
View.OVER_SCROLL_NEVER
滑動道邊界後,接續滑動永遠不會出現提示的光暈
-
View.OVER_SCROLL_IF_CONTENT_SCROLLS
如果內容大於容器,滑到邊界會出現提示的光暈,反之則不會出現提示的光暈
-
-
最後若consumeX和consumeY有其一不爲0,就執行dispatchOnScrolled操作,其實就是滾動回調操作,方法如下
- 傳遞當前的scrollx和scrolly,通過onScrolled傳出,以便響應視圖內部滾動處理
- 如果scrollListener不爲null則將其操作通過onScrollChanged傳出
-
是否喚醒scrollBar,若喚醒則執行invalidate操作
至此,我們的onTouch的部分就分析完畢了,
onInterceptTouchEvent
-
還是老規矩,首先看佈局是否允許滾動,在然後就是itemTouchListerner是否攔截消費,重置touch狀態,不再處理
若mOnItemTouchListners的size大於0則執行listener的onInterceptTouchEvent方法
-
接着如果layout爲null不處理,然後判斷layout支持橫向還是豎向
-
藉助VelocityTracker來處理滑動速度
-
接着就是事件的分發處理(由於與touchEvent比較類似所以就不再分析,具體可借鑑前方的touchEvent分析)
至此我們的RecyclerView的touchEvent事件篇就到這結束了,從這裏我們可以看出對於touch事件,recyclerView自身對於scroll狀態以及嵌套情況的處理,若有不對,敬請批評指出