您的位置:首页 > 移动开发

android5.0协调布局CoordinatorLayout(第一篇CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout之间的关系详解)原理

2017-10-13 15:25 543 查看
首先从协调布局最简单的例子为入口开始分析,由浅到深,看效果图:



此效果如果不用5.0以下的自定义的效果的话,相对麻烦很多,而用5.0的协调布局的话只需要简单的写一个布局文件就搞定了,看布局文件代码

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app1="http://schemas.android.com/apk/res/com.ricky.materialdesign.fab.animation"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.ricky.materialdesign.fab.animation.MainActivity" >

<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<android.support.v7.widget.CardView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_margin="0dp"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:contentPadding="5dp" >

<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/tulips2" />
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_margin="0dp"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:contentPadding="5dp" >

<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/tulips2" />
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_margin="0dp"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:contentPadding="5dp" >

<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/tulips2" />
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_margin="0dp"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:contentPadding="5dp" >

<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/tulips2" />
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_margin="0dp"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:contentPadding="5dp" >

<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/tulips2" />
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_margin="0dp"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:contentPadding="5dp" >

<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/tulips2" />
</android.support.v7.widget.CardView>

<android.support.v7.widget.CardView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_margin="0dp"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:contentPadding="5dp" >

<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/tulips2" />
</android.support.v7.widget.CardView>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="260dp" >

<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:minHeight="200dp"
app1:collapsedTitleGravity="center_horizontal"
app1:contentScrim="@color/mytextcolor"
app1:expandedTitleGravity="center"
app1:expandedTitleMargin="5dp"
app1:statusBarScrim="@color/colorPrimary_pink"
app1:title="6666" >

<!-- 视差值越小滚动越明显 -->

<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.7"
android:scaleType="centerCrop"
android:src="@drawable/tulips2" />

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/abc_ic_ab_back_mtrl_am_alpha" />
</androi
4000
d.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:layout_behavior="com.ricky.materialdesign.fab.animation.FabBehavior"
android:onClick="rotate"
android:src="@drawable/ic_favorite_outline_white_24dp" />

</android.support.design.widget.CoordinatorLayout>从这个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);
}
}

额,这个方法又分三种形式,如果子View设置了app:layout_anchor="@id/toolbar",app:layout_anchorGravity="bottom|right"属性的话,就会走layoutChildWithAnchor(child,
lp.mAnchorView, layoutDirection)布局,这个两个属性的作用就是如果当前控件设置了此属性的话那么它将排列在指定id控件的范围内排版,也就是依赖另一个view排版,做个试验看看效果,现在将悬浮按钮FloatingActionButton依赖于标题进行排版,看效果



可以看到悬浮按钮跑到标题的右下方,但是可以看悬浮按钮的高度好像居中,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;
}


这里最重要的一句话getAnnotation(DefaultBehavior.class),通过反射获取属性,从而为LayoutParams设置Behavior,看一下标题包裹控件
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout
AppBarLayout实现了这个注解,关系一已经找到,CoordinatorLayout通过AppBarLayout的Behavior控制AppBarLayout的移动,从而产生关系,接着看排序

private static void selectionSort(final List<View> list,
final Comparator<View> 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]);
}
}
}

这里用到了自定义Comparator,熟悉java的小伙伴应该对这个类不会陌生,那么看一下它怎么排序的

final Comparator<View> mLayoutDependencyComparator = new Comparator<View>() {
@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;
}
}
};


是不是很晴朗了,如果当前的子view依赖于另一个子View,那么就将它排列到集合的前面,最终先对前面的进行布局,通过分析布局大体上也清楚了CoordinatorLayout和帧布局有点像,不过加了其他强大的规则,yes,那么暂且叫他加强版的帧布局。

既然是帧布局的话,为啥内容布局android.support.v4.widget.NestedScrollView为啥没把标题布局给覆盖掉,或者说刚开始的时候内容布局为啥排在了标题布局下面?

<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" >
xml属性中,咱们可以看到NestedScrollView也声明了一个behavior,这个Behavior是android.support.design.widget.AppBarLayout$ScrollingViewBehavior,AppBarLayout
的一个内部类,猜测一下如果NestedScrollView要放在AppBarLayout紧贴下方的话,8成是ScrollingViewBehavior中做了什么手脚,好带着这个推测,进入源码瞧一瞧
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
// imp
161f3
ortant for things
// like activity transitions which rely on accurate positioning
// after the first layout.
/**
* 如果用了ScrollingViewBehavior的话,此控件将紧贴着appbarlayout
*/

final List<View> 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;
}


ScrollingViewBehavior中的onLayoutChild决定了此控件是否决定自己的布局位置规则,这个方法的核心思想就是获得所有依赖的View,并通过他们的高度与偏移量让子View偏移多少量,这里NestScrollView依赖于AppBarLayout,那么就会根据AppBarLayout的高度和已经偏移了多少量来让NestScrollView最初显示时偏移到AppBarLayout下方,看偏移的方法
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);
}
}
}


果然如所料,如果Behavior的onLayoutChild方法返回true则子view自己确定自己的位置,如果为false则依靠CoordinatorLayout显示它的位置,分析到这里可以看出,如果我们想让一个布局文件一直显示在AppBarLayout下面的话就让它实现Behavior为ScrollingViewBehavior,看名字它还会滚动,怎么滚动的呢?我们将手指放在NestedScrollView范围内移动的话,按道理触摸事件应该被NestedScrollView获得,除非父类拦截,好看看CoordinatorLayout有没有拦截,推测不会拦截,拦截话滑动就不连贯了,带着推测进入
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;
}

看这个方法得知只要intercepted为true的话,那么NestedScrollView的事件将不会被执行,那么接下来进入performIntercept瞧一瞧什么时候会拦截,什么时候不会拦截分发事件
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<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;
}


看中文注释最后是否拦截事件完全交给子view的Behavier去决定是否拦截,那么看一下AppBarLayout的Behavier拦截事件

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;
}

这个方法最核心的判断就是触摸事件是否在AppBarLayout内,建立在这个前提下,又滑动了手机认为的最小距离,那么就会拦截,也就是说,如果手指的触摸在AppBarLayout
内触摸的,那么事件和NestedScrollView半毛钱关系都没有了,直接都不会传到它哪了了,那么再看一下NestedScrollView的Behavier的拦截事件

public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child,
MotionEvent ev) {
return false;
}压根就没复写父类方法,始终返回false,666666既然大NestedScrollView已经实现了那么牛逼的事件处理了,自己拦截自己不是脱裤子放屁找麻烦吗,所以如果没有其他子控件的Behavier拦截的话,手指触摸在NestedScrollView中,事件自动就会被NestedScrollView处理,那么问题又来了,既然事件在标题栏中处理的话,怎么将事件传给NestedScrollView的呢?,好既然你拦截了,我就看看你的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;
}
CoordinatorLayout还是什么事件都不做,还是交给子View的Behavier去处理,相当于它告诉子View你愿怎么滑动就怎么滑动,我不管,这种设计有利于开发者自己实现自己的事件,提高了可扩展性,好继续看AppBarLayout的Ontouch事件
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;
}


代码比较多,这里只看分叉口,当满足滚动时,滚动scroll(parent, child, dy, getMaxDragOffset(child), 0),进入此方法
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;
}

经过一系列运算(具体怎么算的这里不详细阐述),来最终确定需要移动的值,看到这里就更奇怪了,只是appBarLayout移动了,NestedScrollView还在原来的位置,搞毛啊,演示效果明明会跟着一块移动。那么继续找,观看文档可知Behavier有这么一个方法
public boolean onDependentViewChanged(CoordinatorLayout parent,
View child, View dependency) {
updateOffset(parent, child, dependency);
return false;
}这个方法就是在此View所依赖的view发生改变的时候回调此方法,什么改变,当然是位置,显示隐藏等就会回调此方法,那么巧了,此时NestedScrollView就是观察者,appBarLayout是被观察者,appBarLayout移动一点就会通知NestedScrollView,然后NestedScrollView也改变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);
}
}
}
}
}

代码较多,但是还是能一眼发现重点,b.onDependentViewChanged(this,checkChild, child),哎要不错奥,看到这已经知道AppbarLayout在移动时怎么将移动通知给依赖它的View了,很明显,根据触摸手势不断的改变AppbarLayout的距离,然后会引起ViewTree树的重画,然后通过重画之前的回调事件通知依赖它的子View,从而实现级联移动的效果。AppBarLayout怎么影响NestedScrollView介绍完了,接下来介绍NestedScrollView怎么将事件朝上传递并影响AppBarLayout的。
public class CoordinatorLayout extends ViewGroup implements
NestedScrollingParent
看的出来CoordinatorLayout
实现了NestedScrollingParent接口,是不是很熟悉,不熟悉的童鞋可以查一下NestedScrollingChild和NestedScrollingParent用法,既然事件先是在NestedScrollView传递的,那么进入这个类看看它的Ontouch事件

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;
}它在这个方法里获得,NestedScrollView获取实现了NestedScrollingParent接口的父View,也就是我们的协调布局,最后通过接口将滑动的事件传给CoordinatorLayout的,然后CoordinatorLayout处理滑动的部分距离或者全部或者不处理再把剩下的距离交给NestedScrollView进行最后的滑动,从而实现了事件从NestedScrollView>CoordinatorLayout的事件传输,那么是不是CoordinatorLayout又通过Behavier将事件传给子View最终实现联动呢?答案是肯定的,不信?那么再次进入
CoordinatorLayout求证这个推论

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);
}
}
是不是和我们的推论一样,首先滑动NestedScrollView的时候,通过NestedScrollingParent接口将事件传给协调布局,然后协调布局再通过在View的Behavier,交给AppBarLayout来处理,比如向上滑动时,AppBarLayout根据属性还没有滑动到边界的话,那么AppBarLayout完全消耗掉滑动事件,然后告诉NestedScrollView的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改变位置时会调用dispatchOffsetUpdates,看看它都干了啥?

/**
* 通知注册了AppBarLayout接口
*
* @param layout
*/
private void dispatchOffsetUpdates(AppBarLayout layout) {
final List<OnOffsetChangedListener> 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());
}
}
}


吼吼吼,回调有木有,那么既然有回调那么在CollapsingToolbarLayout注册这个接口不就能收到AppBarLayout改变了吗?66666,找一下在哪注册的
@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进行相应的控件属性处理。好了,本篇就分析到这里
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐