美文网首页androidMD风格CoordinatorLayout
CoordinatorLayout——源码分析

CoordinatorLayout——源码分析

作者: 皮球二二 | 来源:发表于2016-09-19 15:44 被阅读1102次

    CoordinatorLayout作为协调布局,而真正实现功能的部分在于Behavior,所以我打算将这两地方都捎带说说,若有意见请及时提出帮助我改正

    Behavior的初始化

    Behavior是CoordinatorLayout内部静态抽象类,它是一种新的view关系描述,即依赖关系。一般我们都是继承这个类去完成自己的自定义功能

    之前我们提及Behavior可以通过注解或者layout_behavior来声明,如果你是通过xml来初始化,那么在CoordinatorLayout初始化的时候就完成了

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

    如果你是使用注解进行初始化,那么他在onMeasure的时候通过prepareChildren才进行初始化,注意看setBehavior这里。所以xml里初始化优先级高。xml内指定的话,是在inflate的时候对mBehavior赋值;注解里指定的话,是在onMeasure内赋值,稍有不同。

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

    前面我们提及反射初始化Behavior的,在这个parseBehavior里面就能看到

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

    还有一个需要注意的地方,我们看到反射的方法是2个参数的构造方法

    static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
            Context.class,
            AttributeSet.class
    };
    

    所以我们在自定义Behavior的时候,一定要去重写

    NestedScrolling概念

    其实想说一下为什么叫嵌套滑动,之前我们老是提及这个概念。CoordinatorLayout本身是不能动的,但是一旦其中包含了具备NestedScrolling功能的滚动视图,那就不一样了。它在滑动过程中会对Behavior产生影响,进而可以通过动画或者View之间的关联关系进行改变。这里,就是有嵌套这么一层关系

    之前那种TouchEvent形式的滑动方式,一旦子View拦截了事件,除非重新进行一次事件传递,不然父View是拿不到事件的。而NestedScrolling很好的解决了这个问题

    在阅读源码的时候,请着重关注这4个类

    1. NestedScrollingChild
      如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口
    2. NestedScrollingParent
      作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现 NestedScrollingParent接口,这个接口方法和 NestedScrollingChild大致有一一对应的关系
    3. NestedScrollingChildHelper
      实现好了 Child 和 Parent 交互的逻辑
    4. NestedScrollingParentHelper
      实现好了 Child 和 Parent 交互的逻辑

    NestedScrolling滑动机制流程

    完整的事件流程大致是这样的:
    滑动开始的调用startNestedScroll(),Parent收到onStartNestedScroll()回调,决定是否需要配合Child一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted()。每次滑动前,Child 先询问Parent是否需要滑动,即 dispatchNestedPreScroll(),这就回调到Parent的onNestedPreScroll(),Parent可以在这个回调中“劫持”掉Child的滑动,也就是先于Child滑动。Child滑动以后,会调用onNestedScroll(),回调到Parent的onNestedScroll()。最后滑动结束,调用 onStopNestedScroll()表示本次处理结束。

    NestedScrollingChild与NestedScrollingChildHelper的交互流程 NestedScrollingChildHelper与ViewParentCompat的交互流程 ViewParentCompat与CoordinatorLayout的交互流程 CoordinatorLayout与Behavior的交互流程

    主要回调方法介绍

    • onStartNestedScroll

    在NestedScrollView的ACTION_DOWN事件中开始流程

    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    

    NestedScrollingChildHelper里循环查找直到找出CoordinatorLayout,继续发送

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

    ViewParentCompat里面,parent只要实现了onStartNestedScroll就可以继续流程,这里也是说添加Behavior的控件必须直接从属于CoordinatorLayout,否则没有效果

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

    CoordinatorLayout循环通知所有第一层子视图中的Behavior

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

    它的返回值,决定了NestedScrollingChildHelper.onStartNestedScroll是不是要继续遍历,如果我们的Behavior对这个滑动感兴趣,就返回true,它的遍历就会结束掉。

    • onNestedPreScroll

    在ACTION_MOVE中进行触发传递,注意这边的deltaY是已经计算好的偏移量,deltaY>0就是往上滑动,反之往下滑动

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

    其实这边所有Behavior接收流程都是一样的,主要看看AppBarLayout对onNestedPreScroll的处理以便于我们后续自定义Behavior的实现。这里的dy就是刚才说的偏移量,target就是发起者NestedScrollView。consumed数组是由x\y组成,AppBarLayout执行完成之后存储其本次垂直方向的滚动值。这里scroll方法会将AppBarLayout的移动范围固定在0-AppBarLayout高度这2个值范围内执行滚动操作,如果在范围外的话,AppBarLayout就不执行滚动操作,consumed[1]的值也为0

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

    只要你记得dy是已经处理好的偏移量并且方向不要搞错就行了。这个函数一般在滚动前调用。

    • onNestedScroll

    这个实际上是NestedScrollingChild自身改变的回调,看看之前dispatchNestedPreScroll触发之后的部分有一句这个

    deltaY -= mScrollConsumed[1];
    

    刚才也说了AppBarLayout在不超过滚动范围的时候,consumed[1]为实际Y方向滚动量,反之则为0,也就是在滚够了的情况下才会调用dispatchNestedScroll

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

    再看看源码,使用overScrollByCompat发生了自身的滚动,所以两次滚动之间的值就是scrolledDeltaY,作为已消费的值。未消费部分unconsumedY就是手指之间的距离减去滚动值之差。其实这个也好理解,当这个NestedScrollView滚到最底部的时候滚不动了,那么它的消费值就是0,未消费值就是手指之间的距离

    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))
        .........
    }
    

    其实我不知道什么情况下unconsumedY是负数,AppBarLayout倒是处理了这个情况。这个函数一般在scroll后调用。

    总之滑动过程为AppBarlayout先滑,NestedScrollView再滑

    • onNestedPreFlingonNestedFling
      这个其实与onNestedPreScroll,onNestedScroll之间的关系差不多,我就不多说了

    • onStopNestedScroll
      一切都结束的时候,执行这个方法

    • onDependentViewChangedlayoutDependsOnonDependentViewRemoved

    layoutDependsOn就是用来告诉NestedScrollingParent我们依赖的是哪个View。除了滚动事件会被处理以外,这个View的大小、位置等变化也一样可以通过回调方法进行通知,通知是通过onDependentViewChanged回调告诉Behavior的

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        // We depend on any AppBarLayouts
        return dependency instanceof AppBarLayout;
    }
    

    看看源码,在onAttachedToWindow中我们看到了ViewTreeObserver的身影,那么view的各种状态变化都会被他抓到

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

    这里有一个mNeedsPreDrawListener,它是什么情况变成true的?原来是ensurePreDrawListener这个方法里面判断了只要它有依赖关系,就可以添加监听。ensurePreDrawListener在刚才所说的prepareChildren之后调用,符合逻辑。

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

    回头看看prepareChildren方法,存储了全部被依赖的子View

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

    再来看看onChildViewsChanged方法,循环遍历所有Child, 将每个子View都使用layoutDependsOn来比较一下, 确保所有互相依赖的子View都可以联动起来,如果是依赖关系,再调用onDependentViewChanged。这里checkChild是待检查的View,也就是我们添加Behavior的那个View,child就是被checkChild所依赖的View

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

    最后我们就来解决上一篇文章中那个思考题,为什么NestedScrollView下面会有一截在屏幕外,这是因为他依赖于AppBarLayout,否则他们的顶点应该在一个位置

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

    关于onLayout方面的问题,可以通过onLayoutChild这个方法来细细研究

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

    onDependentViewRemoved就是移除View后进行调用,想象一下Snackbar与FloatingActionButton的使用场景就可以理解

    参考链接

    深入理解CoordinatorLayout(草稿)
    Android 嵌套滑动机制(NestedScrolling)
    源码看CoordinatorLayout.Behavior原理
    android.support.design 学习笔记 1
    【译】Nested Scrolling With CoordinatorLayout On Android

    相关文章

      网友评论

      本文标题:CoordinatorLayout——源码分析

      本文链接:https://www.haomeiwen.com/subject/ycuvettx.html