Android NestedScrolling嵌套滑動機制

Android NestedScrolling嵌套滑動機制

Android在發布5.0之后加入了嵌套滑動機制NestedScrolling,為嵌套滑動提供了更方便的處理方案。在此對嵌套滑動機制進行詳細的分析。

嵌套滑動的常見用法比如在滑動列表的時候隱藏相關的TopBar和BottomBar,增加列表的信息展示范圍,讓用戶聚焦于App想展示的內容上等。官方出的Design包里也有很多支持該機制的炫酷控件,比如CoordinatorLayout,AppBarLayout等,在用戶體驗上有很大的進步。

說道嵌套滑動,離不開一下幾個內容:

NestedScrollingChild

NestedScrollingParent

NestedScrollingChildHelper

NestedScrollingParentHelper

簡單看看這幾個類是如何使用的,在系統為我們提供的控件中,NestedScrollView是實現了這個機制的控件,以它的實現為例,首先看作為嵌套滑動的子View:

// NestedScrollingChild

@Override

public void setNestedScrollingEnabled(boolean enabled) {

mChildHelper.setNestedScrollingEnabled(enabled);

}

@Override

public boolean isNestedScrollingEnabled() {

return mChildHelper.isNestedScrollingEnabled();

}

@Override

public boolean startNestedScroll(int axes) {

return mChildHelper.startNestedScroll(axes);

}

@Override

public void stopNestedScroll() {

mChildHelper.stopNestedScroll();

}

@Override

public boolean hasNestedScrollingParent() {

return mChildHelper.hasNestedScrollingParent();

}

@Override

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,

int dyUnconsumed, int[] offsetInWindow) {

return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,

offsetInWindow);

}

@Override

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {

return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);

}

@Override

public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {

return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);

}

@Override

public boolean dispatchNestedPreFling(float velocityX, float velocityY) {

return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);

}

再來看看同樣作為嵌套滑動父View的NestedScrollView的實現

// NestedScrollingParent

@Override

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {

return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;

}

@Override

public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {

mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);

startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);

}

@Override

public void onStopNestedScroll(View target) {

mParentHelper.onStopNestedScroll(target);

stopNestedScroll();

}

@Override

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,

int dyUnconsumed) {

final int oldScrollY = getScrollY();

scrollBy(0, dyUnconsumed);

final int myConsumed = getScrollY() - oldScrollY;

final int myUnconsumed = dyUnconsumed - myConsumed;

dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);

}

@Override

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

dispatchNestedPreScroll(dx, dy, consumed, null);

}

@Override

public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {

if (!consumed) {

flingWithNestedDispatch((int) velocityY);

return true;

}

return false;

}

@Override

public boolean onNestedPreFling(View target, float velocityX, float velocityY) {

return dispatchNestedPreFling(velocityX, velocityY);

}

@Override

public int getNestedScrollAxes() {

return mParentHelper.getNestedScrollAxes();

}

從上面的實現可以看出,基本上都是通過mParentHelper和mChildHelper來完成滑動的,沒接觸過這方面的同學看著肯定覺得很難理解,的確有些跳躍性,在說清楚這個問題之前必須先把這幾個類之間的交互邏輯理清楚才能不至于不知所云。

先來梳理一下子View和父View的接中都有哪些方法。這種套路一般都是子View發起的然后父View進行回調從而完成配合。

子View父View

startNestedScrollonStartNestedScroll、onNestedScrollAccepted

dispatchNestedPreScrollonNestedPreScroll

dispatchNestedScrollonNestedScroll

stopNestedScrollonStopNestedScroll

為了避免重復造輪子,有個同學已經寫了一套很炫酷的開源控件( 地址:https://github.com/race604/FlyRefresh),借用他的實現結合NestedScrollView來用,來講解這套機制。這里的子View指的是實現了NestedScrollingChild的View,例如我們的NestedScrollView,父View指的是實現了NestedScrollingParent的View,比如這位同學開源控件里寫的PullHeaderLayout。

首先在子View滑動還未開始之前將調用startNestedScroll,對應NestedScrollView中的ACTION_DOWN:

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_DOWN: {

......

startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);//在接到點擊事件之初調用

break;

}

}

那么調用 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)寓意何在?跟進去看到其實是調用mChildHelper.startNestedScroll(axes)的實現

public boolean startNestedScroll(int axes) {

if (hasNestedScrollingParent()) {

// Already in progress

return true;

}

if (isNestedScrollingEnabled()) {

ViewParent p = mView.getParent();

View child = mView;

while (p != null) {

//重點在這-------> 在子View開始滑動前通知父View,回調到父View的onStartNestedScroll(),

//父View需要滑動則返回true:

if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {

mNestedScrollingParent = p;

//---------> 如果父View決定要和子View一塊滑動,調用父ViewonNestedScrollAccepted()

ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);

return true;

}

if (p instanceof View) {

child = (View) p;

}

p = p.getParent();

}

}

return false;

}

大家仔細看我在代碼里加的注釋,需要關心的就是父View在此時需要決定是否跟隨子View滑動,看看父View的實現:

@Override

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {

return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;

}

ViewCompat.SCROLL_AXIS_VERTICAL的值是2(10),所以當nestedScrollAxes 也為2的時候,返回true,回到上面可以看到只要是豎直方向的 滑動,父View就會和子View進行嵌套滑動。而在父View的

onNestedScrollAccepted中,則把滑動的方向給保存下來了。這樣父View和子View的第一次合作關系就結束了,再看看接下來是如何配合的。

當子View在滑動的Move事件中,又開始了嵌套滑動

@Override

public boolean onTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_MOVE:

final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);

int deltaY = mLastMotionY - y;

if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {

deltaY -= mScrollConsumed[1];

vtev.offsetLocation(0, mScrollOffset[1]);

mNestedYOffset += mScrollOffset[1];

}

}

在子View決定滑動的時候,再次在進行自己的滑動前調用dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {

if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {

if (dx != 0 || dy != 0) {

int startX = 0;

int startY = 0;

if (offsetInWindow != null) {

mView.getLocationInWindow(offsetInWindow);

startX = offsetInWindow[0];

startY = offsetInWindow[1];

}

if (consumed == null) {

if (mTempNestedScrollConsumed == null) {

mTempNestedScrollConsumed = new int[2];

}

consumed = mTempNestedScrollConsumed;

}

//--------->重點在這,首先把consume封裝好,consumed[0]表示X方向父View消耗的距離,

// consumed[1]表示Y方向上父View消耗的距離,在父View處理前當然都是0

consumed[0] = 0;

consumed[1] = 0;

//然后調用父View的onNestedPreScroll并把當前的dx,dy以及消耗距離的consumed傳遞過去

ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

if (offsetInWindow != null) {

mView.getLocationInWindow(offsetInWindow);

offsetInWindow[0] -= startX;

offsetInWindow[1] -= startY;

}

return consumed[0] != 0 || consumed[1] != 0;

} else if (offsetInWindow != null) {

offsetInWindow[0] = 0;

offsetInWindow[1] = 0;

}

}

return false;

}

看看父View是怎么處理的,也是實現了這套機制的,看看他是怎么處理的:

@Override

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

if (dy > 0 && mHeaderController.canScrollUp()) {

final int delta = moveBy(dy);

consumed[0] = 0;

consumed[1] = delta;

}

}

通過moveby計算父View滑動的距離,并將父ViewY方向消耗的距離記錄下來

繼續來看子View,在通知了父View并且父View消耗了滑動距離之后,剩下的就是自己進行滑動了

@Override

public boolean onTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_MOVE:

final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);

int deltaY = mLastMotionY - y;

if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {

deltaY -= mScrollConsumed[1];

//重點在這:-------->父View滑動之后調整自己的Offset為父View滑動的距離

vtev.offsetLocation(0, mScrollOffset[1]);

mNestedYOffset += mScrollOffset[1];

}

.........

if(mIsBeingDragged){

mLastMotionY = y - mScrollOffset[1];

final int oldY = getScrollY();

final int range = getScrollRange();

final int overscrollMode = ViewCompat.getOverScrollMode(this);

boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||

(overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&

range > 0);

// Calling overScrollByCompat will call onOverScrolled, which

// calls onScrollChanged if applicable.

//重點在這:-------->父View消耗了部分滑動距離后,子View自己開始滑動,通過overScrollByCompat

//把滑動距離的參數傳給mScroller進行彈性滑動

if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,

0, true) && !hasNestedScrollingParent()) {

// Break our velocity if we hit a scroll barrier.

mVelocityTracker.clear();

}

}

......

//重點在這:-------->自己滑動完之后再調用dispatchNestedScroll通知父View滑動結束

if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {

mLastMotionY -= mScrollOffset[1];

vtev.offsetLocation(0, mScrollOffset[1]);

mNestedYOffset += mScrollOffset[1];

}

break;

}

接下來又是父View的回調了,來看看父View的處理:

@Override

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,

int dyUnconsumed) {

final int myConsumed = moveBy(dyUnconsumed);

final int myUnconsumed = dyUnconsumed - myConsumed;

}

父View在這里將最后子View滑動完后剩余的距離進行收尾處理,自我調整后第二輪的嵌套滑動也結束了。

那么再看看最后一輪滑動:

@Override

public boolean onTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_UP:

/* Release the drag */

mIsBeingDragged = false;

mActivePointerId = INVALID_POINTER;

recycleVelocityTracker();

if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {

ViewCompat.postInvalidateOnAnimation(this);

}

stopNestedScroll();

break;

}

在觸控事件的最后一個階段,也就是ACTION_UP時,調用stopNestedScroll(),這時會通知父View的onStopNestedScroll()來對整個系列的滑動來收尾

public void stopNestedScroll() {

if (mNestedScrollingParent != null) {

ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);

mNestedScrollingParent = null;

}

}

父類最后在自己的onStopNestedScroll()實現相關的收尾處理,比如重置滑動狀態標記,完成動畫操作,通知滑動結束等。這樣,整個滑動嵌套流程就完成了。

最后來總結一下整個流程,分為三個步驟:

步驟一:子View的ACTION_DOWN調用startNestedScroll—->父View的onStartNestedScroll判斷是否要一起滑動,父ViewonNestedScrollAccepted同意協同滑動

步驟二:子View的ACTION_MOVE調用dispatchNestedPreScroll—->父View的onNestedPreScroll在子View滑動之前先進行滑動并消耗需要的距離—->父View完成該次滑動之后返回消耗的距離,子View在剩下的距離中再完成自己需要的滑動

步驟三:子View滑動完成之后調用dispatchNestedScroll—->父View的onNestedScroll處理父View和子View之前滑動剩余的距離

步驟四:子View的ACTION_UP調用stopNestedScroll—->父View的onStopNestedScroll完成滑動收尾工作

這樣,子View和父View的一系列嵌套滑動就完成了,可以看出來整個嵌套滑動還是靠子View來推動父View進行滑動的,這也解決了在傳統的滑動事件中一旦事件被子View處理了就很難再分享給父View共同處理的問題,這也是嵌套滑動的一個特點。

來源:https://dreamerhome.github.io/2016/11/03/nestedscrolling/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容