繼上文說了CoordinatorLayout其子view的measure和layout后挫掏,要探究其最重要的滑動(dòng)機(jī)制了,我們都知道在appbarlayout的子view設(shè)置了scroll_flag為scroll屬性時(shí)會(huì)出現(xiàn)滑動(dòng)現(xiàn)象那首先提一個(gè)問題:
1.為何觸摸到appBarlayout時(shí),nestScrollview會(huì)跟著appBarLayout就行滑動(dòng)
這兩個(gè)問題如果在linearlayout里當(dāng)然用不著問巩割,但是細(xì)細(xì)一想在coordinatorLayout里nestScrollview在appBarlayout下方本來就是onlayout里設(shè)置的,那滑動(dòng)肯定的話肯定也做了特殊的處理,當(dāng)然牽扯到滑動(dòng)就要進(jìn)行事件分發(fā)了焕窝,不了解的朋友可以看下事件分發(fā)之結(jié)論篇嫉戚。當(dāng)然默認(rèn)先從coordinatorLayout的onInterceptTouchEvent說起:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors(true);
}
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(true);
}
return intercepted;
}
很明顯重點(diǎn)在于performIntercept方法刨裆,讓我們來看一下
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
final List<View> topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
}
這里獲取到topmostChildList然后進(jìn)行循環(huán),對(duì)mBehaviorTouchView就行了賦值也就是這個(gè)coordinatorLayout的事件交給了哪個(gè)子view的behavior進(jìn)行處理彬檀,先來看下nestScrollview處理了沒有,很明顯在其headScrollbehavior里沒有對(duì)其進(jìn)行復(fù)寫帆啃,也就說取得就是默認(rèn)值也就是false,然后看下appBarlayout的headBehavior
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
if (mTouchSlop < 0) {
mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
final int action = ev.getAction();
// Shortcut since we're being dragged
if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
return true;
}
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
mIsBeingDragged = false;
final int x = (int) ev.getX();
final int y = (int) ev.getY();
if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
ensureVelocityTracker();
}
break;
}
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY = y;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
}
return mIsBeingDragged;
}
這里我們看到在down的時(shí)候coordinatorLayout和appBarlayout都是返回的false,也就是說是交給appBarlayout的ontouchEvent處理的窍帝,然后在move的時(shí)候努潘,如果當(dāng)手指落在appBarlayout范圍內(nèi)且移動(dòng)了,appBarlayout的攔截事件返回了true坤学,那也就是說此時(shí)coordinatorLayout的攔截事件也返回了true疯坤,并會(huì)給appBarlayout的onTouchEvent事件發(fā)送一個(gè)cancel事件,就表明此事件要自己處理了深浮,也就是說當(dāng)在move的時(shí)候會(huì)調(diào)用coordinatorLayout的ontouchevent方法压怠,我們來看下
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// Safe since performIntercept guarantees that
// mBehaviorTouchView != null if it returns true
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
// Keep the super implementation correct
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
} else if (cancelSuper) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
super.onTouchEvent(cancelEvent);
}
if (!handled && action == MotionEvent.ACTION_DOWN) {
}
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(false);
}
return handled;
}
這里mBehaviorTouchView肯定是有的,也就是appBarlayout,進(jìn)入這個(gè)if語句飞苇,發(fā)現(xiàn)其還是交給appBarlayout的onTouchEvent處理的
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
if (mTouchSlop < 0) {
mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
final int x = (int) ev.getX();
final int y = (int) ev.getY();
if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
ensureVelocityTracker();
} else {
return false;
}
break;
}
case MotionEvent.ACTION_MOVE: {
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
return false;
}
final int y = (int) ev.getY(activePointerIndex);
int dy = mLastMotionY - y;
if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
mIsBeingDragged = true;
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
}
if (mIsBeingDragged) {
mLastMotionY = y;
// We're being dragged so scroll the ABL
scroll(parent, child, dy, getMaxDragOffset(child), 0);
}
break;
}
case MotionEvent.ACTION_UP:
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
}
// $FALLTHROUGH
case MotionEvent.ACTION_CANCEL: {
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
}
return true;
}
到這里相信大家能叫看出來了菌瘫,move的時(shí)候簡單來說就調(diào)用了scroll方法最終也是調(diào)用了其
@Override
int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
final int curOffset = getTopBottomOffsetForScrollingSibling();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
// If we have some scrolling range, and we're currently within the min and max
// offsets, calculate a new offset
newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
? interpolateOffset(appBarLayout, newOffset)
: newOffset;
final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
// Update the stored sibling offset
mOffsetDelta = newOffset - interpolatedOffset;
if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
// If the offset hasn't changed and we're using an interpolated scroll
// then we need to keep any dependent views updated. CoL will do this for
// us when we move, but we need to do it manually when we don't (as an
// interpolated scroll may finish early).
coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
}
// Dispatch the updates to any listeners
appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());
// Update the AppBarLayout's drawable state (for any elevation changes)
updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
newOffset < curOffset ? -1 : 1, false);
}
} else {
// Reset the offset delta
mOffsetDelta = 0;
}
return consumed;
}
這個(gè)consumed也就是相對(duì)于每次滑動(dòng)多少距離,當(dāng)然向下滑動(dòng)的話
consumed是>0的,向上反之布卡。
setTopAndBottomOffset方法最終也是調(diào)用了ViewOffsetHelper的updateOffsets
private void updateOffsets() {
ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));
ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
}
可以看到并不是通過scroller滑動(dòng)的雨让,直接的改變了位置,而mLayoutTop就是我們一開始傳進(jìn)來初始的appBarlayout的位置
那我們分析到現(xiàn)在羽利,也沒有找到問題一的答案宫患,為何appBarlayout滑動(dòng)了,nestScrollview會(huì)滑動(dòng)?
其實(shí)啊當(dāng)調(diào)用ViewCompat.offsetTopAndBottom方法后娃闲,大家可以看下其源碼會(huì)進(jìn)行一次重繪虚汛,這并不會(huì)調(diào)用次view的draw方法,因?yàn)樵创a里其設(shè)置的invalidateCache參數(shù)是false皇帮,也就是說少了一個(gè)標(biāo)志卷哩,
但是對(duì)整個(gè)view樹會(huì)執(zhí)行performTraversals方法,那有啥監(jiān)聽可以監(jiān)聽到么,其實(shí)是有的也就是preDrawListener
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
}
也就是在執(zhí)行performDraw的上面會(huì)執(zhí)行dispatchOnPreDraw也就是會(huì)調(diào)用onPreDraw的監(jiān)聽属拾,我們?cè)賮砜聪耤oordinatorLayout的onpreDraw
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
很明顯又回到了這里将谊,也就是第一篇文章分析的那里,其實(shí)又回調(diào)了onDependentViewChanged方法渐白,這才導(dǎo)致了nestScrollview的滑動(dòng)
也就真相大白了尊浓,想不到一個(gè)簡單的滑動(dòng)在coordinatorLayout竟如此繞,這也是它可以定制化的魅力所在吧