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/