仿京东首页滑动效果以及AppBarLayout和Rv到底是怎么做到滑动的

仿京东1.gif

这个是简单的视觉效果。代码也超级简单也就十行代码不到。但是本文关键在于介绍他是如何滚动的以及如何RV到底是如何被放到ABL(Appbarlayout 下面都用这个个简称)下面的以及Coor中的View是如何测量的。

RV是如何被放到ABL下面的

因为CoordinatorLayout(下面简称Coor 他太长了。。) 本身继承自ViewGroup
而且他对没有Behavior(或者Behavior 的layoutchild 返回false [这个后面会讲他为什么能跑到ABL下面去]也就是不自己layout的childview 布局按照framelayout的方式进行布局),所以我们如果顶部不是ABL 底部放一个(RecyclerView 简称RV)Rv他们是重叠的。

  1. 上面提到的如何放置的问题,也就是layout但是提到layout 我们得先想measure 这里Rv的高度 到底是多少?
    image.png

    他的高度是红框的高度么?答案是否 看看源码 Coor 中 measure的时候做了什么

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        ensurePreDrawListener();

        final int paddingLeft = getPaddingLeft();
        final int paddingTop = getPaddingTop();
        final int paddingRight = getPaddingRight();
        final int paddingBottom = getPaddingBottom();
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        final int widthPadding = paddingLeft + paddingRight;
        final int heightPadding = paddingTop + paddingBottom;
        int widthUsed = getSuggestedMinimumWidth();
        int heightUsed = getSuggestedMinimumHeight();
        int childState = 0;

        final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
     //上面的我们不管
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            if (child.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            //这里就是真相 上面删了一部分代码因为和我们要讲的几乎无关
            final Behavior b = lp.getBehavior();
           // 对于设置了behavior的View 他可以自己处理测量 并必须返回true 需要注意的是即便你在 b.onMeasureChild 自己测量了但是返回false Coor会重新给你重新测量一遍覆盖掉你的逻辑(可能我说的废话 😁)
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }

            widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                    lp.leftMargin + lp.rightMargin);

            heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                    lp.topMargin + lp.bottomMargin);
            childState = View.combineMeasuredStates(childState, child.getMeasuredState());
        }

        final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
                childState & View.MEASURED_STATE_MASK);
        final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
                childState << View.MEASURED_HEIGHT_STATE_SHIFT);
        setMeasuredDimension(width, height);
    }

接下来就是到对应Behavior的 onMeasureChild源码里边
1.先看ABL的behavior 他的逻辑很简单

@Override
    public boolean onMeasureChild(
        @NonNull CoordinatorLayout parent,
        @NonNull T child,
        int parentWidthMeasureSpec,
        int widthUsed,
        int parentHeightMeasureSpec,
        int heightUsed) {
      final CoordinatorLayout.LayoutParams lp =
          (CoordinatorLayout.LayoutParams) child.getLayoutParams();
      if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
        // If the view is set to wrap on it's height, CoordinatorLayout by default will
        // cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
        // what we actually want, so we measure it ourselves with an unspecified spec to
        // allow the child to be larger than it's parent
// 这段话翻译过来的意思就是说 因为ABl是可以滑动的 如果ABl的高度模式是
//CoordinatorLayout.LayoutParams.WRAP_CONTENT 那么ABL的对子View的测量模式就是MeasureSpec.UNSPECIFIED 也就是不限制子View的高度允许他啊超过ABL的高度
        parent.onMeasureChild(
            child,
            parentWidthMeasureSpec,
            widthUsed,
            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
            heightUsed);
        return true;
      }

在上面还引出一个问题 自定义View如果不重写onMeasure方法他的高度就是父容器的高度么? 之前看网上的博客还好视频还好 都是很明确的说只要你不重写onMeasure方法 自定义View 高度设置为wrapcontent的话的他的高度就是父容器的高度 但是如果你试试放到NestSceollView里边去,虽然我们一般不会这么做,但是你可以尝试放进去,你会发现自定义view的高度是0不知道大家有没有发现,其实这个东西很简单
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),从这个代码可看到 父容器传递给子view的Spec 高度是0 模式UNSPECIFIED是无限的,也就是说允许子view无限高,之所以不重写onmeasure方法在Linearlayout这种布局下view的高度是父容器的高度是因为线性布局传递给子view的高度MeasureSpec中含有高度值和宽度值(基于水平或者竖向布局而定 并且这个值是父容器允许你的最大值 也就是说父容器测量子View的时候对子View是约束条件 UNSPECIFIED 这种不约束其实也是一种约束)
\color{#FF0000}{Spec中含有的值是剩余水平距离或者剩余纵向距离即使他传递给child的都是自己的最大值,那也是剩余距离}

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

      /*
有了上面的红字部分 我们不难看出 自定义View 如果父容器给我们的限定模式是MeasureSpec.UNSPECIFIED 这个时候 这个specSize 就是0 
//而对于ATMOST 也就是 wrapcontent  这个 specSize  就取决于父容器给我们的限定值了 
*/ 
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

很有趣的一点是 \color{#FF0000}{UNSPECIFIED 这种无限制模式并不是天生存在,} 比如我们使用了NestScroolView \color{#FF0000}{他就是这种测量模式}而是NestScroolView本身不包含这种测量模式,而是他给他的child传递的是这种测量模式,告诉child 你可以比我大而且随便大,那里能看到这个东西呢? onMeasure方法很简单他调用super 传递给child的测量模式仍然是NestScroolView的父容器传递过来的是exctly模式,大家可以去debug或者打日志看一下但是\color{#FF0000}{真正让唯一直接子控件使用UNSPECIFIED模式是在measureChildWithMargins方法 } NestScroolView重写了measureChildWithMargins方法

@Override
    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);
  /*
    这里 看到没有 !!!!
*/
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);//!!!!!!!!!!!!!!!!!看这里

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

所以说真正决定child多高(只讨论高宽度一样的道理)是父容器给的限定模式和childView向父容器申请的模式的综合结果 我们以Linearlayout举个例子
\color{#FF0000}{父容器测量子View 会调用这个方法 getChildMeasureSpec}

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        
        int size = Math.max(0, specSize - padding);
这里size的计算意就味着是剩余的空间
        int resultSize = 0;
        int resultMode = 0;
  以Linearlayout举例 因为他Vertical的时候 specSize是纵向剩余距离所以我们挨个三个case举个例子
      三个case  表示父容器的三种限定模式 
  childDimension表示自己向父容器申请的大大小 
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY://父容器大小固定
            if (childDimension >= 0) {//代表自己向父容器申请了具体的高度值 比如0  100 200 那么不管Linearlayout有多高我们想多大就多大 比如父容器仅仅有1000px 我们甚至可以有2000 甚至更大px 只不过超出的部分不绘制而已 是不是感觉和UNSPECIFIED有点像 哈哈但是这种设置方式其实没屁用,除非你自己滚动比如调用父容器的scrollto或者by方法否则超出去的部分永运不会显示
但是你自己大归大 父容器并不会因此而撑开,他的高度仍然要接受父容器的父容器的限制最大只能是Spec中给定的最大值。在View中有一个方法叫 resolveSizeAndState方法 他就是做这个工作的。感兴趣可以去看一下
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;  
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
//这种就特殊了 因为上面提到了spec传递过来的是剩余距离,所以这里的spec取出来的高度值size 就是Linearlayout的剩余高度了 假如剩余0 了 那么你即便向Linearlayout申请的是MATCH_PARENT 你的高度仍然是0 
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
      //这个同理 因为是剩余距离 剩多少你child就只能有多少
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST://父容器大小不确定
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
//这个其实也同理和上面的 解释一下注释 意思就是说 child想和父容器一样大,但是父容器的大小不固定,将child的大小限制为不能比父容器大

                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
//此时 size的值是父器能给我们的最大值了 这里就出现了那个自定义View如果不重写onMeasure方法他的高度就是MATCHPARENT问题了 假设Linearlayout 最大 高度为1000有两个child 分别是TextView 他的高度是100px 
第二个给一个View (就是一个View 布局<View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>,因为直接使用它就相当于没有重写onMEasure方法了)此时这个id为 view的View 高度应该是多少? 1000么?很可惜他的高度是1000-100=900px的高度。因为还是我们前面说过的父容器的剩余空间
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
走完这个方法  走到这里 最终 child会调用         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);这个方法里边的两个参数 最终就会走到onMeasure方法里边去 取出里边对应高度或者宽度值 这个onMeasure方法可能是你重写的 也可能是super的View的 取决于我们自己的实现了

值得注意的是
\color{#FF0000}{Linearlayout的高度是靠每个child的高度和相加的,如果超过了那高度就是Linearlayout的父容器给的最大值 }
但是每个容器类控件对于onMeasure方法实现不一样这里仅讨论Linearlayout

测量模式走的有点远了。。。。。接下来回归正题继续看RV的高度问题
因为RV 拥有ScrollingViewBehavior 那么我们就得看他的 behavior的
onMeasureChild 方法做了什么但是很可惜。。。源码里貌似没看到这个方法
但是他有爹呀。。那就肯定在他爹那里
\color{#FF0000}{HeaderScrollingViewBehavior这个类说的很明确让一个滚动的View位于其他View的下方,关于解析都在代码里边进去看看到底怎么回事 }

@Override
  public boolean onMeasureChild(
      @NonNull CoordinatorLayout parent,
      @NonNull View child,
      int parentWidthMeasureSpec,
      int widthUsed,
      int parentHeightMeasureSpec,
      int heightUsed) {
    //注意这里的child 已经是RV了 因为当前和这个behavoir关联的就是RV
    final int childLpHeight = child.getLayoutParams().height;
//当前RV向Coor申请的高度 
//注意下面这个if Rv只能是这两种模式 至于为什么后面会说
    if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
        || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
      // If the menu's height is set to match_parent/wrap_content then measure it
      // with the maximum visible height
       

    //寻找Coor中所有被依赖的View 类似ABL这样的View 这里通俗的讲是 所有Behavior中 layoutDependsOn 方法 返回true的View 你也可以很任性的直接返回true  这里用到了一个叫有向非循环图的数据结构存储存储View 和他所依赖的View的关系,基本原理就是使用了    private final SimpleArrayMap<T, ArrayList<T>> mGraph = new SimpleArrayMap<>();
其中 T表示某个View  ArrayList<T> 表示他所依赖的所有View的集合

其中被依赖的View是
      final List<View> dependencies = parent.getDependencies(child);
//在RV和ABL的这种情况下我们要找的是ABL 如果我们想自己写一个类似这样的滚动效果 也可以自己在这里找到自己想找的View 
      final View header = findFirstDependency(dependencies);
      if (header != null) {
        int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
        if (availableHeight > 0) {
          if (ViewCompat.getFitsSystemWindows(header)) {
            final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
            if (parentInsets != null) {
              availableHeight += parentInsets.getSystemWindowInsetTop()
                  + parentInsets.getSystemWindowInsetBottom();
            }
          }
        } else {
          // If the measure spec doesn't specify a size, use the current height
          availableHeight = parent.getHeight();
        }
      //这里才是真正的高度来了注意这一行的高度是Coor的高度加上滚动高度
//滚动高度的计算和scrollFlag有关 当前这种仿京东效果就是ABl的高度减去CollapsingToolbarLayout的minHeight的高度
        int height = availableHeight + getScrollRange(header);
        int headerHeight = header.getMeasuredHeight();
        if (shouldHeaderOverlapScrollingChild()) {//这个属于压盖滚动 我们这里不讨论
          child.setTranslationY(-headerHeight);
        } else {
          height -= headerHeight; //这个才是我们的RV的高度 注意看这里他的高度
      Rv的height= Coor的高度+滚动高度-header的高度。但其实 header的高度又等于 headerHeight= 滚动高度+不能滚动的高度  所以上面的等式又相当于 
Rv的height= Coor的高度+滚动高度-(滚动高度+不能滚动的高度) = Coor的高度-不能滚动的高度 记住这个公式 后面有大用 
        }
        final int heightMeasureSpec =
            View.MeasureSpec.makeMeasureSpec(
                height,
                childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                    ? View.MeasureSpec.EXACTLY
                    : View.MeasureSpec.AT_MOST);

        // Now measure the scrolling view with the correct height
        parent.onMeasureChild(
            child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);

        return true;
      }
    }
    return false;
  }

这张图能能明确的说明Rv高度的问题


解释1.png

接下来看RV是怎么放到ABL的下面的
同样在HeaderScrollingViewBehavior 的layoutChildchild方法中

@Override
  protected void layoutChild(
      @NonNull final CoordinatorLayout parent,
      @NonNull final View child,
      final int layoutDirection) {
    final List<View> dependencies = parent.getDependencies(child);
    final View header = findFirstDependency(dependencies);

    if (header != null) {
      final CoordinatorLayout.LayoutParams lp =
          (CoordinatorLayout.LayoutParams) child.getLayoutParams();
      final Rect available = tempRect1;
  //注意看这个矩形的设置 
      available.set(
      //left    parent.getPaddingLeft() + lp.leftMargin,
    //top  ABL的bottom 很明显吧?    header.getBottom() + lp.topMargin,
      //right    parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
      //bottom   这个相加也很明显 和我们上面讨论的高度完全吻合  parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);

      final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
      if (parentInsets != null
          && ViewCompat.getFitsSystemWindows(parent)
          && !ViewCompat.getFitsSystemWindows(child)) {
        // If we're set to handle insets but this child isn't, then it has been measured as
        // if there are no insets. We need to lay it out to match horizontally.
        // Top and bottom and already handled in the logic above
        available.left += parentInsets.getSystemWindowInsetLeft();
        available.right -= parentInsets.getSystemWindowInsetRight();
      }

      final Rect out = tempRect2;
      GravityCompat.apply(
          resolveGravity(lp.gravity),
          child.getMeasuredWidth(),
          child.getMeasuredHeight(),
          available,
          out,
          layoutDirection);

      final int overlap = getOverlapPixelsForOffset(header);

      child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
      verticalLayoutGap = out.top - header.getBottom();
    } else {
      // If we don't have a dependency, let super handle it
      super.layoutChild(parent, child, layoutDirection);
      verticalLayoutGap = 0;
    }
  }

讲完了RV高度和位置 该讲怎么滑动了

这个需要对嵌套滚动有基本认识,如果不是很清楚可以自己先去相关博客看看。
嵌套滚动需要滚动发起者,这里因为ABL和RV都可以滑动所以其实我们需要分开看触摸到RV和触摸到ABL的情况

  1. 首先讨论触摸到RV的情况 必然要到RV的onTouchEvent方法中
 @Override
    public boolean onTouchEvent(MotionEvent e) {
       //这里讲点击事件和滚动分开处理了我们不讨论点击事件 点击事件的处理优先于滚动
        if (dispatchToOnItemTouchListeners(e)) {
            cancelScroll();
            return true;
        }
//下面都是对滚动处理了
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        boolean eventAddedToVelocityTracker = false;

        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();

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

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
            //down事件开始嵌套滚动
          
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;

            case MotionEvent.ACTION_POINTER_DOWN: {
                mScrollPointerId = e.getPointerId(actionIndex);
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
            } break;

            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(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) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally) {
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx + mTouchSlop);
                        }
                        if (dx != 0) {
                            startScroll = true;
                        }
                    }
                    if (canScrollVertically) {
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
              //重点是这个方法 这些方法调用都在ChildHelper中 通过ViewParentCompat
调用的,如果Coor中的某个Behavior消费了dy的一部分此时dispatchNestedPreScroll会返回true 
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                    // 如果消费了 这里的dy就已经是剩余的了
                        dy -= mReusableIntPair[1];
                        // Updated the nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // Scroll has initiated, prevent parents from intercepting
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    //此时这个方法才是RV自己滚动的方法 假如dy已经是0了那么RV也就不会滑动了但是假如有剩余dy 剩余的交给RV滑动
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

            case MotionEvent.ACTION_POINTER_UP: {
                onPointerUp(e);
            } break;

            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetScroll();
            } break;

            case MotionEvent.ACTION_CANCEL: {
                cancelScroll();
            } break;
        }

        if (!eventAddedToVelocityTracker) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();

        return true;
    }

因为代码里写注释看起来实在不够清晰,我决定在正文也写一部分注释
startNestedScroll 做了一个很重要的事情,利用NestedScrollingChildHelper寻找实现了NestedScrollingParent2或者NestedScrollingParent3或者API大于21的 ViewParent 找到这个嵌套滚动父容器并记录因为后续的滚动需要优先交给他处理。

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
//这里是如何寻找这个父容器 限制条件有两个
//1 .实现了NestedScrollingParent2或者NestedScrollingParent3或者API大于21的 ViewParent 
//2.ViewParentCompat.onStartNestedScroll 必须返回true 而Coor的返回值又取决于任意一个Behavior的onStartNestedScroll的返回值 
只有这个方法返回true 才有机会在发起嵌套滚动后在onNestPreScroll优先消费但是不影响最后的一起滚动NestedScroll 这个方法只要有Coor 这样的容器并且一开始里边有任意一个Behavior的onStartNestedScroll返回true了 那么Coor中的所有Behavior都可以在对应的onNestedScroll方法滚动自己默认这个方法是什么都不做的。

                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

\color{#FF0000}{但还一个需要注意的就是 这个父容器不需要是发起滚动的View的直接ViewParent }

Coor的onStartNestedScroll方法 

public boolean onStartNestedScroll(View child, View target, int axes, int type) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == View.GONE) {
                // If it's GONE, don't dispatch
                continue;
            }
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
        //只要有一个Behavior返回true Coor就会处理分发滚动事件
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                        target, axes, type);
                handled |= accepted;
                lp.setNestedScrollAccepted(type, accepted);
            } else {
                lp.setNestedScrollAccepted(type, false);
            }
        }
        return handled;
    }

既然滑动产生了,也通过Coor 再交由Bahavior 那么就到了ABL滚动的时候了,因为这里是ABL先滚动的,RV的dispatchNestedPreScroll 通过Coor (因为Coor是之前找到的嵌套滚动父容器上面有解释)的onNestedPreScroll 这个方法中,调用之前之前onStartNestScroll中记录的behavior 调用这个Behavior的onNestedPreScroll方法 至此 从RV->Coor->Behavior的滚动就联系起来了接下来就到了ABL的onNestedPreScroll方法了看看他是怎么滚动的


@Override
    public void onNestedPreScroll(
        CoordinatorLayout coordinatorLayout,
        @NonNull T child,
        View target,
        int dx,
        int dy,
        int[] consumed,
        int type) {
      if (dy != 0) {
        int min;
        int max;
        if (dy < 0) {
          // We're scrolling down
          min = -child.getTotalScrollRange();
          max = min + child.getDownNestedPreScrollRange();
        } else {
          // We're scrolling up
          min = -child.getUpNestedPreScrollRange();
          max = 0;
        }
 // 上面是计算 不深究
        if (min != max) {
//这个scroll方法是滚动
          consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
        }
      }
      if (child.isLiftOnScroll()) {
        child.setLiftedState(child.shouldLift(target));
      }
    }

最终这个scroll方法其实是调用View的offsetTopAndBottom实现的ABL的滚动
其实ViewOffsetBehavior 这个Behavior是 ABl和HeadScrollViewingBehavior共同的父类正常来讲ABL offset了 RV其实是没动的前面我们说了 RV其实是超出屏幕的,那我们的RV为什么跟着一起上去了呢,看起来像RV滑动了一样这里就又涉及到了一个非常重要的方法 叫做 onDependentViewChanged
核心方法是这个 offsetChildAsNeeded(child, dependency); //

@Override
    public boolean onDependentViewChanged(
        @NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
      offsetChildAsNeeded(child, dependency); //
      updateLiftedStateIfNeeded(child, dependency);
      return false;
    }
private void offsetChildAsNeeded(@NonNull View child, @NonNull View dependency) {
      final CoordinatorLayout.Behavior behavior =
          ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
      if (behavior instanceof BaseBehavior) {
        // Offset the child, pinning it to the bottom the header-dependency, maintaining
        // any vertical gap and overlap
        final BaseBehavior ablBehavior = (BaseBehavior) behavior;
        ViewCompat.offsetTopAndBottom(
            child,
// 主要是这个当dependency offset之后 getBottom()会变,本来一开始RV的top应该等于ABl的bottom的 此时产生了差值,这部分差值就是ABL偏移的部分。也就看起来是RV和ABL一起上去了 但是他们能一起滚动的区域上面我们也说过了仅限于ABL的滚动区域之后就是RV自己滚动了。就是RV的scrollInternal里边的代码了
            (dependency.getBottom() - child.getTop())
                + ablBehavior.offsetDelta
                + getVerticalLayoutGap()
                - getOverlapPixelsForOffset(dependency));
      }
    }

最后 onDependentViewChanged 这个方法又是什么时候调用的呢?
因为前面说到Coor负责分派的preScroll

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int  type) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mBehaviorConsumed[0] = 0;
                mBehaviorConsumed[1] = 0;
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mBehaviorConsumed, type);

                xConsumed = dx > 0 ? Math.max(xConsumed, mBehaviorConsumed[0])
                        : Math.min(xConsumed, mBehaviorConsumed[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mBehaviorConsumed[1])
                        : Math.min(yConsumed, mBehaviorConsumed[1]);

                accepted = true;
            }
        }

        consumed[0] = xConsumed;
        consumed[1] = yConsumed;
//  这里 假如上面的Behavior 消费了Prescroll 并返回了true
 accepted 就是true
        if (accepted) { 看下onChildViewsChanged做了什么
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

Coor 中的方法

 final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = acquireTempRect();
        final Rect drawRect = acquireTempRect();
        final Rect lastDrawRect = acquireTempRect();

        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
                // Do not try to update GONE child views in pre draw updates.
                continue;
            }

            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }

            // Get the current draw rect of the view
            getChildRect(child, true, drawRect);

         

            

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
                //
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        // If this is from a pre-draw and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
 //会走到这里 checkChild就是RV child 是ABL 

                            // Otherwise we dispatch onDependentViewChanged()
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }
               
                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }

        releaseTempRect(inset);
        releaseTempRect(drawRect);
        releaseTempRect(lastDrawRect);
    }


进到HeadScrollIngBehavior的 onDependentViewChanged 就执行了RV的offsettopandBottom

结束

写了好多天感觉越写越乱。。。。无力优化了就当给自己做笔记了

代码地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容