首先从协调布局最简单的例子为入口开始分析,由浅到深,看效果图:
此效果如果不用5.0以下的自定义的效果的话,相对麻烦很多,而用5.0的协调布局的话只需要简单的写一个布局文件就搞定了,看布局文件代码
从这个xml文件得知,此布局大约可以分成三部分
也就是说CoordinatorLayout 有三个直接子孩子,我们知道,以前我们常用的布局有线性布局、帧布局、相对布局、等分比布局等,其中的控件摆在那个位置,根据属性一看就会知道摆放的位置,那么CoordinatorLayout 到底是怎么摆放的呢?以及为什么内容布局会在标题布局的下面呢?带着问题点进源码
public class CoordinatorLayout extends ViewGroup implements
NestedScrollingParent
CoordinatorLayout继承于ViewGroup,它没有继承我们常用的布局方式,这就有点坑了,那么既然直接继承了ViewGroup的话,根据布局规则那么自己实现OnLayout方法对子控件进行排序,好看一下这个方法
public void onLayoutChild(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.checkAnchorChanged()) {
throw new IllegalStateException(
"An anchor may not be changed after CoordinatorLayout"
+ " measurement begins before layout is complete.");
}
if (lp.mAnchorView != null) {
// 有参照物情况下的布局
layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
}
// 假如keyline不为空则以它keyline为依据布局子View
else if (lp.keyline >= 0) {
layoutChildWithKeyline(child, lp.keyline, layoutDirection);
}
// 没有设置参照物或参照线的情况下的普通布局
else {
layoutChild(child, layoutDirection);
}
}
可以看到悬浮按钮跑到标题的右下方,但是可以看悬浮按钮的高度好像居中,why?带着疑问进入源码依赖布局的源码看一下
private void layoutChildWithAnchor(View child, View anchor,
int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect anchorRect = mTempRect1;
final Rect childRect = mTempRect2;
// 得到参照物view的位置,child将放在anchor上
getDescendantRect(anchor, anchorRect);
getDesiredAnchoredChildRect(child, layoutDirection, anchorRect,
childRect);
child.layout(childRect.left, childRect.top, childRect.right,
childRect.bottom);
}
在真正布局子view之前,还需要排序一下直接子View,也就是说,真正布局之前不是根据xml的树形结构来排版的,而是根据
Behavior行为依赖来排版的,
Behavior依赖可以让我们的当前view根据另一个view的移动而移动,也可以自己拥有自己的
Touch事件。好进入
getDesiredAnchoredChildRect方法,计算当前子view的位置坐标
void getDesiredAnchoredChildRect(View child, int layoutDirection,
Rect anchorRect, Rect out) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//absGravity默认为center
final int absGravity = GravityCompat.getAbsoluteGravity(
resolveAnchoredChildGravity(lp.gravity), layoutDirection);
//设置了依赖view的排版属性
final int absAnchorGravity = GravityCompat.getAbsoluteGravity(
resolveGravity(lp.anchorGravity), layoutDirection);
final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int anchorHgrav = absAnchorGravity
& Gravity.HORIZONTAL_GRAVITY_MASK;
final int anchorVgrav = absAnchorGravity
& Gravity.VERTICAL_GRAVITY_MASK;
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
int left;
int top;
// Align to the anchor. This puts us in an assumed right/bottom child
// view gravity.
// If this is not the case we will subtract out the appropriate portion
// of
// the child size below.
switch (anchorHgrav) {
default:
case Gravity.LEFT:
left = anchorRect.left;
break;
case Gravity.RIGHT:
left = anchorRect.right;
break;
case Gravity.CENTER_HORIZONTAL:
left = anchorRect.left + anchorRect.width() / 2;
break;
}
switch (anchorVgrav) {
default:
case Gravity.TOP:
top = anchorRect.top;
break;
case Gravity.BOTTOM:
top = anchorRect.bottom;
break;
case Gravity.CENTER_VERTICAL:
top = anchorRect.top + anchorRect.height() / 2;
break;
}
// Offset by the child view's gravity itself. The above assumed
// right/bottom gravity.
switch (hgrav) {
default:
case Gravity.LEFT:
left -= childWidth;
break;
case Gravity.RIGHT:
// Do nothing, we're already in position.
break;
case Gravity.CENTER_HORIZONTAL:
left -= childWidth / 2;
break;
}
switch (vgrav) {
default:
case Gravity.TOP:
top -= childHeight;
break;
case Gravity.BOTTOM:
// Do nothing, we're already in position.
break;
case Gravity.CENTER_VERTICAL:
top -= childHeight / 2;
break;
}
final int width = getWidth();
final int height = getHeight();
// Obey margins and padding
// 为子view留足够的空间
left = Math.max(
getPaddingLeft() + lp.leftMargin,
Math.min(left, width - getPaddingRight() - childWidth
- lp.rightMargin));
top = Math.max(
getPaddingTop() + lp.topMargin,
Math.min(top, height - getPaddingBottom() - childHeight
- lp.bottomMargin));
out.set(left, top, left + childWidth, top + childHeight);
}
xml中,我们设置了左右坐标为right,那么此时悬浮按钮的坐标left=ToolBar的right,同理top为ToolBar的bottom,xml文件里没有设置
gravity
属性,默认就为center,这个我怎么知道的,俗话说没有代码就没有真相看代码
/**
* 默认放到参照物中间
*
* @param gravity
* @return
*/
private static int resolveAnchoredChildGravity(int gravity) {
return gravity == Gravity.NO_GRAVITY ? Gravity.CENTER : gravity;
}
this.gravity = a
.getInteger(
R.styleable.CoordinatorLayout_LayoutParams_android_layout_gravity,
Gravity.NO_GRAVITY);
如果没有设置这个属性,那么就会默认
NO_GRAVITY,那么最终悬浮按钮的gravity就是center,那么继续回到前面计算,都默认center了
left -= childWidth / 2,
top -= childHeight / 2,
这也就是为什么我们的悬浮按钮的顶部上在ToolBar的中间了,最后再计算是否为悬浮按钮留在屏幕上有足够的显示空间,当然,你都不在屏幕上了,我还计算它干嘛。上面提到根据依赖,对view树的先排列进行重新排序,在哪实现的呢?
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//对view树进行重新排序,进行layout的时,进行新view树的layout
prepareChildren();
计算子View大小的时候在此方法调用了
prepareChildren()
进行重新排序,look源码
/**
* 初始化child,为他们添加依赖排序
*/
private void prepareChildren() {
mDependencySortedChildren.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View child = getChildAt(i);
// 为子类准备注解的behavier
final LayoutParams lp = getResolvedLayoutParams(child);
lp.findAnchorView(this, child);
// 加入到依赖集合中
mDependencySortedChildren.add(child);
}
// We need to use a selection sort here to make sure that every item is
// compared
// against each other
selectionSort(mDependencySortedChildren, mLayoutDependencyComparator);
}
看注解,为子类准备注解的
Behavier,什么鬼?
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
Class> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
while (childClass != null
&& (defaultBehavior = childClass
.getAnnotation(DefaultBehavior.class)) == null) {
childClass = childClass.getSuperclass();
}
if (defaultBehavior != null) {
try {
result.setBehavior(defaultBehavior.value().newInstance());
} catch (Exception e) {
Log.e(TAG,
"Default behavior class "
+ defaultBehavior.value().getName()
+ " could not be instantiated. Did you forget a default constructor?",
e);
}
}
result.mBehaviorResolved = true;
}
return result;
}
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout
private static void selectionSort(final List list,
final Comparator comparator) {
if (list == null || list.size() < 2) {
return;
}
final View[] array = new View[list.size()];
list.toArray(array);
final int count = array.length;
for (int i = 0; i < count; i++) {
int min = i;
for (int j = i + 1; j < count; j++) {
// 第一个view与后面的view依次做比较
if (comparator.compare(array[j], array[min]) < 0) {
min = j;
}
}
if (i != min) {
// We have a different min so swap the items
// 调换位置,小的放前面
final View minItem = array[min];
array[min] = array[i];
array[i] = minItem;
}
}
// Finally add the array back into the collection
list.clear();
for (int i = 0; i < count; i++) {
list.add(array[i]);
}
}
}
final Comparator mLayoutDependencyComparator = new Comparator() {
@Override
public int compare(View lhs, View rhs) {
// 对象完全一样返回相等
if (lhs == rhs) {
return 0;
}
// 假如当前的view依赖后面的view,那么后面的view放在集合的前面
else if (((LayoutParams) lhs.getLayoutParams()).dependsOn(
CoordinatorLayout.this, lhs, rhs)) {
return 1;
// 被依赖的view放前面
} else if (((LayoutParams) rhs.getLayoutParams()).dependsOn(
CoordinatorLayout.this, rhs, lhs)) {
return -1;
} else {
return 0;
}
}
};
xml属性中,咱们可以看到
NestedScrollView也声明了一个
behavior,
这个
Behavior
是android.support.design.widget.AppBarLayout$ScrollingViewBehavior,AppBarLayout
public boolean onLayoutChild(CoordinatorLayout parent, View child,
int layoutDirection) {
// First lay out the child as normal
super.onLayoutChild(parent, child, layoutDirection);
// Now offset us correctly to be in the correct position. This is
// important for things
// like activity transitions which rely on accurate positioning
// after the first layout.
/**
* 如果用了ScrollingViewBehavior的话,此控件将紧贴着appbarlayout
*/
final List dependencies = parent.getDependencies(child);
for (int i = 0, z = dependencies.size(); i < z; i++) {
if (updateOffset(parent, child, dependencies.get(i))) {
// If we updated the offset, break out of the loop now
break;
}
}
return true;
}
private boolean updateOffset(CoordinatorLayout parent, View child,
View dependency) {
final CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) dependency
.getLayoutParams()).getBehavior();
if (behavior instanceof Behavior) {
// Offset the child so that it is below the app-bar (with any
// overlap)
final int offset = ((Behavior) behavior)
.getTopBottomOffsetForScrollingSibling();
//设置子控件的偏移位置
setTopAndBottomOffset(dependency.getHeight() + offset
- getOverlapForOffset(dependency, offset));
return true;
}
return false;
}
这里又有一个疑问了,
onLayoutChild方法什么时候被调用的呢?是不是
CoordinatorLayout中的
Onlayout方法呢?
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
/**
* 注册behavior的自己实现布局,behavior.onLayoutChild如果返回为true的话
*/
if (behavior == null
|| !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
onInterceptTouchEvent的方法,对分发事件不是很了解的小伙伴请自行脑补
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors();
}
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (cancelEvent != null) {
cancelEvent.recycle();
}
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors();
}
return intercepted;
}
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
final List 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;
}
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 (MotionEventCompat.getActionMasked(ev)) {
case MotionEvent.ACTION_DOWN: {
mIsBeingDragged = false;
final int x = (int) ev.getX();
final int y = (int) ev.getY();
//最关键的部分判断触摸事件是否在AppBarLayout控件范围内,并且
if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {
mLastMotionY = y;
mActivePointerId = MotionEventCompat.getPointerId(ev, 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 = MotionEventCompat.findPointerIndex(ev, activePointerId);
if (pointerIndex == -1) {
break;
}
final int y = (int) MotionEventCompat.getY(ev, 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;
}
内触摸的,那么事件和NestedScrollView半毛钱关系都没有了,直接都不会传到它哪了了,那么再看一下NestedScrollView的Behavier的拦截事件
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child,
MotionEvent ev) {
return false;
}
压根就没复写父类方法,始终返回false,666666既然大
NestedScrollView已经实现了那么牛逼的事件处理了,自己拦截自己不是脱裤子放屁找麻烦吗,所以如果没有其他子控件的Behavier拦截的话,手指触摸在
NestedScrollVie
w中,事件自动就会被
NestedScrollVie
w处理,那么问题又来了,既然事件在标题栏中处理的话,怎么将事件传给
NestedScrollVie
w的呢?,好既然你拦截了,我就看看你的Ontouch事件的具体处理方法
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
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();
}
return handled;
}
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
if (mTouchSlop < 0) {
mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
switch (MotionEventCompat.getActionMasked(ev)) {
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 = MotionEventCompat.getPointerId(ev, 0);
ensureVelocityTracker();
} else {
return false;
}
break;
}
case MotionEvent.ACTION_MOVE: {
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
mActivePointerId);
if (activePointerIndex == -1) {
return false;
}
final int y = (int) MotionEventCompat.getY(ev, 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 = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
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;
}
final int scroll(CoordinatorLayout coordinatorLayout, V header,
int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(coordinatorLayout, header,
getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
}
看起来很熟悉,最终改变
View的
top或
bottom
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset,
int minOffset, int maxOffset) {
final int curOffset = getTopAndBottomOffset();
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.constrain(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
setTopAndBottomOffset(newOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
}
}
return consumed;
}
public boolean onDependentViewChanged(CoordinatorLayout parent,
View child, View dependency) {
updateOffset(parent, child, dependency);
return false;
}
这个方法就是在此View所依赖的view发生改变的时候回调此方法,什么改变,当然是位置,显示隐藏等就会回调此方法,那么巧了,此时
NestedScrollVie
w就是观察者,appBarLayout是被观察者,
appBarLayout移动一点就会通知
NestedScrollVie
w,然后
NestedScrollVie
w也改变
top或
bottom,问题又来了
onDependentViewChanged具体回调
是发生在什么地方?带着疑问接着深入
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors();
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets
// yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
在这个方法里,发现端倪,既然子View的位置改变了,那么肯定会引起view树的重画,那么重画之前,就会回调OnPreDrawListener方法,看看它都干了什么!
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
Log.i("huoying", "OnPreDrawListener");
dispatchOnDependentViewChanged(false);
return true;
}
}
哎要不错奥,通知某个
View改变了,将消息发给依赖它改变的某个
View 的
Behavier,从而实现联动!
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Check child views before for anchor
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
// Did it change? if not continue
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
recordLastChildRect(child, newRect);
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild
.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (!fromNestedScroll
&& checkLp.getChangedAfterNestedScroll()) {
// If this is not from a nested scroll and we have
// already been changed
// from a nested scroll, skip the dispatch and reset the
// flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
//通知依赖它的View
final boolean handled = b.onDependentViewChanged(this,
checkChild, child);
if (fromNestedScroll) {
// If this is from a nested scroll, set the flag so that
// we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
}
public class CoordinatorLayout extends ViewGroup implements
NestedScrollingParent
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = MotionEventCompat.getActionMasked(ev);
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
//调用滑动开始
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
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];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
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.
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
//把偏移量交给父View处理部分,然后处理余下的部分
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
ensureGlows();
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
MotionEventCompat.getX(ev, activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - MotionEventCompat.getX(ev, activePointerIndex)
/ getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
//交给fuView飞一会
flingWithNestedDispatch(-initialVelocity);
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
mLastMotionY = (int) MotionEventCompat.getY(ev, index);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) MotionEventCompat.getY(ev,
MotionEventCompat.findPointerIndex(ev, mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
代码比较多,大家只看关键注释的位置,快速滑动和滑动差不多,这里只看滑动部分dispatchNestedScroll方法
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
//这里有个mNestedScrollingParent很重要
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
这个方法有个
mNestedScrollingParent,事件传给父View全靠它
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
它在这个方法里获得,
NestedScrollVie
w获取实现了
NestedScrollingParent接口的父
View,也就是我们的协调布局,最后通过接口将滑动的事件传给
CoordinatorLayout的,然后
CoordinatorLayout处理滑动的部分距离或者全部或者不处理再把剩下的距离交给
NestedScrollVie
w进行最后的滑动,从而实现了事件从
NestedScrollVie
w>
CoordinatorLayout的事件传输,那么是不是
CoordinatorLayout又通过Behavier将事件传给子
View最终实现联动呢?答案是肯定的,不信?那么再次进入
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//看这里又将事件传给ziView的Behavier消耗了
viewBehavior.onNestedScroll(this, view, target, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
accepted = true;
}
}
if (accepted) {
//通知依赖他的view我改变了
dispatchOnDependentViewChanged(true);
}
}
是不是和我们的推论一样,首先滑动
NestedScrollVie
w的时候,通过
NestedScrollingParent接口将事件传给协调布局,然后协调布局再通过在View的Behavier,交给
AppBarLayout来处理,比如向上滑动时,
AppBarLayout根据属性还没有滑动到边界的话,那么
AppBarLayout完全消耗掉滑动事件,然后告诉
NestedScrollVie
w的Behavier我改变了,你也改变吧,从而实现两个view紧紧的联系在一块,这就是
CoordinatorLayout和
AppBarLayout和实现了
NestedScrollingChild接口的滑动
View之间的关系了,最后还剩折叠布局
CollapsingToolbarLayout是怎么获取变化通知,形成视差移动,或者折叠效果的,通过xml可以
CollapsingToolbarLayout是
AppBarLayout的子类
public class AppBarLayout extends LinearLayout {
public AppBarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(VERTICAL);
从上边两行源码可知
AppBarLayout
就是竖向的线性布局,既然它都是通过Behavier运动的,那么必然Behavier里有通知CollapsingToolbarLayout的方式,好找起来
int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
AppBarLayout header, int newOffset, int minOffset, int maxOffset) {
此处省略几十行............
dispatchOffsetUpdates(appBarLayout);
}
}
return consumed;
}
/**
* 通知注册了AppBarLayout接口
*
* @param layout
*/
private void dispatchOffsetUpdates(AppBarLayout layout) {
final List listeners = layout.mListeners;
// Iterate backwards through the list so that most recently added
// listeners
// get the first chance to decide
for (int i = 0, z = listeners.size(); i < z; i++) {
final OnOffsetChangedListener listener = listeners.get(i);
if (listener != null) {
listener.onOffsetChanged(layout, getTopAndBottomOffset());
}
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// Add an OnOffsetChangedListener if possible
final ViewParent parent = getParent();
if (parent instanceof AppBarLayout) {
if (mOnOffsetChangedListener == null) {
mOnOffsetChangedListener = new OffsetUpdateListener();
}
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
}
}
private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
@Override
public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
mCurrentOffset = verticalOffset;
final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
final int scrollRange = layout.getTotalScrollRange();
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
switch (lp.mCollapseMode) {
case LayoutParams.COLLAPSE_MODE_PIN:
if (getHeight() - insetTop + verticalOffset >= child.getHeight()) {
offsetHelper.setTopAndBottomOffset(-verticalOffset);
}
break;
case LayoutParams.COLLAPSE_MODE_PARALLAX:
offsetHelper.setTopAndBottomOffset(
Math.round(-verticalOffset * lp.mParallaxMult));
break;
}
}
最终通过回调方法实现折叠布局的移动了,内部控件的透明度改变了,改变移动的速率从而实现视差效果,到此三者之间的关系已经很明了了,
AppBarLayout通过
Behaiver接收
CoordinatorLayout传过来的事件并进行移动,每次变化通过注册的接口回调给
CollapsingToolbarLayout进行相应的控件属性处理。好了,本篇就分析到这里