說一說android的嵌套滾動機制

欣賞一下
年輕時候的貝魯奇
1. 系統(tǒng)接口

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

  • 在傳統(tǒng)的事件分發(fā)機制中逸雹,如果一次手勢想讓多個view來聯(lián)動,只能讓里面的view先滾動起來然后等到適當?shù)臈l件攔截事件讓外面的view滾動初坠,若想交換滾動順序即先讓外面的view動再讓里面的view動查排,這是做不到的躯舔,因為事件機制是由里向外拋出驴剔,沒法再回到里面了!但是在5.0左右的時候粥庄,提供了NestedScrollingParent丧失,NestedScrollingChild接口,支持了嵌套手勢操作惜互,可以彌補這個缺陷哦布讹。

  • 什么是嵌套滾動呢?

    • 當頁面里面的控件在接受到手勢行為去滾動的時候训堆,能夠讓外面的view去滾動描验,然后外面滾到到符合你的要求了,你再讓里面的控件滾動坑鱼,也可以讓外面的view和里面的控件一起滾動, 這個過程都是在一次手勢中哦膘流,所以正好彌補了傳統(tǒng)事件機制中的不足。
  1. NestedScrollingParent: 作為嵌套滑動的parent

    public interface NestedScrollingParent {
        
        /**
        *當嵌套的child調(diào)用startNestedScroll鲁沥,會觸發(fā)這個方法呼股,檢測我們的parent是否支持嵌套的去滾動操作;
        return true即支持parent來滾動,return false即不支持嵌套滾動画恰。
        * target是我們的發(fā)起嵌套滾動操作的view哦彭谁。
         */
        public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    
        /**
        *當上面的onStartNestedScroll返回true的時候,會觸發(fā)這個方法來做你想要的初始化操作;
         */
        public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    
        /**
         */
        public void onStopNestedScroll(View target);
    
        /**
        * 接收到滾動請求阐枣,此時可以主動滑動來消費掉發(fā)起方提供的未消費完剩下的距離
         */
        public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed);
    
        /**
        * 在嵌套的層級中马靠,當嵌套的子view滑動時候,我們想在他之前先讓parent來滑動蔼两,就執(zhí)行這個操作。
         */
        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    
        /**
        *parent實現(xiàn)一定的滑翔處理
         */
        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 {
    
    /**
     * 設(shè)置child支持嵌套滑動,表示是否支持滾動的時候是否將發(fā)給parent.
     */
    public void setNestedScrollingEnabled(boolean enabled);

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

    /**
     *child 開始著手觸發(fā)嵌套活動了
     */
    public boolean startNestedScroll(int axes);

    /**
     * child開始想要停止嵌套滑動了,與startNestedScroll對應(yīng)馆匿,由他發(fā)起自然要由他結(jié)束了抑胎。
     */
    public void stopNestedScroll();

    /**
     * 在child自身滾動之后分發(fā)剩余的未消費滑動距離
     */
    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的關(guān)系圖


    圖片來自http://www.reibang.com/p/490659fae773
  1. NestedScrollingParentHelper:嵌套滾動的parent輔助類, 只是設(shè)計的方便阿逃,里面并沒有做什么實際的動作。

  2. NestedScrollingChildHelper:嵌套滾動的發(fā)起方child, 下面列出幾個關(guān)鍵的方法

    
    //當child滾動的時候,會調(diào)用該方法恃锉,找到可以接受嵌套去滾動的父容器搀菩, true表示找到了,false表示沒有找到
    public boolean startNestedScroll(int axes) {
        //如果已經(jīng)有了破托,直接返回
        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;
                }
                //沒找到繼續(xù)向上遍歷土砂。
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }
    
    //在嵌套滾動的時候州既,child在自己滾動前會先問問他的parent要不要先滾動下,是通過該方法來實現(xiàn)的萝映。
    //
    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. 嵌套在系統(tǒng)中的應(yīng)用:NestedScrollView作為嵌套的parent, RecyclerView作為嵌套滾動的child的場景
  1. NestedScrollView:他既充當著嵌套滾動的父view,(其實也可同時充當著嵌套滾動的子child) 這里就看看作為parent實現(xiàn)了的NestedScrollingParent的相關(guān)接口吧, 接受嵌套child發(fā)起的滾動的操作都會在下面的接口中進行動作啦:
 @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, 將嵌套事件發(fā)給他的parent中去;是一種遞歸嵌套
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
          //NestedScrollView同時也作為child,將嵌套滾動發(fā)給他的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;
        //將自己未消耗完的距離繼續(xù)遞歸地給到他的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中的攔截和消耗事件對嵌套滾動原則的相關(guān)處理,看看onInterceptTouchEvent和onTouchEvent.

    • onInterceptTouchEvent, 如何攔截的呢箩绍,看源碼注釋解讀
    //返回true就是攔截下來, false就是不攔截
    public boolean onInterceptTouchEvent(MotionEvent ev) {
      
      final int action = ev.getAction();
        //如果當前是move,并且當前NestedScrollView處于了滾動狀態(tài)孔庭,就返回true.滾動事件不會下去,所以他的子view沒法發(fā)起嵌套滾動的操作材蛛。
         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壓根就不支持嵌套滾動,那么就表示自己要來實現(xiàn)滾動啦卑吭。這時候后面的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內(nèi)部,啥都不做豆赏,沒法滾動
                 if (!inChild((int) ev.getX(), (int) y)) {
                     mIsBeingDragged = false;
                     recycleVelocityTracker();
                     break;
                 }
    
            
                 mLastMotionY = y;
                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
             
                 //建立速度跟蹤挣菲,然后跟蹤手勢
                 initOrResetVelocityTracker();
                 mVelocityTracker.addMovement(ev);
                 //計算滾動
                 mScroller.computeScrollOffset();
                 //滾動沒結(jié)束富稻,mIsBeingDragged為true.
                 mIsBeingDragged = !mScroller.isFinished();
                 //作為嵌套的child, 發(fā)起滾動請求
                 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                 break;
             }
    
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
                 //mIsBeingDragged清除掉狀態(tài), 
                 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:如何響應(yīng)的呢唉窃,看源碼注釋解讀

       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);
                   //這里是和嵌套滾動相關(guān)的地方,作為嵌套的child, 發(fā)起縱向的滾動請求
                   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表示要自己來滾動對應(yīng)的距離啦笋额,并不一定會滾動完所有的剩余距離
                       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)) {
                           //該方法會做嵌套滑行分發(fā)茉盏,也就是當錢view支持滑行的時候也會給parent-view去滑行一下,不過他們沒有做距離和速度分減少枢冤,也不好做因為他們都是根據(jù)最后的初始速度去減速滑行的鸠姨。只是對應(yīng)的parent可以根據(jù)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來發(fā)起的然后parent響應(yīng)巍糯,onTouchEvent自然是動作發(fā)起的地方,所以這里基本就是child的動作行為客扎。我們在認識傳統(tǒng)事件分發(fā)的時候祟峦,知道滾動這些move操作當前只能給某個view去消耗,沒法給多個人使用的徙鱼,而嵌套滾動卻可以搀愧,在這里總結(jié)下他的實現(xiàn),他在move的時候先將滾動距離通過dispatchNestedPreScroll傳遞給實現(xiàn)了NestedScrollingParent的接口的parent, 讓他先滾動滾動疆偿,然后扣除parent滾動過的距離,接著自己再調(diào)用overScrollByCompat搓幌,NestedScrollView自己來滾動杆故,如果還有剩余又調(diào)用dispatchNestedScroll, 繼續(xù)讓parent去滾動。在手指抬起的時候如果有滑行操作溉愁,也會把滑行速度傳遞父parent处铛,父parent可以自行決定要不要進行滑行饲趋。大概就是這么個邏輯,實現(xiàn)了多個view來消耗一次手勢操作呢撤蟆。

  1. RecyclerView, 他只能作為嵌套的子child, 即實現(xiàn)NestedScrollingChild奕塑,而沒能做parent. 就來看看他的onInterceptTouchEvent和onTouchEvent,是如何處理嵌套相關(guān)的行為吧家肯。感覺應(yīng)該和NestedScrollView應(yīng)該是很相似的邏輯的哦

    • 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的時候發(fā)起嵌套滾動請求
                 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;
     }
    
    
    
    • 總結(jié)一下,從recyclerView的攔截方法中可以看出讨衣,其實和嵌套滾動操作的內(nèi)容是很少的换棚,只有在down的時候發(fā)起一下嵌套操作startNestedScroll,在up的時候停止嵌套滾動反镇,告知到他的父容器固蚤,比如NestedScrollView。那么就看看他的其他關(guān)于攔截的邏輯吧歹茶,只要在拖拽的過程中夕玩,就會攔截下來,那么他的子view一般在這里就沒法響應(yīng)觸摸事件啦惊豺。
  • 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系統(tǒng)也是這么做的哦扮叨,也擔心上面被攔了
                        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;
                }
                //根據(jù)是橫向的還是豎向的啟動嵌套滾動
                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];
                }
    
                //這里應(yīng)該是一個設(shè)定缤弦,只要我們的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;
                    }
                    //設(shè)置滾動態(tài)
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }
    
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
    
                    //scrollByInternal自己滾動一段距離碍沐,并且內(nèi)部還會將剩下的距離又傳遞給parent.
                    //以后可以去查看該方法的實現(xiàn)。
                    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操作衷蜓,在這里處理嵌套滑行的行為累提,可以查看里面的方法細節(jié)
                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;
    }
    
  • 總結(jié)一下,RecyclerView的onTouchEvent和NestedScrollView的邏輯很相似磁浇,二者在這個區(qū)間里表現(xiàn)的都是一個嵌套child的行為斋陪,在down的時候發(fā)起,在move先傳遞給parent, 然后自己消耗置吓。大概就這樣子吧无虚。

3.  嵌套存在著的問題,以及造成的原因
  1. NestedScrollView/ScrollView嵌套ListView顯示不全衍锚,經(jīng)常顯示一行問題!

    • 原因在哪里呢友题,如下:
    ---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模式踢匣,他設(shè)置的高度就是單個條目加上padding距離啊戈抄!所以就顯示了一行......但是如果我們用其他的布局嵌套listView的時候离唬,一般是不會傳遞UNSPECIFIED的規(guī)格的,所以沒問題划鸽。
            if (heightMode == MeasureSpec.UNSPECIFIED) {
                heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                        getVerticalFadingEdgeLength() * 2;
            }
    
            setMeasuredDimension(widthSize, heightSize);
    
            mWidthMeasureSpec = widthMeasureSpec;
        }   
    
    • 解決输莺, 重寫LinearLayout的onMeasure方法,改寫ScrollView傳進來的測量規(guī)格哦漾稀,雖然解決了顯示不全的問題模闲,但是復用規(guī)則被打破!這不是好的辦法崭捍。
      @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //改寫規(guī)格尸折,將高度設(shè)置成無限。因此也就造成了一開始就全部展開殷蛇,無法復用listView的單元控件实夹。重要弊端!
            int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
            super.onMeasure(widthMeasureSpec, heightSpec);
        }
    
    1. NestedScrollView與RecyclerView嵌套粒梦,RecyclerView不能被重復利用
    • 原因亮航,還是看代碼吧:
    --- LineaLayoutManager
    //當layoutState.mInfinite為true的時候,會一直調(diào)用layoutChunk匀们,從而讓所有的itemView一次性全部創(chuàng)建了缴淋。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);
                 }
    
    
    • 總結(jié)泄朴,上面兩個都有復用規(guī)則打破的問題重抖,這是個大問題,在少量數(shù)據(jù)還好祖灰,數(shù)據(jù)多了就會出現(xiàn)crash的钟沛,所以利用NestedScrollView+RecyclerView的去實現(xiàn)復雜界面并沒有好的實現(xiàn)策略。雖然系統(tǒng)對二者都實現(xiàn)了嵌套滾動的策略局扶,看上去處理的很好恨统,然而卻是存在著巨大的bug, google也推薦我們不要這么搞,但是實際有這樣的需求啊, 感覺google這里好坑啊!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末三妈,一起剝皮案震驚了整個濱河市畜埋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌畴蒲,老刑警劉巖由捎,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異饿凛,居然都是意外死亡狞玛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門涧窒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來心肪,“玉大人,你說我怎么就攤上這事纠吴∮舶埃” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵戴已,是天一觀的道長固该。 經(jīng)常有香客問我,道長糖儡,這世上最難降的妖魔是什么伐坏? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮握联,結(jié)果婚禮上桦沉,老公的妹妹穿的比我還像新娘。我一直安慰自己金闽,他們只是感情好纯露,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著代芜,像睡著了一般埠褪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挤庇,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天钞速,我揣著相機與錄音,去河邊找鬼罚随。 笑死玉工,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的淘菩。 我是一名探鬼主播遵班,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼潮改!你這毒婦竟也來了狭郑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤汇在,失蹤者是張志新(化名)和其女友劉穎翰萨,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糕殉,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡亩鬼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年殖告,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雳锋。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡黄绩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出玷过,到底是詐尸還是另有隱情爽丹,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布辛蚊,位于F島的核電站粤蝎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏袋马。R本人自食惡果不足惜初澎,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望飞蛹。 院中可真熱鬧谤狡,春花似錦、人聲如沸卧檐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽霉囚。三九已至捕仔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盈罐,已是汗流浹背榜跌。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盅粪,地道東北人钓葫。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像票顾,于是被迫代替她去往敵國和親础浮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345