Android NestedScrolling嵌套滑動機制

Android NestedScrolling嵌套滑動機制

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

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

說道嵌套滑動,離不開一下幾個內(nèi)容:

NestedScrollingChild

NestedScrollingParent

NestedScrollingChildHelper

NestedScrollingParentHelper

簡單看看這幾個類是如何使用的,在系統(tǒng)為我們提供的控件中囤躁,NestedScrollView是實現(xiàn)了這個機制的控件冀痕,以它的實現(xiàn)為例,首先看作為嵌套滑動的子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的實現(xiàn)

// 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();

}

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

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

子View父View

startNestedScrollonStartNestedScroll婿斥、onNestedScrollAccepted

dispatchNestedPreScrollonNestedPreScroll

dispatchNestedScrollonNestedScroll

stopNestedScrollonStopNestedScroll

為了避免重復(fù)造輪子,有個同學(xué)已經(jīng)寫了一套很炫酷的開源控件( 地址:https://github.com/race604/FlyRefresh)翩瓜,借用他的實現(xiàn)結(jié)合NestedScrollView來用受扳,來講解這套機制。這里的子View指的是實現(xiàn)了NestedScrollingChild的View兔跌,例如我們的NestedScrollView勘高,父View指的是實現(xiàn)了NestedScrollingParent的View,比如這位同學(xué)開源控件里寫的PullHeaderLayout。

首先在子View滑動還未開始之前將調(diào)用startNestedScroll华望,對應(yīng)NestedScrollView中的ACTION_DOWN:

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_DOWN: {

......

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

break;

}

}

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

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蕊蝗,回調(diào)到父View的onStartNestedScroll(),

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

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

mNestedScrollingParent = p;

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

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

return true;

}

if (p instanceof View) {

child = (View) p;

}

p = p.getParent();

}

}

return false;

}

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

@Override

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

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

}

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

onNestedScrollAccepted中,則把滑動的方向給保存下來了讲衫。這樣父View和子View的第一次合作關(guān)系就結(jié)束了缕棵,再看看接下來是如何配合的。

當(dāng)子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決定滑動的時候招驴,再次在進行自己的滑動前調(diào)用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處理前當(dāng)然都是0

consumed[0] = 0;

consumed[1] = 0;

//然后調(diào)用父View的onNestedPreScroll并把當(dāng)前的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是怎么處理的,也是實現(xiàn)了這套機制的丹允,看看他是怎么處理的:

@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方向消耗的距離記錄下來

繼續(xù)來看子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滑動之后調(diào)整自己的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

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

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

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

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

mVelocityTracker.clear();

}

}

......

//重點在這:-------->自己滑動完之后再調(diào)用dispatchNestedScroll通知父View滑動結(jié)束

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

mLastMotionY -= mScrollOffset[1];

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

mNestedYOffset += mScrollOffset[1];

}

break;

}

接下來又是父View的回調(diào)了批狐,來看看父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滑動完后剩余的距離進行收尾處理,自我調(diào)整后第二輪的嵌套滑動也結(jié)束了前塔。

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

@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時,調(diào)用stopNestedScroll(),這時會通知父View的onStopNestedScroll()來對整個系列的滑動來收尾

public void stopNestedScroll() {

if (mNestedScrollingParent != null) {

ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);

mNestedScrollingParent = null;

}

}

父類最后在自己的onStopNestedScroll()實現(xiàn)相關(guān)的收尾處理华弓,比如重置滑動狀態(tài)標(biāo)記食零,完成動畫操作,通知滑動結(jié)束等寂屏。這樣贰谣,整個滑動嵌套流程就完成了娜搂。

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

步驟一:子View的ACTION_DOWN調(diào)用startNestedScroll—->父View的onStartNestedScroll判斷是否要一起滑動吱抚,父ViewonNestedScrollAccepted同意協(xié)同滑動

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

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啄刹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子凄贩,更是在濱河造成了極大的恐慌鸵膏,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怎炊,死亡現(xiàn)場離奇詭異,居然都是意外死亡廓译,警方通過查閱死者的電腦和手機评肆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來非区,“玉大人瓜挽,你說我怎么就攤上這事≌鞒瘢” “怎么了久橙?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長管怠。 經(jīng)常有香客問我淆衷,道長,這世上最難降的妖魔是什么渤弛? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任祝拯,我火速辦了婚禮,結(jié)果婚禮上她肯,老公的妹妹穿的比我還像新娘佳头。我一直安慰自己,他們只是感情好晴氨,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布康嘉。 她就那樣靜靜地躺著,像睡著了一般籽前。 火紅的嫁衣襯著肌膚如雪亭珍。 梳的紋絲不亂的頭發(fā)上敷钾,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機與錄音块蚌,去河邊找鬼闰非。 笑死,一個胖子當(dāng)著我的面吹牛峭范,可吹牛的內(nèi)容都是我干的财松。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纱控,長吁一口氣:“原來是場噩夢啊……” “哼辆毡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起甜害,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤舶掖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后尔店,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體眨攘,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年嚣州,在試婚紗的時候發(fā)現(xiàn)自己被綠了鲫售。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡该肴,死狀恐怖情竹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匀哄,我是刑警寧澤秦效,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站涎嚼,受9級特大地震影響阱州,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜法梯,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一贡耽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鹊汛,春花似錦蒲赂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至至耻,卻和暖如春若皱,著一層夾襖步出監(jiān)牢的瞬間镊叁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工走触, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晦譬,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓互广,卻偏偏與公主長得像敛腌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子惫皱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容