说一说android的嵌套滚动机制

欣赏一下
年轻时候的贝鲁奇
1. 系统接口

NestedScrollingParent, NestedScrollingChild,android5.0之后新增的特性

  • 在传统的事件分发机制中,如果一次手势想让多个view来联动,只能让里面的view先滚动起来然后等到适当的条件拦截事件让外面的view滚动,若想交换滚动顺序即先让外面的view动再让里面的view动,这是做不到的,因为事件机制是由里向外抛出,没法再回到里面了!但是在5.0左右的时候,提供了NestedScrollingParent,NestedScrollingChild接口,支持了嵌套手势操作,可以弥补这个缺陷哦。

  • 什么是嵌套滚动呢?

    • 当页面里面的控件在接受到手势行为去滚动的时候,能够让外面的view去滚动,然后外面滚到到符合你的要求了,你再让里面的控件滚动,也可以让外面的view和里面的控件一起滚动, 这个过程都是在一次手势中哦,所以正好弥补了传统事件机制中的不足。
  1. NestedScrollingParent: 作为嵌套滑动的parent

    public interface NestedScrollingParent {
        
        /**
        *当嵌套的child调用startNestedScroll,会触发这个方法,检测我们的parent是否支持嵌套的去滚动操作;
        return true即支持parent来滚动,return false即不支持嵌套滚动。
        * target是我们的发起嵌套滚动操作的view哦。
         */
        public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    
        /**
        *当上面的onStartNestedScroll返回true的时候,会触发这个方法来做你想要的初始化操作;
         */
        public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    
        /**
         */
        public void onStopNestedScroll(View target);
    
        /**
        * 接收到滚动请求,此时可以主动滑动来消费掉发起方提供的未消费完剩下的距离
         */
        public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed);
    
        /**
        * 在嵌套的层级中,当嵌套的子view滑动时候,我们想在他之前先让parent来滑动,就执行这个操作。
         */
        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    
        /**
        *parent实现一定的滑翔处理
         */
        public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
    
        /**
        * 一般没什么用,在子child滑翔之前开始滑翔,一般不会有这个操作。retur false即可。
         */
        public boolean onNestedPreFling(View target, float velocityX, float velocityY);
    
        /**
        * 返回当前滚动的坐标轴线,横轴线/纵轴
         */
        public int getNestedScrollAxes();
    
    
    
  2. NestedScrollingChild: 作为嵌套滑动的child

public interface NestedScrollingChild {
    
    /**
     * 设置child支持嵌套滑动,表示是否支持滚动的时候是否将发给parent.
     */
    public void setNestedScrollingEnabled(boolean enabled);

    /**
     * 判断是否支持嵌套滑动
     */
    public boolean isNestedScrollingEnabled();

    /**
     *child 开始着手触发嵌套活动了
     */
    public boolean startNestedScroll(int axes);

    /**
     * child开始想要停止嵌套滑动了,与startNestedScroll对应,由他发起自然要由他结束了。
     */
    public void stopNestedScroll();

    /**
     * 在child自身滚动之后分发剩余的未消费滑动距离
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    /**
     * 在子child决定滑动前先让他的parent来尝试下要不要先滑动下.
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    /**
     *当child滑翔的过程中时候,问问parent要不要也滑一下。
     */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
     * 略
     */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);


  • NestedScrollingParent和NestedScrollingChild的关系图


    图片来自https://www.jianshu.com/p/490659fae773
  1. NestedScrollingParentHelper:嵌套滚动的parent辅助类, 只是设计的方便,里面并没有做什么实际的动作。

  2. NestedScrollingChildHelper:嵌套滚动的发起方child, 下面列出几个关键的方法

    
    //当child滚动的时候,会调用该方法,找到可以接受嵌套去滚动的父容器, true表示找到了,false表示没有找到
    public boolean startNestedScroll(int axes) {
        //如果已经有了,直接返回
        if (hasNestedScrollingParent()) {
            return true;
        }
        //需要当前的child能支持嵌套滚动哦
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            //递归地上巡找到能够接收嵌套滚动的parent
            while (p != null) {
                //这个if检测当前的container是否支持嵌套滚动哦,
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    //如果支持赋值给mNestedScrollingParent,后面就直接用它就好了
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                //没找到继续向上遍历。
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }
    
    //在嵌套滚动的时候,child在自己滚动前会先问问他的parent要不要先滚动下,是通过该方法来实现的。
    //
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        //如果child支持嵌套滚动,并且存在嵌套的父容器,
        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;
                }
                //consumed记录了父容器消耗的距离,有就会返回true.
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
    
    
    
    //在嵌套滚动的时候,如果child滚动了一段距离,还剩下一段手势距离,就交给他的父容器问问他要不要划一划,基本逻辑和前面的方法是一样的呢,return true表明有这样的parent并且划了。
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        //如果child支持嵌套滚动,并且有嵌套的parent.
        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];
                }
             //那么就让嵌套的parent来滑动一下
                ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                                                dyConsumed, dxUnconsumed, dyUnconsumed);
    
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                //表明parent滚动了一段距离
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        //表明没有滚动距离
        return false;
    }
    
    
    
    
2. 嵌套在系统中的应用:NestedScrollView作为嵌套的parent, RecyclerView作为嵌套滚动的child的场景
  1. NestedScrollView:他既充当着嵌套滚动的父view,(其实也可同时充当着嵌套滚动的子child) 这里就看看作为parent实现了的NestedScrollingParent的相关接口吧, 接受嵌套child发起的滚动的操作都会在下面的接口中进行动作啦:
 @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        //如果是纵向的滚动,NestedScrollView支持嵌套地滚动;
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {         
        //如果onStartNestedScroll返回true,走到这里。
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        //NestedScrollView同时也作为child, 将嵌套事件发给他的parent中去;是一种递归嵌套
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
          //NestedScrollView同时也作为child,将嵌套滚动发给他的parent中去;
        stopNestedScroll();
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed) {
        final int oldScrollY = getScrollY();
        //消耗child没有滚动完的距离,
        scrollBy(0, dyUnconsumed);
        final int myConsumed = getScrollY() - oldScrollY;
        final int myUnconsumed = dyUnconsumed - myConsumed;
        //将自己未消耗完的距离继续递归地给到他的parent去消耗。NestedScrollView这时候又冲到嵌套的child
        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        // Do nothing
        //不会在child滑行前做什么
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        //如果child没有消耗,NestedScrollView将消耗掉这些。
        if (!consumed) {
            flingWithNestedDispatch((int) velocityY);
            return true;
        }
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        // Do nothing
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        //获取滚动的轴,横向的或是纵向的。
        return mParentHelper.getNestedScrollAxes();
    }

  1. NestedScrollView中的拦截和消耗事件对嵌套滚动原则的相关处理,看看onInterceptTouchEvent和onTouchEvent.

    • onInterceptTouchEvent, 如何拦截的呢,看源码注释解读
    //返回true就是拦截下来, false就是不拦截
    public boolean onInterceptTouchEvent(MotionEvent ev) {
      
      final int action = ev.getAction();
        //如果当前是move,并且当前NestedScrollView处于了滚动状态,就返回true.滚动事件不会下去,所以他的子view没法发起嵌套滚动的操作。
         if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
             return true;
         }
    
         switch (action & MotionEventCompat.ACTION_MASK) {
             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) {
                     Log.e(TAG, "Invalid pointerId=" + activePointerId
                           + " in onInterceptTouchEvent");
                     break;
                 }
    
                 final int y = (int) MotionEventCompat.getY(ev, pointerIndex);
                 final int yDiff = Math.abs(y - mLastMotionY);
                 //如果当前move是滚动操作,并且当前View压根就不支持嵌套滚动,那么就表示自己要来实现滚动啦。这时候后面的move都会被该NestedScrollView拦截下来的。
                 if (yDiff > mTouchSlop
                     && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                     mIsBeingDragged = true;
                     mLastMotionY = y;
                     initVelocityTrackerIfNotExists();
                     mVelocityTracker.addMovement(ev);
                     mNestedYOffset = 0;
                     final ViewParent parent = getParent();
                     if (parent != null) {
                         parent.requestDisallowInterceptTouchEvent(true);
                     }
                 }
                 break;
             }
    
             case MotionEvent.ACTION_DOWN: {
                 final int y = (int) ev.getY();
                 //如果down位置落点不在他的child内部,啥都不做,没法滚动
                 if (!inChild((int) ev.getX(), (int) y)) {
                     mIsBeingDragged = false;
                     recycleVelocityTracker();
                     break;
                 }
    
            
                 mLastMotionY = y;
                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
             
                 //建立速度跟踪,然后跟踪手势
                 initOrResetVelocityTracker();
                 mVelocityTracker.addMovement(ev);
                 //计算滚动
                 mScroller.computeScrollOffset();
                 //滚动没结束,mIsBeingDragged为true.
                 mIsBeingDragged = !mScroller.isFinished();
                 //作为嵌套的child, 发起滚动请求
                 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                 break;
             }
    
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
                 //mIsBeingDragged清除掉状态, 
                 mIsBeingDragged = false;
                 mActivePointerId = INVALID_POINTER;
                 //清除掉速度跟踪
                 recycleVelocityTracker();
                 //检查是否要滚动回弹一下
                 if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
                     ViewCompat.postInvalidateOnAnimation(this);
                 }
                 //停下嵌套滚动,如果有嵌套滚动的操作。
                 stopNestedScroll();
                 break;
            
         }
     //mIsBeingDragged其实表示的就是档次拖动是不是给这个ScrollView用;
         return mIsBeingDragged;
     }
    
    • onTouchEvent:如何响应的呢,看源码注释解读

       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;
                   }
                   //down的时候,请求父容器不要拦截;
                   if ((mIsBeingDragged = !mScroller.isFinished())) {
                       final ViewParent parent = getParent();
                       if (parent != null) {
                           parent.requestDisallowInterceptTouchEvent(true);
                       }
                   }
      
                 //当我们滚动scrollView的时候,如果还在滑行,我们突然按下手指,滚动就会停下来,就是因为这里的处理哦!
                   if (!mScroller.isFinished()) {
                       mScroller.abortAnimation();
                   }
      
                   // Remember where the motion event started
                   mLastMotionY = (int) ev.getY();
                   mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                   //这里是和嵌套滚动相关的地方,作为嵌套的child, 发起纵向的滚动请求
                   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;
                   //这时候其实作为一个child,滚动前先问问parent要不要滚动一下
                   if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                       //除去parent滚动过的距离
                       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) {//表示NestedScrollView自己要滚动了
                       // 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);
      
                       //overScrollByCompat表示要自己来滚动对应的距离啦,并不一定会滚动完所有的剩余距离
                       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;
                       //这里还是作为child, 把还没滚完的手势给到父parent.让他去滚动
                       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)) {
                           //该方法会做嵌套滑行分发,也就是当钱view支持滑行的时候也会给parent-view去滑行一下,不过他们没有做距离和速度分减少,也不好做因为他们都是根据最后的初始速度去减速滑行的。只是对应的parent可以根据child是否到边界了选择滑还是不滑。
                           flingWithNestedDispatch(-initialVelocity);
                       } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                                                       getScrollRange())) {
                           ViewCompat.postInvalidateOnAnimation(this);
                       }
                   }
                   mActivePointerId = INVALID_POINTER;
                   endDrag();
                   break;
            
              .......
                  
           vtev.recycle();
           return true;
       }
      
      
      
    • 本来是想分析NestedScrollView作为嵌套的parent行为,但从前面的onTouchEvent中源码可以看到,NestedScrollView这里其实基本充当着嵌套的child角色的,想想也是对的,嵌套滚动操作是由child来发起的然后parent响应,onTouchEvent自然是动作发起的地方,所以这里基本就是child的动作行为。我们在认识传统事件分发的时候,知道滚动这些move操作当前只能给某个view去消耗,没法给多个人使用的,而嵌套滚动却可以,在这里总结下他的实现,他在move的时候先将滚动距离通过dispatchNestedPreScroll传递给实现了NestedScrollingParent的接口的parent, 让他先滚动滚动,然后扣除parent滚动过的距离,接着自己再调用overScrollByCompat,NestedScrollView自己来滚动,如果还有剩余又调用dispatchNestedScroll, 继续让parent去滚动。在手指抬起的时候如果有滑行操作,也会把滑行速度传递父parent,父parent可以自行决定要不要进行滑行。大概就是这么个逻辑,实现了多个view来消耗一次手势操作呢。

  1. RecyclerView, 他只能作为嵌套的子child, 即实现NestedScrollingChild,而没能做parent. 就来看看他的onInterceptTouchEvent和onTouchEvent,是如何处理嵌套相关的行为吧。感觉应该和NestedScrollView应该是很相似的逻辑的哦

    • RecyclerView.onInterceptTouchEvent,看源码注释解读
     public boolean onInterceptTouchEvent(MotionEvent e) {
         if (mLayoutFrozen) {
             return false;
         }
         if (dispatchOnItemTouchIntercept(e)) {
             cancelTouch();
             return true;
         }
    
         if (mLayout == null) {
             return false;
         }
    
         final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
         final boolean canScrollVertically = mLayout.canScrollVertically();
    
         if (mVelocityTracker == null) {
             mVelocityTracker = VelocityTracker.obtain();
         }
         mVelocityTracker.addMovement(e);
    
         final int action = MotionEventCompat.getActionMasked(e);
         final int actionIndex = MotionEventCompat.getActionIndex(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);
                 }
    
                 // Clear the nested offsets
                 mNestedOffsets[0] = mNestedOffsets[1] = 0;
    
                 int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                 if (canScrollHorizontally) {
                     nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                 }
                 if (canScrollVertically) {
                     nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                 }
                 //down的时候发起嵌套滚动请求
                 startNestedScroll(nestedScrollAxis);
                 break;
    
             case MotionEventCompat.ACTION_POINTER_DOWN:
                 mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
                 mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
                 mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
                 break;
    
             case MotionEvent.ACTION_MOVE: {
                 final int index = MotionEventCompat.findPointerIndex(e, 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) (MotionEventCompat.getX(e, index) + 0.5f);
                 final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
                 if (mScrollState != SCROLL_STATE_DRAGGING) {
                     final int dx = x - mInitialTouchX;
                     final int dy = y - mInitialTouchY;
                     boolean startScroll = false;
                     if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                         mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
                         startScroll = true;
                     }
                     if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                         mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
                         startScroll = true;
                     }
                     if (startScroll) {
                         setScrollState(SCROLL_STATE_DRAGGING);
                     }
                 }
             } break;
                 
         .......
    
             case MotionEvent.ACTION_UP: {
                 mVelocityTracker.clear();
                 //停止嵌套滚动
                 stopNestedScroll();
             } break;
    
             case MotionEvent.ACTION_CANCEL: {
                 cancelTouch();
             }
         }
         return mScrollState == SCROLL_STATE_DRAGGING;
     }
    
    
    
    • 总结一下,从recyclerView的拦截方法中可以看出,其实和嵌套滚动操作的内容是很少的,只有在down的时候发起一下嵌套操作startNestedScroll,在up的时候停止嵌套滚动,告知到他的父容器,比如NestedScrollView。那么就看看他的其他关于拦截的逻辑吧,只要在拖拽的过程中,就会拦截下来,那么他的子view一般在这里就没法响应触摸事件啦。
  • RecyclerView.onTouchEvent,看源码注释解读

    public boolean onTouchEvent(MotionEvent e) {
      
       ......
            
        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
    
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
               
                if (mScrollState == SCROLL_STATE_SETTLING) {
                    //请求recyclerView的父容器不要拦截啊,看样子android系统也是这么做的哦,也担心上面被拦了
                        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;
    
         ......
            
            case MotionEvent.ACTION_MOVE: {
                final int index = MotionEventCompat.findPointerIndex(e, 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) (MotionEventCompat.getX(e, index) + 0.5f);
                final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
               // 传递给parent去预先滚动一段距离
                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];
                }
    
                //这里应该是一个设定,只要我们的move达到了一段的距离,我们就要让recyclerView滚动起来!
                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];
    
                    //scrollByInternal自己滚动一段距离,并且内部还会将剩下的距离又传递给parent.
                    //以后可以去查看该方法的实现。
                    if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                        //请求父容器不要拦截
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
            } break;
    
           ......
    
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally ?
                    -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
                final float yvel = canScrollVertically ?
                    -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
                //fling操作,在这里处理嵌套滑行的行为,可以查看里面的方法细节
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } break;
    
            case MotionEvent.ACTION_CANCEL: {
                cancelTouch();
            } break;
        }
    
        if (!eventAddedToVelocityTracker) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();
    
        return true;
    }
    
  • 总结一下,RecyclerView的onTouchEvent和NestedScrollView的逻辑很相似,二者在这个区间里表现的都是一个嵌套child的行为,在down的时候发起,在move先传递给parent, 然后自己消耗。大概就这样子吧。

3.  嵌套存在着的问题,以及造成的原因
  1. NestedScrollView/ScrollView嵌套ListView显示不全,经常显示一行问题!

    • 原因在哪里呢,如下:
    ---NestedScrollView方法:
    
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                            + widthUsed, lp.width);
        //在测量子View的高度的时候传递进去的是UNSPECIFIED,也就是不限制子view的高度。
            final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    
    
    ---ListView方法:
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // Sets up mListPadding
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
           .......
               
            if (widthMode == MeasureSpec.UNSPECIFIED) {
                widthSize = mListPadding.left + mListPadding.right + childWidth +
                        getVerticalScrollbarWidth();
            } else {
                widthSize |= (childState & MEASURED_STATE_MASK);
            }
        .......
            
            //重点在这里呢,如果是MeasureSpec.UNSPECIFIED模式,他设置的高度就是单个条目加上padding距离啊!所以就显示了一行......但是如果我们用其他的布局嵌套listView的时候,一般是不会传递UNSPECIFIED的规格的,所以没问题。
            if (heightMode == MeasureSpec.UNSPECIFIED) {
                heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                        getVerticalFadingEdgeLength() * 2;
            }
    
            setMeasuredDimension(widthSize, heightSize);
    
            mWidthMeasureSpec = widthMeasureSpec;
        }   
    
    • 解决, 重写LinearLayout的onMeasure方法,改写ScrollView传进来的测量规格哦,虽然解决了显示不全的问题,但是复用规则被打破!这不是好的办法。
      @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //改写规格,将高度设置成无限。因此也就造成了一开始就全部展开,无法复用listView的单元控件。重要弊端!
            int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
            super.onMeasure(widthMeasureSpec, heightSpec);
        }
    
    1. NestedScrollView与RecyclerView嵌套,RecyclerView不能被重复利用
    • 原因,还是看代码吧:
    --- LineaLayoutManager
    //当layoutState.mInfinite为true的时候,会一直调用layoutChunk,从而让所有的itemView一次性全部创建了。ayoutState.mInfinite的计算就是mLayoutState.mInfinite = mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED;而这个mode也是ScrollView传递进来的!
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
                layoutChunkResult.resetInternal();
                    layoutChunk(recycler, state, layoutState, layoutChunkResult);
                if (layoutChunkResult.mFinished) {
                    break;
                }
                layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
                /**
                 * Consume the available space if:
                 * * layoutChunk did not request to be ignored
                 * * OR we are laying out scrap children
                 * * OR we are not doing pre-layout
                 */
                if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                        || !state.isPreLayout()) {
                    layoutState.mAvailable -= layoutChunkResult.mConsumed;
                    // we keep a separate remaining space because mAvailable is important for recycling
                    remainingSpace -= layoutChunkResult.mConsumed;
                }
    
                if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
                    layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                    if (layoutState.mAvailable < 0) {
                        layoutState.mScrollingOffset += layoutState.mAvailable;
                    }
                    recycleByLayoutState(recycler, layoutState);
                }
                if (stopOnFocusable && layoutChunkResult.mFocusable) {
                    break;
                }
            }
    
    
    
     void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                LayoutState layoutState, LayoutChunkResult result) {
            View view = layoutState.next(recycler);
         ........
             if (layoutState.mScrapList == null) {
                 if (mShouldReverseLayout == (layoutState.mLayoutDirection
                                              == LayoutState.LAYOUT_START)) {
                     addView(view);
                 } else {
                     addView(view, 0);
                 }
    
    
    • 总结,上面两个都有复用规则打破的问题,这是个大问题,在少量数据还好,数据多了就会出现crash的,所以利用NestedScrollView+RecyclerView的去实现复杂界面并没有好的实现策略。虽然系统对二者都实现了嵌套滚动的策略,看上去处理的很好,然而却是存在着巨大的bug, google也推荐我们不要这么搞,但是实际有这样的需求啊, 感觉google这里好坑啊!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,780评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,424评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,397评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,576评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,997评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,945评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,107评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,850评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,625评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,804评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,285评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,613评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,291评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,164评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,963评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,096评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,886评论 2 278