事件分发(3)-全面的CoordinateLayout和Beha

作者: ZJ_Rocky | 来源:发表于2017-09-22 14:33 被阅读231次

主目录见:Android高级进阶知识(这是总目录索引)
本来以为这个知识点大家已经说得很多了,直接链接别人文章就可以了,可是后面看看讲的感觉不是很清楚,所以决定这边多写这一篇文章,同时也是因为自定义behavior的例子写完还需要完善,所以这篇就提前来说了。

我不是随意的人

一.目标

自定义behavior其实是非常重要的知识,他决定了几个控件怎么协调运动,做出合理且酷炫的效果,我们这篇文章的目标有以下几点:
 1.了解layout_behavior属性怎么加载的。
 2.了解layoutDependsOn和onDependentViewChanged调用过程。
 3.深入嵌套滑动的机制。
 4.为下一篇自定义behavior例子打基础。

二.源码分析

1.layout_behavior属性加载

我们都知道我们在使用自定义behavior的时候,我们都要使用app:layout_behavior来引用。但是我们这个属性是怎么被识别的呢?其实这个知识点我们已经讲过了,我们知道拦截View的创建有自定义LayoutInflater(换肤框架(一)之Support v7库解析)和重写ViewGroup下面的LayoutParams(自定义ViewGroup(一)之卡牌)。其实这里用的方法是第二种。我们看下CoordinateLayout重写的LayoutParams:

 public static class LayoutParams extends ViewGroup.MarginLayoutParams {
}

我们看到这里继承MarginLayoutParams自定义了一个LayoutParams ,我们看这个方法里面是怎么获取layout_behavior的:

   LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);

            final TypedArray a = context.obtainStyledAttributes(attrs,
                    R.styleable.CoordinatorLayout_Layout);

            this.gravity = a.getInteger(
                    R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
                    Gravity.NO_GRAVITY);
            mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
                    View.NO_ID);
            this.anchorGravity = a.getInteger(
                    R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
                    Gravity.NO_GRAVITY);

            this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
                    -1);

            insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
            dodgeInsetEdges = a.getInt(
                    R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior);
            if (mBehaviorResolved) {
//判断如果有自定义behavior的话就调用这个方法解析
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));
            }
            a.recycle();

            if (mBehavior != null) {
                // If we have a Behavior, dispatch that it has been attached
                mBehavior.onAttachedToLayoutParams(this);
            }
        }

我们看到这个方法其实很简单,其实就是获取自定义属性,一共获取的layout_gravity,layout_anchor,layout_anchorGravity,layout_keyline,layout_insetEdge,layout_dodgeInsetEdges,layout_behavior等自定义属性。这些属性大家应该都非常熟悉,这个就是使用CoordinateLayout会使用到的。我们看到最后调用了方法parseBehavior()来解析behavior:

 static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
//判断这个behavior是否以.开头,如果是以.开头我们需要拼凑上我们应用的包名
        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
//如果我们包含.但是不是以.开头,说明我们填写的是全类名,直接反射即可
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
//不然我们判断widget的包名是否为空,不为空就添加上widget的包名即可
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : name;
        }

        try {
//这个map是用来保存bahavior的构造函数的,获取到通过反射构造函数来初始化自定义behavior
            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);
        }
    }

上面的注释已经说得非常清楚了,这个方法很简单,其实就是根据我们传进layout_behavior属性的类名反射实例化我们的自定义behavior。到这里我们的behavior就被加载进来了。

2.layoutDependsOn和onDependentViewChanged调用过程

这个过程其实也不难,我们想想,一个控件运动,另外一个控件也会跟着运动,这个我们自己来实现要怎么实现呢?首先我们肯定会先确立好依赖关系,layoutDependsOn要做的事情就是判断这个依赖关系是否成立的,然后我们肯定要监听我们控件运动,我们知道我们平常会用到ViewTreeObserver来监听视图的状态变化,然后我们监听到变化后悔通知依赖的控件进行相应的运动,过程就完成了。这里CoordinateLayout也是这么干的。

 final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);

我们看到coordinateLayout里面的ViewTreeObserver里面设置了监听mOnPreDrawListener,这个监听器是干什么的呢?我们的逻辑肯定是在这个监听器里面的,我们来看看这个监听器:

    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

我们看了这个监听器里面就调用了onChildViewsChanged方法,我们直接跟进去:

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
//得到依赖的视图数量
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = mTempRect4;
        inset.setEmpty();
        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);
//根据anchor的值来停靠
                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }

//省略一些未说明代码   
..........

            // 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();
//得到视图里面的behavior属性对应的自定义behavior对象
                final Behavior b = checkLp.getBehavior();
//调用behavior的layoutDependsOn来判断依赖关系是否成立
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        // If this is from a pre-draw and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        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:
//最后我们会调用behavior的onDependentViewChanged来通知依赖对象说我们的视图状态已经改变了
                            // 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);
                    }
                }
            }
        }
    }

这段代码略微有点长了,但是我已经是尽力把代码省略了,留下来都是要讲的关键代码了。首先我们来看看mDependencySortedChildren是什么?这个就是记录依赖对象的数据结构,那么我们这个是怎么确定这个依赖关系的呢?我们来看下这个mDependencySortedChildren是怎么构建的,我们在CoordinateLayout的prepareChildren()方法里面看到:

        // Finally add the sorted graph list to our list
        mDependencySortedChildren.addAll(mChildDag.getSortedList());

那么我们这里又要来看看mChildDag是个啥。首先我们看下他的声明:

 private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

我先告诉你这个是啥,这个其实是有向非循环图,我们的依赖视图其实就是添加进这个图里面,然后程序会利用深度优先搜索(dfs)来检索排序添加进去的视图节点,现在我们来一步一步来看,首先我们看视图是怎么添加进这个图的,还是在prepareChildren()方法里面:

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

我们看到首先是遍历CoordinateLayout的所有的一级子类,添加进去,所以我们要注意,我们自定义behavior只能使用在CoordinateLayout的一级子类上面,不然不会被监听到,就是说如果是用在嵌套几层层级关系的视图上面是不会有效果的。然后添加完一级子类后会重新遍历一级子类,把依赖的子类(用dependsOn判断)的视图用addEdge方法添加进来这个图。添加进来之后我们看下后面调用了mChildDag.getSortedList()方法来进行排序:

   @NonNull
    ArrayList<T> getSortedList() {
        mSortResult.clear();
        mSortTmpMarked.clear();

        // Start a DFS from each node in the graph
        for (int i = 0, size = mGraph.size(); i < size; i++) {
            dfs(mGraph.keyAt(i), mSortResult, mSortTmpMarked);
        }

        return mSortResult;
    }

我们看到这个方法很简单,就是遍历图中每个元素即添加进去的一级子类,然后用深度优先搜索(dfs)来分别遍历每个一级子类的依赖视图,我们来看下这个算法做了啥:

    private void dfs(final T node, final ArrayList<T> result, final HashSet<T> tmpMarked) {
        if (result.contains(node)) {
            // We've already seen and added the node to the result list, skip...
            return;
        }
        if (tmpMarked.contains(node)) {
            throw new RuntimeException("This graph contains cyclic dependencies");
        }
        // Temporarily mark the node
        tmpMarked.add(node);
        // Recursively dfs all of the node's edges
        final ArrayList<T> edges = mGraph.get(node);
        if (edges != null) {
            for (int i = 0, size = edges.size(); i < size; i++) {
                dfs(edges.get(i), result, tmpMarked);
            }
        }
        // Unmark the node from the temporary list
        tmpMarked.remove(node);
        // Finally add it to the result list
        result.add(node);
    }

这个方法做法很明确,递归搜索所有的依赖对象,然后加到result中,这样我们的依赖关系就确定完毕了。那么我们视图状态变化就可以通知了,我们来看下后面是怎么通知,我们继续看上面代码:

 final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//得到视图里面的behavior属性对应的自定义behavior对象
                final Behavior b = checkLp.getBehavior();
//调用behavior的layoutDependsOn来判断依赖关系是否成立
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        // If this is from a pre-draw and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

这句代码就是获取节点后面的后一个节点,同样这是从图里面获取的,我们取出后一个节点的behavior,然后调用behavior的layoutDependsOn方法来判断这个节点是否依赖前面那个节点。如果有依赖关系则最后会调用:

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

然后我们看到在default里面会调用behavior里面的onDependentViewChanged方法。
总结:到这里我们的layoutDependsOn和onDependentViewChanged就说完了,总过程就是先把依赖关系添加进图里然后搜索添加进ArrayList里面,最后取出依赖视图的behavior然后调用layoutDependsOn判断是否有依赖关系,如果有则调用behavior的onDependentViewChanged通知。

大boss已经在路上

3.嵌套滑动的机制

这个机制应该说是CoordinateLayout的精髓了,非常精彩的源码,看了热血沸腾,你也有充分地理由来了解它!!!说这个之前我们先来明确下下面几个类的作用:
 1.interface NestedScrollingChild
 2.class NestedScrollingChildHelper
 3.interface NestedScrollingParent
 4.class NestedScrollingParentHelper

为了能够看得懂下面的源码我先来串下整个过程:首先CoordinateLayout实现NestedScrollingParent接口,并且里面的滚动处理由NestedScrollingParentHelper来转发调用。那么嵌套滑动到底是什么呢?其实就是实现了NestedScrollingChild接口的子类(RecyclerView,NestedScrollerView,SwipeRrefreshLayout所以CoordinateLayout必须包含这几个控件之一,不然滑动事件不会被监听到)等先接收到滑动事件,然后通过调用NestedScrollingChildHelper类中的相应方法,传给CoordinateLayout(实现了NestedScrollingParent的子类)处理滑动事件,然后coordinateLayout调用相应的自定义behavior处理,完成后会把剩余的未消费的事件传回实现了NestedScrollingChild接口的子类(RecyclerViewNestedScrollerView,SwipeRrefreshLayout)等。

整个过程讲解的已经很明白了,所以嵌套滑动可以达到拦截RecyclerView等控件的滑动事件,决定消费掉多少滑动值,然后再交给RecyclerView自己来处理,这个功能非常管用,可以做好多效果。接下来我们举RecyclerView为例子来走下代码,我们从RecyclerView的onTouchEvent方法开始首先看下ACTION_DOWN:

   case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis);
            } 
break;

我们看到在ACTION_DOWN事件中我们首先判断滑动是垂直的还是水平的,然后调用startNestedScroll()方法:

 @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

我们看到这个地方调用了NestedScrollerChildHelper中的的startNestedScroll方法:

 public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
//这个方法最终会调用到NestedScrollingParent中的onStartNestedScroll方法
                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;
    }

我们看到这个方法是循环向上查找实现了NestedScrollingParent接口的父类,找到了就调用onStartNestedScroll方法,我们这里coordinateLayout实现了NestedScrollingParent,所以这个地方我们调用了CoordinateLayout的onStartNestedScroll方法:

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

我们看到这个地方CoordinateLayout会遍历所以的子类,然后获取他们的behavior,然后调用behavior的onStartNestedScroll()方法。所以onStartNestedScroll()是在事件DOWN中调用的,说明我要开始滑动了。接着我们开始看RecyclerView的onTouchEvent中的事件ACTION_MOVE做了啥:

  case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id " +
                            mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
//在这个地方调用dispatchNestedPreScroll最终会调用到NestedScrollingParent的onNestedPreScroll方法
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
//这个地方mScrollConsumed记录了子视图behavior消费了多少的x轴方向距离和y轴方向距离
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        if (dx > 0) {
                            dx -= mTouchSlop;
                        } else {
                            dx += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        if (dy > 0) {
                            dy -= mTouchSlop;
                        } else {
                            dy += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
//然后把剩余的x轴方向滑动距离和y轴方向滑动距离传给这个方法,这个方法最终会调用到NestedScrollingParent的onNestedScroll方法
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (ALLOW_PREFETCHING) {
                        mViewPrefetcher.postFromTraversal(dx, dy);
                    }
                }
            } break;

我们看到在MOVE事件中我们首先是调用到了dispatchNestedPreScroll方法,这个方法首先会调用NestedScrollingChildHelper的dispatchNestedPreScroll方法:

   @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

这样的话我们直接就跟进NestedScrollingChildHelper的dispatchNestedPreScroll方法:

    public boolean
    dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {
........
                consumed[0] = 0;
                consumed[1] = 0;
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

我们直接看到 consumed[0] = 0;consumed[1] = 0;这个数组待会用来保存x轴和y轴消费掉的滑动值,然后调用ViewParentCompat的onNestedPreScroll,我们继续跟进这个方法:

      @Override
        public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
                int[] consumed) {
            if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
            }
        }

我们看到这里调用了NestedScrollingParent子类的onNestedPreScroll方法,也就是调用到了CoordinateLayout的onNestedPreScroll()方法:

 @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
//同样是遍历所有子视图,获取到所有子视图的behavior,然后调用他的onNestedPreScroll然后记录下来消耗的x,y方向滑动值
        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) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);

                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }

        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

到这里我们知道在ACTION_MOVE事件的时候会先调用behavior的onNestedPreScroll方法,然后看用户在自定义的behavior里面消耗多少x,y方向的滑动值,然后保存起来。然后ACTION_MOVE事件我们继续往下看:

 if (scrollByInternal(canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
  }

我们看到我们会把未消费掉的dx和dy传进这个scrollByInternal()方法,我们看下这个方法做了些什么:

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0, unconsumedY = 0;
        int consumedX = 0, consumedY = 0;
.......
        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
            // Update the last touch co-ords, taking any scroll offset into account
            mLastTouchX -= mScrollOffset[0];
            mLastTouchY -= mScrollOffset[1];
            if (ev != null) {
                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            }
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
        } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
            if (ev != null) {
                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
            }
            considerReleasingGlowsOnScroll(x, y);
        }
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
        if (!awakenScrollBars()) {
            invalidate();
        }
        return consumedX != 0 || consumedY != 0;
    }

我们看到这个方法又调用了dispatchNestedScroll()方法,我们跟进这个方法看做了啥:

  @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

同样的套路,这个方法最后调用了CoordinateLayout的onNestedScroll方法,也就是遍历调用了子视图的behavior的onNestedScroll方法。最后我们又会判断有没有消耗完如果还有剩余则会调用我们自己的事件来处理:

  if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }

到这里我们的RecyclerView的onTouchEvent方法的MOVE也就执行完成了。也就是说在MOVE方法里面我们会触发子视图自定义behavior的onNestedPreScroll方法和onNestedScroll方法。如果还有剩余的交给自己处理。这样我们就剩下ACTION_UP方法了,其实我们知道onTouchEvent方法前面会先调用onInterceptTouchEvent的,我们的stop方法其实是在这里的UP调用的:

 case MotionEvent.ACTION_UP: {
                mVelocityTracker.clear();
                stopNestedScroll();
            } break;

这个过程跟前面其他的方法调用过程是一样的,也是会调用到CoordinateLayout中各个视图behavior的onStopNestedScroll()方法。到这里我们的嵌套滑动已经讲解完毕,其实到这里已经大部分知识点已经讲完了。但是我们还有两个我们可能会用到的方法:onLayoutChild和onMeasureChild,这两个方法其实还是蛮简单的,我们这里也来说明说明。

4.onMeasureChild和onLayoutChild

这两个方法在自定义控件时候我们已经知道,我们一个自定义控件会先测量然后再布局,首先我们来看onMeasureChild方法,这个方法主要是在CoordinateLayout类里面的onMeasure,我们程序会遍历所有的子视图,然后获取他的behavior,然后调用他的onMeasureChild:

  final Behavior b = lp.getBehavior();
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }

我们看到如果behavior存在的话则会调用他的onMeasureChild方法,不然就会调用自己的onMeasureChild方法。同样的onLayoutChild方法也是一样的。是在onLayout里面,程序会在CoordinateLayout里面会遍历所有子视图,然后获取他们的behavior,然后调用他们的onLayoutChild:

     if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }

这个也是一样,如果behavior存在则会调用他们的onLayoutChild。所以我们在这两个方法里面可以来测量和布局我们的视图,当然这两个方法有可能被调用多次,所以请注意一下。
总结:到这里我们的讲解就已经完毕了,其实从讲解中可以看到这个机制可以协调各个控件的整体运动,可以做好多效果,所以我们一定要完全掌握了这个机制,希望大家享受谷歌这个优秀的代码。

相关文章

网友评论

    本文标题:事件分发(3)-全面的CoordinateLayout和Beha

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