继上文说了CoordinatorLayout其子view的measure和layout后,要探究其最重要的滑动机制了,我们都知道在appbarlayout的子view设置了scroll_flag为scroll属性时会出现滑动现象那首先提一个问题:
1.为何触摸到appBarlayout时,nestScrollview会跟着appBarLayout就行滑动
这两个问题如果在linearlayout里当然用不着问,但是细细一想在coordinatorLayout里nestScrollview在appBarlayout下方本来就是onlayout里设置的,那滑动肯定的话肯定也做了特殊的处理,当然牵扯到滑动就要进行事件分发了,不了解的朋友可以看下事件分发之结论篇。当然默认先从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;
}
很明显重点在于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然后进行循环,对mBehaviorTouchView就行了赋值也就是这个coordinatorLayout的事件交给了哪个子view的behavior进行处理,先来看下nestScrollview处理了没有,很明显在其headScrollbehavior里没有对其进行复写,也就说取得就是默认值也就是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的时候coordinatorLayout和appBarlayout都是返回的false,也就是说是交给appBarlayout的ontouchEvent处理的,然后在move的时候,如果当手指落在appBarlayout范围内且移动了,appBarlayout的拦截事件返回了true,那也就是说此时coordinatorLayout的拦截事件也返回了true,并会给appBarlayout的onTouchEvent事件发送一个cancel事件,就表明此事件要自己处理了,也就是说当在move的时候会调用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,进入这个if语句,发现其还是交给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的时候简单来说就调用了scroll方法最终也是调用了其
@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;
}
这个consumed也就是相对于每次滑动多少距离,当然向下滑动的话
consumed是>0的,向上反之。
setTopAndBottomOffset方法最终也是调用了ViewOffsetHelper的updateOffsets
private void updateOffsets() {
ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));
ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
}
可以看到并不是通过scroller滑动的,直接的改变了位置,而mLayoutTop就是我们一开始传进来初始的appBarlayout的位置
那我们分析到现在,也没有找到问题一的答案,为何appBarlayout滑动了,nestScrollview会滑动?
其实啊当调用ViewCompat.offsetTopAndBottom方法后,大家可以看下其源码会进行一次重绘,这并不会调用次view的draw方法,因为源码里其设置的invalidateCache参数是false,也就是说少了一个标志,
但是对整个view树会执行performTraversals方法,那有啥监听可以监听到么,其实是有的也就是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 {
}
也就是在执行performDraw的上面会执行dispatchOnPreDraw也就是会调用onPreDraw的监听,我们再来看下coordinatorLayout的onpreDraw
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
很明显又回到了这里,也就是第一篇文章分析的那里,其实又回调了onDependentViewChanged方法,这才导致了nestScrollview的滑动
也就真相大白了,想不到一个简单的滑动在coordinatorLayout竟如此绕,这也是它可以定制化的魅力所在吧
网友评论