CoordinatorLayout 滑动机制

CoordinatorLayout 中的触摸滑动事件是如何传递给子控件的,子控件是如何处理的,又是如何反馈给 CoordinatorLayout 的?我们以 RecyclerView 作为 CoordinatorLayout 的子控件为例子,看看源码,是如何调用的。

触摸滑动,说白了,都是要看 dispatchTouchEvent() 、 onInterceptTouchEvent() 、 onTouchEvent() 这三个方法的逻辑,RecyclerView 中有重写后两个方法,我们就来看看 ACTION_DOWN 逻辑

    public boolean onInterceptTouchEvent(MotionEvent e) {
        ...
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (mIgnoreMotionEventTillDown) {
                    mIgnoreMotionEventTillDown = false;
                }
                mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                if (mScrollState == SCROLL_STATE_SETTLING) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                }

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

            ...
        }
        return mScrollState == SCROLL_STATE_DRAGGING;
    }

    public boolean startNestedScroll(int axes) {
        return mScrollingChildHelper.startNestedScroll(axes);
    }

    private void setScrollState(int state) {
        if (state == mScrollState) {
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
                    new Exception());
        }
        mScrollState = state;
        if (state != SCROLL_STATE_SETTLING) {
            stopScrollersInternal();
        }
        dispatchOnScrollStateChanged(state);
    }

ACTION_DOWN 时,判断如果是滑动状态,请求父容器不拦截,然后把状态设置为拖拽状态,停止RecyclerView 的滑动,触发状态改变的回调;下面是关键,根据 canScrollHorizontally 和 canScrollVertically 属性,来给 nestedScrollAxis 赋值,标识是否可以横向或竖向滑动,然后调用了 startNestedScroll() 方法,它里面又调用了 NestedScrollingChildHelper的方法。看名字,它是一个滑动的辅助类,它是在 RecyclerView 的构造方法中创建的,

    public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        ...
        mScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

    public void setNestedScrollingEnabled(boolean enabled) {
        mScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

并设置了允许滑动的标识,看看 NestedScrollingChildHelper 的代码

    public NestedScrollingChildHelper(View view) {
        mView = view;
    }

    public void setNestedScrollingEnabled(boolean enabled) {
        if (mIsNestedScrollingEnabled) {
            ViewCompat.stopNestedScroll(mView);
        }
        mIsNestedScrollingEnabled = enabled;
    }

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


重点关注 startNestedScroll() 方法,如果已经获取到了可以滑动的父容器 mNestedScrollingParent,则 return true; 如果可以滑动,则 p = mView.getParent(),这一步是获取父容器,然后重点来了,看看 while 循环中,如果满足if中的条件,则把当前 p 赋值给 mNestedScrollingParent,我们看看主要的两个方法 onStartNestedScroll() 和 onNestedScrollAccepted(),

    public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
        return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
    }

    public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
        IMPL.onNestedScrollAccepted(parent, child, target, nestedScrollAxes);
    }

这里是做了版本判断,我们找个比较低的版本,看看具体是怎么判断的 

    static class ViewParentCompatStubImpl implements ViewParentCompatImpl {

        @Override
        public boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
            return false;
        }

        @Override
        public void onNestedScrollAccepted(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
                        nestedScrollAxes);
            }
        }
        
        ...
    }

看到这就明白了,还是调用父容器的 onStartNestedScroll() 方法,我们看看 CoordinatorLayout 的方法

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

原来如此,按下 RecyclerView,最终会调到 CoordinatorLayout 中的 onStartNestedScroll() 方法,此方法中会再次把事件传递给 Behavior,这样就形成了一串调用。再看看另外一个方法,调用了 CoordinatorLayout 中的 onNestedScrollAccepted() 方法 

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        mNestedScrollingDirectChild = child;
        mNestedScrollingTarget = target;

        final int childCount = getChildCount();
        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) {
                viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
            }
        }
    }

和前一个方法一样,也是会调用 Behavior 中的方法。看看 RecyclerView 中 onTouchEvent() 方法

    public boolean onTouchEvent(MotionEvent e) {
        ...
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                ...
                startNestedScroll(nestedScrollAxis);
            } 
            break;
            case MotionEvent.ACTION_MOVE: {
                ...
                final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
                final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    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) {
                        final ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    
                    if (scrollByInternal(canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0, vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
            } 
            break;
            ...
        }
        ...
        return true;
    }

ACTION_DOWN 中方法和之前的一样,就不分析了。onInterceptTouchEvent() 中 ACTION_MOVE 没什么重要的,重点看 onTouchEvent() 中的 ACTION_MOVE,这个才是精髓所在。老样子,看看 dispatchNestedPreScroll() 方法

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

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

                if (consumed == null) {
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                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;
    }

重点关注 int[] consumed 这个形参,是从 RecyclerView 中传递进来的数组,记录的是消费的值。dx 和 dy 是偏移量,这里判断的是如果有偏移量,会把 consumed 输入内容清零,然后调用 ViewParentCompat 的 onNestedPreScroll() 方法,返回的值是判断 consumed 中内容是否为零,我们看看调用的方法

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

果然,继续看 CoordinatorLayout 中的方法

    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;
        final int childCount = getChildCount();
        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) {
            dispatchOnDependentViewChanged(true);
        }
    }


最终还是回到这里,我们注意了,int[] consumed 这里所操作的地方,在 Behavior 中传入的 mTempIntPair 数组,比较一番值后,最终决定是否赋值给 consumed,重新回到上一步,dispatchNestedPreScroll() 方法中最终判断的是 consumed[0] != 0 || consumed[1] != 0,如果此时返回为true,再看看 RecyclerView 中,onTouchEvent() 中,dx 和 dy 会减去 consumed 数组中的值;如果 consumed 中都为0,说明父容器没有消费,那么 dx 和 dy 值不变,继续往下看。 if (mScrollState != SCROLL_STATE_DRAGGING) 这个判断只会执行一次,第一次的话,防止位移量偏大,会对 dx 和 dy 做一些修正,然后就把 mScrollState 值给改变了,下次就不会走进if判断了。最后看看 scrollByInternal() 方法,这个是自己滑动及调用了 dispatchNestedScroll() 方法,这个最终会调用 CoordinatorLayout 的 onNestedScroll() 方法,这个对应着已经消费的方法。

CoordinatorLayout 中 onNestedPreScroll() 这个方法中的 int[] consumed 数组,以及传递给 Behavior 中的 onNestedPreScroll() 方法中的数组,它告知我们子view将要滑动的距离,交割给 CoordinatorLayout,如果 CoordinatorLayout 或 Behavior 不消费,或者没消费完,还有剩余,这时候, RecyclerView 才会继续消费,我们自定义控件容器或自定义 Behavior 时,可以通过 onNestedPreScroll() 方法中 consumed 的赋值,来控制自身消费的值,同时做出相应的操作。

ACTION_UP 中,手指抬起时,计算手指的速率,然后是 fling((int) xvel, (int) yvel)) 方法,这个就是控件自身滑动的方法,和上面一样,最终会回调到 CoordinatorLayout 中,这里就不多分析了,剩余的两个方法也一样。最终记住,

自定义Behavior可以选择重写以下的几个方法有:

  ● onStartNestedScroll():嵌套滑动开始(ACTION_DOWN),确定Behavior是否要监听此次事件
  ● onStopNestedScroll():嵌套滑动结束(ACTION_UP或ACTION_CANCEL)

  ● onNestedPreScroll():嵌套滑动进行中,要监听的子 View将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制)
  ● onNestedScroll():嵌套滑动进行中,要监听的子 View的滑动事件已经被消费

  ● onNestedPreFling():要监听的子View即将快速滑动
  ● onNestedFling():要监听的子 View在快速滑动中
发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/100558735