Android嵌套滑动机制分析

简介: Android嵌套滑动机制分析

温馨提示:Android事件分发机制是Android嵌套滑动机制的基石,阅读本文前请务必保证熟练掌握Android事件分发机制。传送门->事件分发四步曲之一《深度遍历讲解Android事件分发机制》


本文大纲


1. 嵌套ScrollView同向滑动

2. 传统事件分发和嵌套滑动事件分发

3. NestedScrollingChild&NestedScrollingParent

4. 结合案例讲解嵌套滑动事件分发顺序


1. 嵌套ScrollView同向滑动


所谓嵌套ScrollView同向滑动,是指两个可滑动的View内外嵌套,并且他们的方向是相同的。

640.jpg


当在内部ScrollView中上下滑动时,会有两种情况。


  1. 外部ScrollView优先获取上下滑动的权力,在蓝色区域上下滑动,内部ScrollView并不会上下滑动

image.png


内部的ScrollView优先获取上下滑动的权力,在蓝色区域内上下滑动,内部ScrollView上下滑动


image.png


在Android未提供嵌套滑动机制之前,嵌套ScrollView同向滑动的效果是情况一,内部ScrollView优先获得DOWN事件的处理机会,外部的ScrollView优先获得MOVE事件的处理机会(yDiff > mTouchSlop)。原因如下:


当外部ScrollView嵌套内部ScrollView时,DOWN事件在内部ScrollView的onTouchEvent中返回true,内部ScrollView处理了DOWN事件。MOVE事件首先到达外部ScrollView的onInterceptTouchEvent方法,当滑动距离大于mTouchSlop时,会拦截掉MOVE事件,给内部ScrollView发出CANCEL事件,从而外部ScrollView获得了事件的处理权,内部ScrollView失去了事件的处理权。


ScrollView#onTouchEvent方法返回true。根据传统事件分发机制,内部ScrollView优先获得DOWN事件的处理机会。


 //ScrollView#onTouchEvent
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        MotionEvent vtev = MotionEvent.obtain(ev);
        final int actionMasked = ev.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                /*
                 * If being flinged and user touches, stop the fling. isFinished
                 * will be false if being flinged.
                 */
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    if (mFlingStrictSpan != null) {
                        mFlingStrictSpan.finish();
                        mFlingStrictSpan = null;
                    }
                }
                // Remember where the motion event started
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                startNestedScroll(SCROLL_AXIS_VERTICAL);
                break;
            }
            //此处省略其它事件的代码
        }
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();
        return true;
    }


关于MOVE事件,首先会调用外部ScrollView的onInterceptTouchEvent方法,如果返回false,MOVE事件会调用内部ScrollView的onTouchEvent方法,如果返回true,会调用外部ScrollView的onTouchEvent方法。当yDiff > mTouchSlop时,方法返回true。

public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
        if (super.onInterceptTouchEvent(ev)) {
            return true;
        }
        if (getScrollY() == 0 && !canScrollVertically(1)) {
            return false;
        }
        switch (action & MotionEvent.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 = ev.findPointerIndex(activePointerId);
                if (pointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + activePointerId
                            + " in onInterceptTouchEvent");
                    break;
                }
                final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                    initVelocityTrackerIfNotExists();
                    mVelocityTracker.addMovement(ev);
                    mNestedYOffset = 0;
                    if (mScrollStrictSpan == null) {
                        mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                    }
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            }
            //此处省略其它事件的代码
        }
        return mIsBeingDragged;
    }


这种效果显然不能满足用户的需求。内部的ScrollView永远无法获得上下滑动的机会。用户希望在内部ScrollView的区域上下滑动时,内部ScrollView优先获得事件的分发权力。Android嵌套滑动机制,很好的解决了ScrollView嵌套时内部ScrollView无法优先处理滑动事件的问题。


2. 传统事件分发和嵌套滑动事件分发


传统事件分发在事件分发四步曲之一《深度遍历讲解Android事件分发机制》中已经详细讲解过,这里不做详细的讲解。事件会调用父控件的onInterceptTouchEvent方法,判断是否拦截事件。换言之传统的事件分发,生杀大权掌握在父控件上,只有父控件不拦截事件,子控件才有处理事件的机会。


传统事件分发树形图如下

image.png


而嵌套滑动事件分发则区别于传统事件分发,事件分发的生杀大权并不掌握在父控件上。当DOWN事件传递到NestedScrollingChild时,NestedScrollingChild控件会向上寻找愿意响应后代View发出嵌套滑动请求的NestedScrollingParent控件,告诉NestedScrollingParent控件,后续的MOVE事件,不要再拦截,事件优先交给NestedScrollingChild处理,如果MOVE滑动超过mTouchSlop阈值,NestedScrollingChild向上请求"不拦截事件"->"parent.requestDisallowInterceptTouchEvent(true)",从此NestedScrollingChild的所有父控件,对事件全部不拦截,直达NestedScrollingChild控件。


嵌套滑动事件分发树形图如下

image.png


总结:传统事件分发,父View会优先获取事件的分发权,并且如果事件被某个View处理了,那么它的父View无法获得事件分发的机会。嵌套滑动事件分发,NestedScrollingChild处理DOWN事件时,会告诉它的NestedScrollingParent,后续的MOVE、UP等事件,NestedScrollingParent不拦截,直接交由NestedScrollingChild处理,而且它的父View有机会再次获得事件分发的机会。


3. NestedScrollingChild&NestedScrollingParent


嵌套滑动机制提供了NestedScrollingChild和NestedScrollingParent接口。

image.jpeg

image.jpeg


定义控件节点如下

640.png

  • NestedScrollingChild节点表示实现了NestedScrollingChild接口的View或者ViewGroup
  • NestedScrollingParent节点表示实现了NestedScrollingParent接口的ViewGroup
  • Both 表示两个接口都实现了的ViewGroup


NSC和NSP是成对出现的,一个NestedScrollingChild只能对应一个NestedScrollingParent,而一个NestedScrollingParent可以对应多个NestedScrollingChild。一个控件即可以同时实现NSC和NSP接口,它对于父的NSP控件而言是一个NSC可以主动发起嵌套滑动的请求,对于它的NSC控件而言是一个NSP,它可以响应后代控件发起的嵌套滑动请求。当NSC的滑动嵌套请求(startNestedScroll)被NSP响应了(onStartNestedScroll返回true)称NSC和NSP为pair(天生一对),NSC可能和最近的祖先NSP组成Pair,也可能与非最近的祖先NSP组成pair。下面我们分析几个Case。


  1. NestedScrollingParent和NestedScrollingChild是有效的pair

640.png


NestedScrollingParent和NestedScrollingChild是无效的pair

image.png


NestedScrollingParent2和NestedScrollingChild是有效的pair,NestedScrollingParent1和NestedScrollingChild也有可能是有效的pair(当NSP2不响应嵌套滑动,而NSP1响应 好好思考一下 下面的Case相同,不表)

image.png



NestedScrollingParent和Both是有效的pair,Both和NestedScrollingChild是有效的pair

640.png


NestedScrollingParent和NestedScrollingChild是有效的pair,尽管中间隔了一个普通的ViewGroup

image.png


NestedScrollingParent和NestedScrollingChild1是有效的pair,NestedScrollingParent和NestedScrollingChild2是有效的pair

640.png


NestedScrollingChild方法以及作用


  1. 控制嵌套滑动机制是否开启->NestedScrollingChild#setNestedScrollingEnable和NestedScrollingChild#isNestedScrollingEnable。
 //View.java
  public void setNestedScrollingEnabled(boolean enabled) {
      if (enabled) {
          mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED;
      } else {
          stopNestedScroll();
          mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED;
      }
  }
  public boolean isNestedScrollingEnabled() {
      return (mPrivateFlags3 & PFLAG3_NESTED_SCROLLING_ENABLED) ==
              PFLAG3_NESTED_SCROLLING_ENABLED;
  }

通知NestedScrollingParent不拦截MOVE、UP等事件->NestedScrollingChild#startNestedScroll。

//View.java
public boolean startNestedScroll(int axes) {
    //如果已经找到的NSP返回
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    //如果开启了嵌套滑动
    if (isNestedScrollingEnabled()) {
        //往上查找NestedScrollingParent
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            try {
              //如果NSP的onStartNestedScroll返回true,表示响应NSC的嵌套滑动
                if (p.onStartNestedScroll(child, this, axes)) {
                    //设置NSC的mNestedScrollingParent对象,下次不用再查找了
                    mNestedScrollingParent = p;
                    p.onNestedScrollAccepted(child, this, axes);
                    return true;
                }
            } catch (AbstractMethodError e) {
                Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
                        "method onStartNestedScroll", e);
                // Allow the search upward to continue
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    //如果没找到返回false
    return false;
}

640.png


如上图,假设NestedScrollingParent2的onStartNestedScroll返回false,NestedScrollingParent1的onStartNestedScroll返回true。当NestedScrollingChild调用startNestedScroll方法时。NestedScrollingParent1和NestedScrollingChild是有效的Pair


调用时机,以ScrollView为例子,ScrollView实现了NSC接口,一般在Down事件中调用,或者在NSP的onNestedScrollAccepted中


  • 调用时机1 ScrollView#onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        return true;
    }
    if (super.onInterceptTouchEvent(ev)) {
        return true;
    }
    if (getScrollY() == 0 && !canScrollVertically(1)) {
        return false;
    }
    switch (action & MotionEvent.ACTION_MASK) {
        //这里省略其它事件
        case MotionEvent.ACTION_DOWN: {
            final int y = (int) ev.getY();
            if (!inChild((int) ev.getX(), (int) y)) {
                mIsBeingDragged = false;
                recycleVelocityTracker();
                break;
            }
            mLastMotionY = y;
            mActivePointerId = ev.getPointerId(0);
            initOrResetVelocityTracker();
            mVelocityTracker.addMovement(ev);
            mScroller.computeScrollOffset();
            mIsBeingDragged = !mScroller.isFinished();
            if (mIsBeingDragged && mScrollStrictSpan == null) {
                mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
            }
            //调用时机1
            startNestedScroll(SCROLL_AXIS_VERTICAL);
            break;
        }
    }
    return mIsBeingDragged;
}

调用时机2 ScrollView#onTouchEvent

  public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        //省略代码
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    if (mFlingStrictSpan != null) {
                        mFlingStrictSpan.finish();
                        mFlingStrictSpan = null;
                    }
                }
                // Remember where the motion event started
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                //调用时机2
                startNestedScroll(SCROLL_AXIS_VERTICAL);
                break;
            }
        }
        //省略代码
        return true;
    }

调用时机3 ScrollView#onNestedScrollAccepted


public void onNestedScrollAccepted(View child, View target, int axes) {
    super.onNestedScrollAccepted(child, target, axes);
    //调用时机3。NSP往上找它的NSP 对应Case4中的Both往上找NSP
    startNestedScroll(SCROLL_AXIS_VERTICAL);
}

理滑动事件之前,将滑动事件的处理权交由NestedScrollingParent处理->(NestedScrollingChild#dispatchNestedPreScroll)。(这里叨唠一下,子View虽然优先获取了事件分发的权力,但是在滑动之前,还是会把事件交给父View去处理。传统事件分发,父View如果获取了分发权,子View就无法获取分发权。滑动嵌套即使子View获取了分发权,它还是会征询父View,是否要处理掉事件的一部分滑动,父View可以选择处理或者不处理,子View再处理事件剩余滑动距离)。


//View.java
public boolean dispatchNestedPreScroll(int dx, int dy,
        @Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        if (dx != 0 || dy != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                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;
            mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
            if (offsetInWindow != null) {
                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;
}

调用时机,一般在NSC真正处理MOVE事件之前


ScrollView#onTouchEvent


public boolean onTouchEvent(MotionEvent ev) {
    initVelocityTrackerIfNotExists();
    MotionEvent vtev = MotionEvent.obtain(ev);
    final int actionMasked = ev.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        mNestedYOffset = 0;
    }
    vtev.offsetLocation(0, mNestedYOffset);
    switch (actionMasked) {
        case MotionEvent.ACTION_MOVE:
            final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
            if (activePointerIndex == -1) {
                Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                break;
            }
            final int y = (int) ev.getY(activePointerIndex);
            int deltaY = mLastMotionY - y;
            //调用时机,在NSC真正处理MOVE事件之前
            if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                deltaY -= mScrollConsumed[1];
                vtev.offsetLocation(0, mScrollOffset[1]);
                mNestedYOffset += mScrollOffset[1];
            }
            //伪代码 真正处理MOVE事件
            //if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
            //               && !hasNestedScrollingParent()) {
            //           
            //}
             //当NSC滚动到底或者到顶了,unconsumedY不为0
             //if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
             //          mLastMotionY -= mScrollOffset[1];
            //          vtev.offsetLocation(0, mScrollOffset[1]);
            //           mNestedYOffset += mScrollOffset[1];
            // } 
            break;
    }
    return true;
}

NestedScrollingChild处理完滑动事件后,如果有剩余的滑动距离没有处理掉,会交给NestedScrollingParent去处理剩余的滑动距离->NestedScrollingChild#dispatchNestedScoll,如果NestedScrollingParent处理了剩余的距离,NestedScrollingChild会校正mLastMotionY。

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable @Size(2) int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            //NSC如果有滑动距离没消耗掉,会交给Pair对应的NSP去处理
            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }
                mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
                if (offsetInWindow != null) {
                    getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

调用时机有两个

  • 调用时机1 ScrollView#onTouchEvent中 NSC处理完事件后,调用


public boolean onTouchEvent(MotionEvent ev) {
    initVelocityTrackerIfNotExists();
    MotionEvent vtev = MotionEvent.obtain(ev);
    final int actionMasked = ev.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        mNestedYOffset = 0;
    }
    vtev.offsetLocation(0, mNestedYOffset);
    switch (actionMasked) {
        case MotionEvent.ACTION_MOVE:
            final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
            if (activePointerIndex == -1) {
                Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                break;
            }
            final int y = (int) ev.getY(activePointerIndex);
            int deltaY = mLastMotionY - y;
            if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                deltaY -= mScrollConsumed[1];
                vtev.offsetLocation(0, mScrollOffset[1]);
                mNestedYOffset += mScrollOffset[1];
            }
            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) {
                // Scroll to follow the motion event
                mLastMotionY = y - mScrollOffset[1];
                final int oldY = mScrollY;
                final int range = getScrollRange();
                final int overscrollMode = getOverScrollMode();
                boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
                // Calling overScrollBy will call onOverScrolled, which
                // calls onScrollChanged if applicable.
                if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
                        && !hasNestedScrollingParent()) {
                    // Break our velocity if we hit a scroll barrier.
                    mVelocityTracker.clear();
                }
                final int scrolledDeltaY = mScrollY - oldY;
                final int unconsumedY = deltaY - scrolledDeltaY;
                #调用时机1
                if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                    mLastMotionY -= mScrollOffset[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                } else if (canOverscroll) {
                    final int pulledToY = oldY + deltaY;
                    if (pulledToY < 0) {
                        mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                                ev.getX(activePointerIndex) / getWidth());
                        if (!mEdgeGlowBottom.isFinished()) {
                            mEdgeGlowBottom.onRelease();
                        }
                    } else if (pulledToY > range) {
                        mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                                1.f - ev.getX(activePointerIndex) / getWidth());
                        if (!mEdgeGlowTop.isFinished()) {
                            mEdgeGlowTop.onRelease();
                        }
                    }
                    if (mEdgeGlowTop != null
                            && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                        postInvalidateOnAnimation();
                    }
                }
            }
            break;
    }
    if (mVelocityTracker != null) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();
    return true;
}


调用时机2 ScrollView#onNestedScroll NSP处理完剩余的滚动之后,如果还有剩余滚动距离


public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
        int dxUnconsumed, int dyUnconsumed) {
    final int oldScrollY = mScrollY;
    scrollBy(0, dyUnconsumed);
    final int myConsumed = mScrollY - oldScrollY;
    final int myUnconsumed = dyUnconsumed - myConsumed;
    dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}
  1. fling的逻辑比较特殊,这里以ScrollView为例,先把fling交给NSP的onNestedPreFling处理,如果返回true结束,如果返回false,先判断NSC能否滑动,如果能交给NSC处理,如果不能让NSP重复该步骤。这里有一个递归的逻辑


 public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        return mNestedScrollingParent.onNestedPreFling(this, velocityX, velocityY);
    }
    return false;
}
 private void flingWithNestedDispatch(int velocityY) {
        final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
                (mScrollY < getScrollRange() || velocityY < 0);
        if (!dispatchNestedPreFling(0, velocityY)) {
            dispatchNestedFling(0, velocityY, canFling);
            if (canFling) {
                fling(velocityY);
            }
        }
    }

NestedScrollingChild事件处理步骤


  1. 获取事件的优先处理权-> 调用startNestedScroll


  1. 处理滑动之前先问NesteScrollingParent要不要处理一部分距离


  1. NesteScrollingParent处理完之后,NestedScrollingChild再来处理剩余的距离


  1. NestedScrollingChild处理完之后还有剩余的距离,交给NesteScrollingParent去处理

NestedScrollingParent接口只能被ViewGroup实现。View实现该接口没有意义。NestedScrollingParent主要有两个作用。作用一、响应NesteScrollingChild的嵌套滑动请求,配合NesteScrollingChild完成嵌套滑动机制的交互。作用二、处理NestedScrollingChild滑动前和滑动后传递过来的事件剩余距离。

NestedScrollingParent方法以及作用


  1. getNestedScrollAxes返回嵌套滑动的方向,垂直方向或者水平方向。NestedScrollingParent正是通过该标志位,决定要不要拦截MOVE事件


  1. onNestedScrollAccepted在onStartNestedScroll返回true时,会被调用,该方法体会给mNestedScrollAxes赋值。
//ViewGroup.java
public int getNestedScrollAxes() {
    return mNestedScrollAxes;
}
//ScrollView.java
public void onNestedScrollAccepted(View child, View target, int axes) {
     super.onNestedScrollAccepted(child, target, axes);
     startNestedScroll(SCROLL_AXIS_VERTICAL);
}

onStartNestedScroll():Boolean方法与NestedScrollingChild#startNestedScroll方法相对应。如果返回true表示NestedScrollingParent响应NestedScrollingChild的嵌套滑动,反之表示不响应嵌套滑动。一般做法是外部ScrollView滑动方向和内部ScrollView滑动方向一致返回true。


//ScrollView.java
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
}

onNestedPreScroll方法会在NestedScrollingChild正式处理滑动事件之前调用。

//ScrollView#onNestedPreScroll 默认分发给父NSP
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    dispatchNestedPreScroll(dx, dy, consumed, null);
}

onNestedScroll方法会在NestedScrollingChild正式处理滑动事件之后调用


//ScrollView.java
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed) {
        final int oldScrollY = mScrollY;
        //NSP先滑动NSC未消费完的
        scrollBy(0, dyUnconsumed);
        final int myConsumed = mScrollY - oldScrollY;
        final int myUnconsumed = dyUnconsumed - myConsumed;
        //还有没消费完的继续向上分发
        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
    }
  1. onNestedPreFling与onNestedPreScroll类似


  1. onNesteFling如果NSC不可滑动了,则NSP分发。分发逻辑前面已经讲过
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        if (!consumed) {
            flingWithNestedDispatch((int) velocityY);
            return true;
        }
        return false;
}


onStopNestedScroll会向上递归调用stopNestedScroll,mNestedScrollAxes置为0


public void onStopNestedScroll(View child) {
        // Stop any recursive nested scrolling.
        stopNestedScroll();
        mNestedScrollAxes = 0;
    }

4.结合案例讲解嵌套滑动事件分发顺序


image.png


上述效果代码如下


自定义的ScrollView,主要是在相关事件方法中加入日志打印

640.jpg


Activity代码

640.jpg

布局文件

640.jpg


我们把布局文件转化成树形图

640.png

关于树中的节点,介绍如下

640.png

日志打印如下

640.jpg

OuterScrollView拦截MOVE事件条件:滑动距离>mTouchSlop&&OuterScrollView未响应嵌套滑动机制。


  1. DOWN事件到达OuterScrollView#onInterceptTouchEvent,不拦截。
  2. DOWN事件到达InnerScrollView#onInterceptTouchEvent,接着调用InnerScrollView#startNestedScroll,触发OuterScrollView#onStartNested,接着调用InnerScrollView#dispatchTouchEvent,由于是DOWN事件,会调用View#dispatchTouchEvent,调用stopNestedScroll,触发OuterScrollView#onStopNestedScroll。
  3. DOWN事件到达InnerScrollView#onTouchEvent方法,调用startNestedScroll,触发OuterScrollView#onStartNestedScroll,OuterScrollView响应嵌套滑动机制。
  4. MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
  5. MOVE事件到达InnerScrollView#onTouchEvent,调用InnerScrollView#dispatchNestedPreScroll,由于dy=0 dx=0,不会触发OuterScrollView的onNestedPreScroll。
  6. MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
  7. MOVE事件到达InnerScrollView#onTouchEvent,dy<mTouchSlop,只会触发InnerScrollView#dispatchNestedPreScroll,不会触发dispatchNestedScroll。
  8. MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
  9. MOVE事件到达InnerScrollView#onTouchEvent,dy<mTouchSlop,只会触发InnerScrollView#dispatchNestedPreScroll,不会触发dispatchNestedScroll。
  10. MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
  11. MOVE事件到达InnerScrollView#onTouchEvent,dy<mTouchSlop,只会触发InnerScrollView#dispatchNestedPreScroll,不会触发dispatchNestedScroll。
  12. MOVE事件到达OuterScrollView#onInterceptTouchEvent,拦截条件不成立,不拦截。
  13. MOVE事件到达InnerScrollView#onTouchEvent,(dy=31)>mTouchSlop,会同时触发InnerScrollView#dispatchNestedPreScroll和dispatchNestedScroll,同时会调用parent.requestDisallowInterceptTouchEvent(true)。从此MOVE事件直达InnerScrollView#onTouchEvent
  14. MOVE事件直达InnerScrollView#onTouchEvent
  15. MOVE事件直达InnerScrollView#onTouchEvent
  16. 处理UP事件


总结DOWN事件和MOVE事件在NestedScrollingChild中的分发逻辑

  1. DOWN事件到达NestedScrollingChild#onInterceptTouchEvent方法会调用startNestedScroll方法。


  1. DOWN事件到达NestedScrollingChild#dispathTouchEvent方法会调用stopNestedScroll方法。


  1. MOVE事件到达NestedScrollingChild#onTouchEvent方法,调用dispatchNestedPreScroll方法。如果mIsBeingDragged为true。会调用dispatchNestedScroll方法。从非dragged状态变成dragged状态时,会调用ViewParent#requestDisallowInterceptTouchEvent(true)。


相关文章
|
20天前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
86 4
|
21天前
|
算法 Linux 调度
深入探索安卓系统的多任务处理机制
【10月更文挑战第21天】 本文旨在为读者提供一个关于Android系统多任务处理机制的全面解析。我们将从Android操作系统的核心架构出发,探讨其如何管理多个应用程序的同时运行,包括进程调度、内存管理和电量优化等方面。通过深入分析,本文揭示了Android在处理多任务时所面临的挑战以及它如何通过创新的解决方案来提高用户体验和设备性能。
34 1
|
26天前
|
存储 安全 Android开发
探索Android与iOS的隐私保护机制
在数字化时代,移动设备已成为我们生活的一部分,而隐私安全是用户最为关注的问题之一。本文将深入探讨Android和iOS两大主流操作系统在隐私保护方面的策略和实现方式,分析它们各自的优势和不足,以及如何更好地保护用户的隐私。
|
1月前
|
安全 Android开发 数据安全/隐私保护
深入探讨iOS与Android系统安全性对比分析
在移动操作系统领域,iOS和Android无疑是两大巨头。本文从技术角度出发,对这两个系统的架构、安全机制以及用户隐私保护等方面进行了详细的比较分析。通过深入探讨,我们旨在揭示两个系统在安全性方面的差异,并为用户提供一些实用的安全建议。
|
12天前
|
Java 开发工具 Android开发
安卓与iOS开发环境对比分析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自占据半壁江山。本文深入探讨了这两个平台的开发环境,从编程语言、开发工具到用户界面设计等多个角度进行比较。通过实际案例分析和代码示例,我们旨在为开发者提供一个清晰的指南,帮助他们根据项目需求和个人偏好做出明智的选择。无论你是初涉移动开发领域的新手,还是寻求跨平台解决方案的资深开发者,这篇文章都将为你提供宝贵的信息和启示。
24 8
|
2月前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
69 15
Android 系统缓存扫描与清理方法分析
|
20天前
|
Linux Android开发 iOS开发
深入探索Android与iOS的多任务处理机制
在移动操作系统领域,Android和iOS各有千秋,尤其在多任务处理上展现出不同的设计理念和技术实现。本文将深入剖析两大平台在后台管理、资源分配及用户体验方面的策略差异,揭示它们如何平衡性能与电池寿命,为用户带来流畅而高效的操作体验。通过对比分析,我们不仅能够更好地理解各自系统的工作机制,还能为开发者优化应用提供参考。
|
16天前
|
安全 Android开发 数据安全/隐私保护
深入探索Android与iOS系统安全性的对比分析
在当今数字化时代,移动操作系统的安全已成为用户和开发者共同关注的重点。本文旨在通过比较Android与iOS两大主流操作系统在安全性方面的差异,揭示两者在设计理念、权限管理、应用审核机制等方面的不同之处。我们将探讨这些差异如何影响用户的安全体验以及可能带来的风险。
21 1
|
2月前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
2月前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
58 1