Android嵌套滑动上篇

本片文章学习分析一下Android中的嵌套滑动。涉及到的类有

  • NestedScrollingChild
  • NestedScrollingParent
  • ViewParent
  • RecyclerView

下一篇会分析NestedScrolling2相关的内容。

为了方便叙述,我们把在内部的滑动View称为内层控件。外部滑动的控件称为外层控件。

内层控件需要实现NestedScrollingChild接口。比如RecyclerView是实现了NestedScrollingChild接口的。

Build.VERSION.SDK_INT小于21版本以下,外层控件必须实现NestedScrollingParent接口。在21及以上版本,外层控件不再需要实现NestedScrollingParent接口,因为ViewParent接口中已经定义了嵌套滑动外层控件相关的方法,ViewGroup实现了ViewParent接口。

本篇文章会实现NestedScrollingParent接口来自定义一个外层控件StickyNavLayout。内层控件,我们直接使用RecyclerView。最终实现的一个效果图如下所示:

嵌套滑动版本1.gif

GitHub源码

我们看到这个滑动效果并不是很流畅,下一篇会分析NestedScrolling2相关的内容就可以解决。

嵌套滑动的整个流程是从内层控件发起的。先看一下流程图。

Android嵌套滑动上篇_第1张图片
NestedScrolling流程.jpg

我们以RecyclerView为例进行分析。注意:我们的是android-25版本里面的RecyclerView源码。

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
    //...
}

RecyclerView的onTouchEvent方法精简版

@Override
public boolean onTouchEvent(MotionEvent e) {
    //...
    final MotionEvent vtev = MotionEvent.obtain(e);
    final int action = MotionEventCompat.getActionMasked(e);
    final int actionIndex = MotionEventCompat.getActionIndex(e);

    if (action == MotionEvent.ACTION_DOWN) {
        mNestedOffsets[0] = mNestedOffsets[1] = 0;
    }
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            //...
            //注释1处
            startNestedScroll(nestedScrollAxis);
        } break;
        case MotionEvent.ACTION_MOVE: {
            final int index = e.findPointerIndex(mScrollPointerId);
               
            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
            //注释2处
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                //注释3处
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // Updated the nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }
            //...
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
                //注释4处
                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);
            //计算fling速度
            final float xvel = canScrollHorizontally ?
                    -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
            final float yvel = canScrollVertically ?
                    -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
            //注释5处
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            //注释6处
            resetTouch();
        } break;           
    }
    //...
    return true;
}

ACTION_DOWN的注释1处

//注释1处
startNestedScroll(nestedScrollAxis);

这里开始嵌套滑动的整个流程。

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

调用NestedScrollingChildHelper的startNestedScroll方法。

/**
 *
 * @param axes 支持嵌套滚动的方向。
 *             
 * @return true 如果找到了协作的嵌套滑动的父控件并成功启动嵌套滑动则返回true。
 */
public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // 已经在嵌套滑动中了,直接返回true
        return true;
    }
    //启用了嵌套滑动
    if (isNestedScrollingEnabled()) {
        //获取父控件
        ViewParent p = mView.getParent();
        View child = mView;
        //注释1处,向上遍历View层级,找到处理嵌套滑动的ViewParent
        while (p != null) {
            //注释2处
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                //当前正在处理嵌套滑动的父控件
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

注释1处,向上遍历View层级,找到处理嵌套滑动的ViewParent。

注释2处,调用ViewParentCompat.onStartNestedScroll来询问是否某个父级控件想处理嵌套滑动,如果ViewParentCompat.onStartNestedScroll返回true,说明父控件想要处理处理嵌套滑动。

ViewParentCompat的onStartNestedScroll方法。

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
    //找到对应的ViewParentCompatImpl来处理
    return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
static final ViewParentCompatImpl IMPL;
static {
    final int version = Build.VERSION.SDK_INT;
    if (version >= 21) {
        IMPL = new ViewParentCompatLollipopImpl();
    } else if (version >= 19) {
        IMPL = new ViewParentCompatKitKatImpl();
    } else if (version >= 14) {
        IMPL = new ViewParentCompatICSImpl();
    } else {
        IMPL = new ViewParentCompatStubImpl();
    }
}

ViewParentCompatImpl在不同的版本中有不同的实现。在Build.VERSION.SDK_INT小于21版本以下,外层控件必须实现NestedScrollingParent接口。在21及以上版本,外层控件不再需要实现NestedScrollingParent接口,因为ViewParent接口中已经定义了嵌套滑动外层控件相关的方法,ViewGroup实现了ViewParent接口。

这里我们就看21以上的版本。

ViewParentCompatLollipopImpl的onStartNestedScroll方法。

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

ViewParentCompatLollipop的onStartNestedScroll方法。

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
    try {
        //注释1处
        return parent.onStartNestedScroll(child, target, nestedScrollAxes);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                    "method onStartNestedScroll", e);
        return false;
    }
}

注释1处,最终调用ViewParent的onStartNestedScroll方法。如果返回true,紧接着会调用ViewParent的onNestedScrollAccepted方法。

public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
            int nestedScrollAxes) {
    try {
        //调用ViewParent的onNestedScrollAccepted方法。
        parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                    "method onNestedScrollAccepted", e);
    }
}

我们回到RecyclerView的onTouchEvent方法的ACTION_MOVE处

//注释2处
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
    //注释3处
    dx -= mScrollConsumed[0];
    dy -= mScrollConsumed[1];
    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
    // Updated the nested offsets
    mNestedOffsets[0] += mScrollOffset[0];
    mNestedOffsets[1] += mScrollOffset[1];
 }

注释2处,在内层控件开始滑动之前先询问外层控件是否要先处理滑动,如果dispatchNestedPreScroll返回true,表明外层控件消耗了某些滑动距离,所以我们在注释3处减去外层控件消耗的滑动距离。

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
/**
 * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent.
 *
 * @return true 如果外层控件消耗了滑动距离,返回true。
 */
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    //启用了嵌套滑动并且有父控件处理嵌套滑动
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        if (dx != 0 || dy != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }

            if (consumed == null) {
                if (mTempNestedScrollConsumed == null) {
                    mTempNestedScrollConsumed = new int[2];
                }
                consumed = mTempNestedScrollConsumed;
            }
            consumed[0] = 0;
            consumed[1] = 0;
            //注释1处
            ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

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

注释一处,调用外层控件的onNestedPreScroll方法。

public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
            int[] consumed) {
    try {
        parent.onNestedPreScroll(target, dx, dy, consumed);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onNestedPreScroll", e);
    }
}

我们回到RecyclerView的onTouchEvent方法的ACTION_MOVE处:

//注释4处
if (scrollByInternal(
        canScrollHorizontally ? dx : 0,
        canScrollVertically ? dy : 0,
        vtev)) {
    getParent().requestDisallowInterceptTouchEvent(true);
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
    //...
    if (mAdapter != null) {
        if (x != 0) {
            //注释1处
            consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
            unconsumedX = x - consumedX;
        }
        if (y != 0) {
            //注释2处
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            unconsumedY = y - consumedY;
        }
    }
    //注释3处
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
    //...
    }
}

注释1处,注释2处,RecyclerView先自己滑动,计算出消耗的滑动距离consumedX、consumedY,没有消耗的滑动距离 unconsumedX、unconsumedY。

注释3处,自身滑动之后通知外层控件。最终会调用外层控件的onNestedScroll方法。

public static void onNestedScroll(ViewParent parent, View target, int dxConsumed,
            int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    try {
        //调用外层控件的onNestedScroll方法。
        parent.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onNestedScroll", e);
    }
}

我们回到RecyclerView的onTouchEvent方法的ACTION_UP处:

//注释5处
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
    setScrollState(SCROLL_STATE_IDLE);
}
//注释6处
resetTouch();

注释5处,开始惯性滑动。

public boolean fling(int velocityX, int velocityY) {
  //...
  //注释1处
  if (!dispatchNestedPreFling(velocityX, velocityY)) {
     //内层控件自身是否可以滑动
     final boolean canScroll = canScrollHorizontal || canScrollVertical;
     //注释2处
     dispatchNestedFling(velocityX, velocityY, canScroll);

     if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
         return true;
     }

     if (canScroll) {
         velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
         velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
         //注释3处
         mViewFlinger.fling(velocityX, velocityY);
         return true;
     }
  }
  return false;
}

注释1处,先询问外层控件是否要处理惯性滑动,如果要处理,直接返回false,RecyclerView 自身不滑动。

public static boolean onNestedPreFling(ViewParent parent, View target, float velocityX,
            float velocityY) {
    try {
        //调用外层控件的onNestedPreFling方法。
        return parent.onNestedPreFling(target, velocityX, velocityY);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onNestedPreFling", e);
        return false;
    }
}

注释2处,最终会调用外层控件的onNestedFling方法。

public static boolean onNestedFling(ViewParent parent, View target, float velocityX,
            float velocityY, boolean consumed) {
    try {
        return parent.onNestedFling(target, velocityX, velocityY, consumed);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onNestedFling", e);
        return false;
    }
}

开始,很疑惑,注释处,dispatchNestedPreFling返回了false,为什么在注释2处还要调用dispatchNestedFling方法呢。我想了想,这里调用dispatchNestedFling方法,紧接着又在注释3处处调用了RecyclerView的fling,可以实现外层控件和内层控件同时滑动。

注释3处,内层控件自身滑动。

 mViewFlinger.fling(velocityX, velocityY);

这里简单说一下mViewFlinger,ViewFlinger实现了Runnable接口,然后通过OverScroller来实现fling。当OverScroller计算滚动还没结束的时候,就将ViewFlinger当做一个Runnable接口post出去(接受帧信后),在下一帧到来的时候在run方法中实现滚动。在ViewFlinger的run方法中,并没有再调用和嵌套滑动相关的逻辑。

ViewFlinger的run方法的精简版。

@Override
public void run() {
    if (mLayout == null) {
        stop();
        return; // no layout, cannot scroll.
    }
    // ...
    final OverScroller scroller = mScroller;
    final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
    if (scroller.computeScrollOffset()) {
        //...
        if (mAdapter != null) {
            //...
            if (dx != 0) {
                //调用LayoutManager的来实现滑动
                 hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                 overscrollX = dx - hresult;
            }
            if (dy != 0) {
                //调用LayoutManager的来实现滑动
                vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                overscrollY = dy - vresult;
            }
            //...
        }
               
        //滚动结束了
        if (scroller.isFinished() || !fullyConsumedAny) {
            setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
        } else {
            //滚动没结束,继续post
            postOnAnimation();
        }
    }
    //...
}

我们回到RecyclerView的onTouchEvent方法的ACTION_UP处:

//注释6处
resetTouch();
private void resetTouch() {
    if (mVelocityTracker != null) {
        mVelocityTracker.clear();
    }
    //停止嵌套滑动
    stopNestedScroll();
    releaseGlows();
}
public static void onStopNestedScroll(ViewParent parent, View target) {
    try {
        //注释1处
        parent.onStopNestedScroll(target);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onStopNestedScroll", e);
    }
}

注释1处,最终调用外层控件的onStopNestedScroll方法。一次完整的嵌套滑动流程结束。

这里注意一下:外层控件的onStopNestedScroll方法被调用的时候,外层控件或者内层控件的惯性滑动可能并没有结束。

参考链接:

  • Android 8.0 NestedScrollingChild2与NestedScrollingParent2实现RecyclerView阻尼回弹效果
  • Android嵌套滑动机制NestedScrolling
  • Andorid 嵌套滑动机制 NestedScrollingParent2和NestedScrollingChild2 详解
  • Android NestedScrolling机制完全解析 带你玩转嵌套滑动

你可能感兴趣的:(Android嵌套滑动上篇)