Android CoordinatorLayout源码分析
burnny0a7q
8年前
<p>CoordinatorLayout作为协调布局,而真正实现功能的部分在于Behavior,所以我打算将这两地方都捎带说说,若有意见请及时提出帮助我改正</p> <h2><strong>Behavior的初始化</strong></h2> <p>Behavior是CoordinatorLayout内部静态抽象类,它是一种新的view关系描述,即依赖关系。一般我们都是继承这个类去完成自己的自定义功能</p> <p>之前我们提及Behavior可以通过注解或者layout_behavior来声明,如果你是通过xml来初始化,那么在CoordinatorLayout初始化的时候就完成了</p> <pre> <code class="language-java">public static class LayoutParams extends ViewGroup.MarginLayoutParams { LayoutParams(Context context, AttributeSet attrs) { mBehaviorResolved = a.hasValue( R.styleable.CoordinatorLayout_LayoutParams_layout_behavior); if (mBehaviorResolved) { mBehavior = parseBehavior(context, attrs, a.getString( R.styleable.CoordinatorLayout_Layout_layout_behavior));} } }</code></pre> <p>如果你是使用注解进行初始化,那么他在onMeasure的时候通过prepareChildren才进行初始化,注意看setBehavior这里。所以xml里初始化优先级高。xml内指定的话,是在inflate的时候对mBehavior赋值;注解里指定的话,是在onMeasure内赋值,稍有不同。</p> <pre> <code class="language-java">protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { prepareChildren(); .... } 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; }</code></pre> <p>前面我们提及反射初始化Behavior的,在这个parseBehavior里面就能看到</p> <pre> <code class="language-java">static Behavior parseBehavior(Context context, AttributeSet attrs, String name) { try { Map<String, Constructor<Behavior>> constructors = sConstructors.get(); if (constructors == null) { constructors = new HashMap<>(); sConstructors.set(constructors); } Constructor<Behavior> c = constructors.get(fullName); if (c == null) { final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true, context.getClassLoader()); c = clazz.getConstructor(CONSTRUCTOR_PARAMS); c.setAccessible(true); constructors.put(fullName, c); } return c.newInstance(context, attrs); } catch (Exception e) { throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e); } }</code></pre> <p>还有一个需要注意的地方,我们看到反射的方法是2个参数的构造方法</p> <pre> <code class="language-java">static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] { Context.class, AttributeSet.class };</code></pre> <p>所以我们在自定义Behavior的时候,一定要去重写</p> <h2><strong>NestedScrolling概念</strong></h2> <p>其实想说一下为什么叫嵌套滑动,之前我们老是提及这个概念。CoordinatorLayout本身是不能动的,但是一旦其中包含了具备NestedScrolling功能的滚动视图,那就不一样了。它在滑动过程中会对Behavior产生影响,进而可以通过动画或者View之间的关联关系进行改变。这里,就是有嵌套这么一层关系</p> <p>之前那种TouchEvent形式的滑动方式,一旦子View拦截了事件,除非重新进行一次事件传递,不然父View是拿不到事件的。而NestedScrolling很好的解决了这个问题</p> <p>在阅读源码的时候,请着重关注这4个类</p> <ol> <li><a href="/misc/goto?guid=4959716612790314110" rel="nofollow,noindex">NestedScrollingChild</a> :<br> 如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口</li> <li><a href="/misc/goto?guid=4959716612876627334" rel="nofollow,noindex">NestedScrollingParent</a> :<br> 作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现 NestedScrollingParent接口,这个接口方法和 NestedScrollingChild大致有一一对应的关系</li> <li><a href="/misc/goto?guid=4959716612962082367" rel="nofollow,noindex">NestedScrollingChildHelper</a><br> 实现好了 Child 和 Parent 交互的逻辑</li> <li><a href="/misc/goto?guid=4959716613071063324" rel="nofollow,noindex">NestedScrollingParentHelper</a><br> 实现好了 Child 和 Parent 交互的逻辑</li> </ol> <h2><strong>NestedScrolling滑动机制流程</strong></h2> <p>完整的事件流程大致是这样的:</p> <p>滑动开始的调用startNestedScroll(),Parent收到onStartNestedScroll()回调,决定是否需要配合Child一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted()。每次滑动前,Child 先询问Parent是否需要滑动,即 dispatchNestedPreScroll(),这就回调到Parent的onNestedPreScroll(),Parent可以在这个回调中“劫持”掉Child的滑动,也就是先于Child滑动。Child滑动以后,会调用onNestedScroll(),回调到Parent的onNestedScroll()。最后滑动结束,调用 onStopNestedScroll()表示本次处理结束。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/06dedf232e23fdde5d457a8414010576.png"></p> <p style="text-align:center">NestedScrollingChild与NestedScrollingChildHelper的交互流程</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/f01bffb7fa0a8e12996395c7243039b1.png"></p> <p style="text-align:center">NestedScrollingChildHelper与ViewParentCompat的交互流程</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d8234be21063f0c064480347c1c694f2.png"></p> <p style="text-align:center">ViewParentCompat与CoordinatorLayout的交互流程</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b931f562f9b211d0a16f7fa1dbaee584.png"></p> <p style="text-align:center">CoordinatorLayout与Behavior的交互流程</p> <h2><strong>主要回调方法介绍</strong></h2> <ul> <li> <p><strong>onStartNestedScroll</strong></p> </li> </ul> <p>在NestedScrollView的ACTION_DOWN事件中开始流程</p> <pre> <code class="language-java">startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);</code></pre> <p>NestedScrollingChildHelper里循环查找直到找出CoordinatorLayout,继续发送</p> <pre> <code class="language-java">public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { 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; }</code></pre> <p>ViewParentCompat里面,parent只要实现了onStartNestedScroll就可以继续流程,这里也是说添加Behavior的控件必须直接从属于CoordinatorLayout,否则没有效果</p> <pre> <code class="language-java">public static boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes) { try { return parent.onStartNestedScroll(child, target, nestedScrollAxes); } catch (AbstractMethodError e) { Log.e(TAG, "ViewParent " + parent + " does not implement interface " + "method onStartNestedScroll", e); return false; } }</code></pre> <p>CoordinatorLayout循环通知所有第一层子视图中的Behavior</p> <pre> <code class="language-java">public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { boolean handled = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View view = getChildAt(i); final LayoutParams lp = (LayoutParams) view.getLayoutParams(); final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target, nestedScrollAxes); handled |= accepted; lp.acceptNestedScroll(accepted); } else { lp.acceptNestedScroll(false); } } return handled; }</code></pre> <p>它的返回值,决定了NestedScrollingChildHelper.onStartNestedScroll是不是要继续遍历,如果我们的Behavior对这个滑动感兴趣,就返回true,它的遍历就会结束掉。</p> <ul> <li> <p><strong>onNestedPreScroll</strong></p> </li> </ul> <p>在ACTION_MOVE中进行触发传递,注意这边的deltaY是已经计算好的偏移量,deltaY>0就是往上滑动,反之往下滑动</p> <pre> <code class="language-java">final int y = (int) ev.getY(activePointerIndex); int deltaY = mLastMotionY - y; if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; }</code></pre> <p>其实这边所有Behavior接收流程都是一样的,主要看看AppBarLayout对onNestedPreScroll的处理以便于我们后续自定义Behavior的实现。这里的dy就是刚才说的偏移量,target就是发起者NestedScrollView。consumed数组是由x\y组成,AppBarLayout执行完成之后存储其本次垂直方向的滚动值。这里scroll方法会将AppBarLayout的移动范围固定在0-AppBarLayout高度这2个值范围内执行滚动操作,如果在范围外的话,AppBarLayout就不执行滚动操作,consumed[1]的值也为0</p> <pre> <code class="language-java">@Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) { if (dy != 0 && !mSkipNestedPreScroll) { int min, max; if (dy < 0) { // We're scrolling down min = -child.getTotalScrollRange(); max = min + child.getDownNestedPreScrollRange(); } else { // We're scrolling up min = -child.getUpNestedPreScrollRange(); max = 0; } consumed[1] = scroll(coordinatorLayout, child, dy, min, max); } }</code></pre> <p>只要你记得dy是已经处理好的偏移量并且方向不要搞错就行了。这个函数一般在scroll前调用。</p> <ul> <li> <p><strong>onNestedScroll</strong></p> </li> </ul> <p>这个实际上是NestedScrollingChild自身改变的回调,看看之前dispatchNestedPreScroll触发的部分有一句这个</p> <pre> <code class="language-java">deltaY -= mScrollConsumed[1];</code></pre> <p>刚才也说了AppBarLayout在不超过滚动范围的时候,consumed[1]为实际Y方向滚动量,反之则为0,那么也就是在滚够了的情况下才会调用onNestedScroll</p> <pre> <code class="language-java">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; } }</code></pre> <p>再看看源码,使用overScrollByCompat发生了自身的滚动,所以两次滚动之间的值就是scrolledDeltaY,作为已消费的值。未消费部分unconsumedY就是手指之间的距离减去滚动值之差。其实这个也好理解,当这个NestedScrollView滚到最底部的时候滚不动了,那么它的消费值就是0,未消费值就是手指之间的距离</p> <pre> <code class="language-java">if (mIsBeingDragged) { // Scroll to follow the motion event mLastMotionY = y - mScrollOffset[1]; final int oldY = getScrollY(); final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 0, true) && !hasNestedScrollingParent()) { mVelocityTracker.clear(); } final int scrolledDeltaY = getScrollY() - oldY; final int unconsumedY = deltaY - scrolledDeltaY; if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) ......... }</code></pre> <p>其实我不知道什么情况下unconsumedY是负数,AppBarLayout倒是处理了这个情况。这个函数一般在scroll后调用。</p> <p>总之滑动过程为AppBarlayout先滑,NestedScrollView再滑</p> <ul> <li> <p>onNestedPreFling与 <strong>onNestedFling</strong></p> <p>这个其实与onNestedPreScroll,onNestedScroll之间的关系差不多,我就不多说了</p> </li> <li> <p>onStopNestedScroll</p> <p>一切都结束的时候,执行这个方法</p> </li> <li> <p>onDependentViewChanged与 <strong>layoutDependsOn</strong> 、 <strong>onDependentViewRemoved</strong></p> </li> </ul> <p>layoutDependsOn就是用来告诉NestedScrollingParent我们依赖的是哪个View。除了滚动事件会被处理以外,这个View的大小、位置等变化也一样可以通过回调方法进行通知,通知是通过onDependentViewChanged回调告诉Behavior的</p> <pre> <code class="language-java">@Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { // We depend on any AppBarLayouts return dependency instanceof AppBarLayout; }</code></pre> <p>看看源码,在onAttachedToWindow中我们看到了ViewTreeObserver的身影,那么view的各种状态变化都会被他抓到</p> <pre> <code class="language-java">@Override public void onAttachedToWindow() { super.onAttachedToWindow(); resetTouchBehaviors(); if (mNeedsPreDrawListener) { if (mOnPreDrawListener == null) { mOnPreDrawListener = new OnPreDrawListener(); } final ViewTreeObserver vto = getViewTreeObserver(); vto.addOnPreDrawListener(mOnPreDrawListener); } .... } class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener { @Override public boolean onPreDraw() { onChildViewsChanged(EVENT_PRE_DRAW); return true; } }</code></pre> <p>这里有一个mNeedsPreDrawListener,它是什么情况变成true的?原来是ensurePreDrawListener这个方法里面判断了只要它有依赖关系,就可以添加监听。ensurePreDrawListener在刚才所说的prepareChildren之后调用,符合逻辑。</p> <pre> <code class="language-java">void ensurePreDrawListener() { boolean hasDependencies = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (hasDependencies(child)) { hasDependencies = true; break; } } if (hasDependencies != mNeedsPreDrawListener) { if (hasDependencies) { addPreDrawListener(); } else { removePreDrawListener(); } } }</code></pre> <p>回头看看prepareChildren方法,存储了全部被依赖的子View</p> <pre> <code class="language-java">private void prepareChildren() { mDependencySortedChildren.clear(); mChildDag.clear(); for (int i = 0, count = getChildCount(); i < count; i++) { final View view = getChildAt(i); final LayoutParams lp = getResolvedLayoutParams(view); lp.findAnchorView(this, view); mChildDag.addNode(view); // Now iterate again over the other children, adding any dependencies to the graph for (int j = 0; j < count; j++) { if (j == i) { continue; } final View other = getChildAt(j); final LayoutParams otherLp = getResolvedLayoutParams(other); if (otherLp.dependsOn(this, other, view)) { if (!mChildDag.contains(other)) { // Make sure that the other node is added mChildDag.addNode(other); } // Now add the dependency to the graph mChildDag.addEdge(view, other); } } } // Finally add the sorted graph list to our list mDependencySortedChildren.addAll(mChildDag.getSortedList()); // We also need to reverse the result since we want the start of the list to contain // Views which have no dependencies, then dependent views after that Collections.reverse(mDependencySortedChildren);}</code></pre> <p>再来看看onChildViewsChanged方法,循环遍历所有Child, 将每个子View都使用layoutDependsOn来比较一下, 确保所有互相依赖的子View都可以联动起来,如果是依赖关系,再调用onDependentViewChanged。这里checkChild是待检查的View,也就是我们添加Behavior的那个View,child就是被checkChild所依赖的View</p> <pre> <code class="language-java">.... 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 (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) { checkLp.resetChangedAfterNestedScroll(); continue; } final boolean handled; switch (type) { case EVENT_VIEW_REMOVED: // EVENT_VIEW_REMOVED means that we need to dispatch // onDependentViewRemoved() instead b.onDependentViewRemoved(this, checkChild, child); handled = true; break; default: // Otherwise we dispatch onDependentViewChanged() handled = b.onDependentViewChanged(this, checkChild, child); break; } if (type == EVENT_NESTED_SCROLL) { // If this is from a nested scroll, set the flag so that we may skip // any resulting onPreDraw dispatch (if needed) checkLp.setChangedAfterNestedScroll(handled); } } } ....</code></pre> <p>最后我们就来解决上一篇文章中那个思考题,为什么NestedScrollView下面会有一截在屏幕外,这是因为他依赖于AppBarLayout,否则他们的顶点应该在一个位置</p> <pre> <code class="language-java">private void layoutChild(View child, int layoutDirection) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Rect parent = mTempRect1; parent.set(getPaddingLeft() + lp.leftMargin, getPaddingTop() + lp.topMargin, getWidth() - getPaddingRight() - lp.rightMargin, getHeight() - getPaddingBottom() - lp.bottomMargin); if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this) && !ViewCompat.getFitsSystemWindows(child)) { // If we're set to handle insets but this child isn't, then it has been measured as // if there are no insets. We need to lay it out to match. parent.left += mLastInsets.getSystemWindowInsetLeft(); parent.top += mLastInsets.getSystemWindowInsetTop(); parent.right -= mLastInsets.getSystemWindowInsetRight(); parent.bottom -= mLastInsets.getSystemWindowInsetBottom(); } final Rect out = mTempRect2; GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), parent, out, layoutDirection); child.layout(out.left, out.top, out.right, out.bottom); }</code></pre> <p>关于onLayout方面的问题,可以通过onLayoutChild这个方法来细细研究</p> <pre> <code class="language-java">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); } else if (lp.keyline >= 0) { layoutChildWithKeyline(child, lp.keyline, layoutDirection); } else { layoutChild(child, layoutDirection); } }</code></pre> <p>onDependentViewRemoved就是移除View后进行调用,想象一下Snackbar与FloatingActionButton的使用场景就可以理解</p> <h3> </h3> <p> </p> <p>来自:http://www.jianshu.com/p/2245af12b241</p> <p> </p>