Android Nested Scrolling

cover
cover

Android常規(guī)的Touch事件傳遞機(jī)制是自頂向下,由外向內(nèi)的,一旦確定了事件消費(fèi)者View,隨后的事件都將傳遞到該View。因?yàn)槭亲皂斚蛳?,父控件可以隨時(shí)攔截事件,下拉刷新、拖拽排序、折疊等交互效果都可以通過這套機(jī)制完成。Touch事件傳遞機(jī)制是Android開發(fā)必須掌握的基本內(nèi)容。但是這套機(jī)制存在一個(gè)缺陷:子View無法通知父View處理事件。NestedScrolling就是為這個(gè)場景設(shè)計(jì)的。

NestedScrollingChild和NestedScrollingParent

NestedScrolling是指存在嵌套滾動(dòng)的場景,常見于下拉刷新、展開/收起標(biāo)題欄等。Support包中的CoordinatorLayoutScrollRefreshLayout就是基于NestedScrolling機(jī)制實(shí)現(xiàn)的。

NestedScrollingChildNestedScrollingParent分別定義了嵌套子View和嵌套父View需要實(shí)現(xiàn)的接口,方法列表分別如下,可以先略過,后面會(huì)把這些方法串起來。另外這些方法基本都是通過NestedScrollingChildHelperNestedScrollingParentHelper來實(shí)現(xiàn),一般并不需要手動(dòng)編寫多少邏輯。

// NestedScrollingChild
void setNestedScrollingEnabled(boolean enabled);
boolean startNestedScroll(int axes);
void stopNestedScroll();
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
boolean hasNestedScrollingParent();
boolean isNestedScrollingEnabled();
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
boolean dispatchNestedPreFling(float velocityX, float velocityY);
// NestedScrollingParent
int getNestedScrollAxes();
boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
boolean onNestedPreFling(View target, float velocityX, float velocityY);
void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
void onStopNestedScroll(View target);

通過方法名可以看出,NestedScrollingChild的方法均為主動(dòng)方法,而NestedScrollingParent的方法基本都是回調(diào)方法。這也是NestedScrolling機(jī)制的一個(gè)體現(xiàn),子View作為NestedScrolling事件傳遞的主動(dòng)方,父View作為被動(dòng)方。

NestedScrolling機(jī)制生效的前提條件是子View作為Touch事件的消費(fèi)者,在消費(fèi)過程中向父View發(fā)送NestedScrolling事件(注意這里不是Touch事件,而是NestedScrolling事件)。

NestedScrolling事件傳遞

NestedScrolling機(jī)制中,NestedScrolling事件使用dx, dy表示,分別表示子View Touch事件處理方法中判定的x和y方向上的滾動(dòng)偏移量。

NestedScrolling事件的傳遞:

  1. 由子View產(chǎn)生NestedScrolling事件;
  2. 發(fā)送給父View進(jìn)行處理,父View處理之后,返回消費(fèi)的偏移量;
  3. 子View根據(jù)父View消費(fèi)的偏移量計(jì)算NestedScrolling事件剩余偏移量;
  4. 根據(jù)剩余偏移量判斷是否能處理滾動(dòng)事件;如果處理滾動(dòng)事件,同時(shí)將自身滾動(dòng)情況通知父View;
  5. 處理結(jié)束,事件傳遞完成。
  1. 這里只說明了一層嵌套的情況,事實(shí)上NestedScrolling很可能出現(xiàn)在多重嵌套的場景。對(duì)于多重嵌套,步驟2、3、4將事件自底向上進(jìn)行傳遞,步驟2中消費(fèi)的偏移量將記錄所有嵌套父View消費(fèi)偏移量的總和。這里不再重復(fù)。
  2. Fling事件的傳遞和Scroll類似,也不再贅述。

方法調(diào)用流程

我們可以把上面的方法根據(jù)NestedScrolling事件傳遞的不同階段進(jìn)行分組(Fling跟隨Scrolling發(fā)生)。

初始階段:確認(rèn)開啟NestedScrolling,關(guān)聯(lián)父View和子View。

// NestedScrollingChild
void setNestedScrollingEnabled(boolean enabled);
boolean startNestedScroll(int axes);
// NestedScrollingParent
int getNestedScrollAxes()
boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

預(yù)滾動(dòng)階段:子View將事件分發(fā)到父View

// NestedScrollingChild
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
// NestedScrollingParent
void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

滾動(dòng)階段:子View處理滾動(dòng)事件。

// NestedScrollingChild
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
// NestedScrollingParent
void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);

結(jié)束階段:結(jié)束。

// NestedScrollingChild
void stopNestedScroll();
// NestedScrollingParent
void onStopNestedScroll(View target);

下面是一次嵌套滾動(dòng)(三級(jí)嵌套)從開始到結(jié)束的方法調(diào)用時(shí)序圖:

methods
methods

金色是NestedScrollingChild的方法,為子View主動(dòng)調(diào)用。

紫色是NestedScrollingParent的回調(diào)方法,由子View相關(guān)方法調(diào)用。

橙色為滾動(dòng)事件被消費(fèi)的時(shí)機(jī)

當(dāng)子View調(diào)用startNestedScroll方法時(shí),開始嵌套滾動(dòng)流程;之后不斷循環(huán)pre-scroll和scroll兩個(gè)過程(一般在子View的onTouchEvent的MOVE分支調(diào)用);直到手指抬起,子View調(diào)用stopNestedScroll方法結(jié)束滾動(dòng)(在結(jié)束之前可能進(jìn)入Fling狀態(tài))。

劃重點(diǎn)

最重要的一點(diǎn):pre-scroll過程是子View向父View傳遞事件的過程,而scroll過程才是子View消耗滾動(dòng)事件的過程,也就是說父View擁有優(yōu)先消費(fèi)事件的權(quán)利。

從事件消耗的優(yōu)先級(jí)來看,可以畫出這樣一張圖。

nested_scrolling_event_flow
nested_scrolling_event_flow

dispatchNestedPreScroll傳給父View的是沒有被消費(fèi)的滾動(dòng)事件,父View消費(fèi)完之后通過consumed數(shù)組返回,如果還有剩余,子View進(jìn)行消費(fèi),并將消費(fèi)多少和剩余多少再次發(fā)給父View。

如果一個(gè)View同時(shí)作為NestedScrollingChild和NestedScrollingParent,那么在處理onNestedPreScrolling和onNestedScrolling的時(shí)候,也要按照自底向上的規(guī)則,先讓父View處理事件。

實(shí)例分析以及Q&A

這里通過對(duì)CoordinatorLayout -> SwipeRefreshLayout -> RecyclerView這個(gè)常用的三級(jí)嵌套實(shí)例進(jìn)行分析,以便深入理解NestedScrolling事件傳遞的機(jī)制。

嗯,其實(shí)上面那張時(shí)序圖基本就通過方法調(diào)用的順序,理清了傳遞的過程。

這里通過幾個(gè)Q&A,來解答疑惑。

如果你還不清楚SwipeRefreshLayout的原理,建議先去看一下我的另一篇文章:SwipeRefreshLayout源碼分析。

CL代表CoordinatorLayout,SRL代表SwipeRefreshLayout,RV表示RecyclerView。實(shí)在打不動(dòng)字了……

Q1: SwipeRefreshLayout在Touch事件分發(fā)過程中,為什么SwipeRefreshLayout沒有作為Touch事件的消費(fèi)者?

A1: Touch事件流從ACTION_DOWN開始:

  1. 先經(jīng)過SRL的onInterceptTouchEvent(),返回false。
  2. 進(jìn)入RV的onInterceptTouchEvent(),進(jìn)入ACTION_DOWN分支,RV調(diào)用startNestedScrolling()方法。
  3. 根據(jù)上面的時(shí)序圖,會(huì)調(diào)用SRL的onNestedScrollAccepted(),而這個(gè)方法里面,會(huì)將SRL的mNestedScrollInProgress設(shè)置為true。事實(shí)上到此為止已經(jīng)進(jìn)入了NestedScrolling事件的分發(fā)流程。
  4. 后續(xù)事件,SRL的onInterceptTouchEvent()方法會(huì)根據(jù)mNestedScrollInProgress屬性返回false,也就不會(huì)攔截事件了。
  5. CV的部分根據(jù)時(shí)序圖可以清楚理解。
Q2: 接Q1,既然沒有攔截,為什么還能處理事件?

A2: 首先,要注意SRL處理的不是Touch事件,而是NestedScrolling事件,還記得嗎,實(shí)際上是以(dx, dy)偏移量的形式存在的。A1中可以看到,一旦觸發(fā)NestedScrolling機(jī)制,作為父View的SRL,就有優(yōu)先處理NestedScrolling事件的權(quán)利,所以當(dāng)然能處理事件(當(dāng)然優(yōu)先級(jí)比CL低,所以只能處理CL處理剩下的部分)。

Q3: 為什么CL能消費(fèi)事件進(jìn)行滾動(dòng)?

**A3: **NestedScrolling機(jī)制決定NestedScrolling事件時(shí)自底向上傳播的,并且通過pre-scroll和scroll兩個(gè)過程的劃分,越上層的View,處理NestedScrolling事件的優(yōu)先級(jí)越高。這個(gè)例子中,CL在最上層,自然優(yōu)先處理事件。

Q4: 對(duì)于SwipeRefreshLayout來說,什么時(shí)候通過onTouchEvent方法處理事件,什么時(shí)候通過NestedScrolling機(jī)制處理事件?

A4: NestedScrolling機(jī)制由實(shí)現(xiàn)了NestedScrollingChild接口的子View觸發(fā),所以事實(shí)上,當(dāng)SRL的子View實(shí)現(xiàn)了NestedScrollingChild接口時(shí),均會(huì)使用NestedScrolling機(jī)制分發(fā)事件給SRL。比如RecyclerView作為子View將通過NestedScrolling處理事件,如果是ListView作為子View,將通過Touch機(jī)制處理事件。

總結(jié)

讀到這里你會(huì)發(fā)現(xiàn),要理解NestedScrolling,實(shí)際上就是要理解NestedScrolling事件分發(fā)流程。這篇博客寫了兩個(gè)晚上,很久沒有花這么長時(shí)間寫huatu客了,希望能給你帶來幫助。歡迎轉(zhuǎn)發(fā)分享贊賞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容