中高级Android程序员必备:嵌套滑动技术总结!

1,181 阅读4分钟

一.何为滑动嵌套?

就是就有滑动功能的view,再嵌套另一个具有滑动功能的view,例如recyclerview嵌套recyclerview。

二.CoordinatorLayout是什么?

是协调布局,是一个超级framelayout,可以控制多个view协调运动。通过behavior来实现。主要原理是,子view获取到滑动事件后,在滑动前,先询问一下父view,问下父view要不要先滑,父view如果要滑,那就滑,然后把滑完之后把剩下的距离告诉子view,然后子view再滑。
所以说,如果处理不好,可以滑起来卡卡的,例如父view滑了一点,然后子view又滑了一点,你就会感觉卡卡的,难受。

三.behavior是怎样的?

    public static abstract class Behavior<V extends View> {

        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
            }
        }

        @Deprecated
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
            // Do nothing
        }

        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
            }
        }

        public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY,
                boolean consumed) {
            return false;
        }

        public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY) {
            return false;
        }
.......

是个抽象类,看上去有点像NestedScrollingParent的方法。

例如,我们看看onDependentViewChanged是怎么调用的把

![](//upload-images.jianshu.io/upload_images/3858093-283f0de8afb98175.png?imageMogr2/auto-orient/strip|imageView2/2/w/943/format/webp)

可以看到,CoordinatorLayout注册了PreDraw的监听器,就是view在绘制前,会有回调函数回调到CoordinatorLayout的onChildViewsChanged,

if (b != null && b.layoutDependsOn(this, checkChild, child)) { //首先检查是不是依赖的view发生了改变
      .....  //依赖的view发生改变了,回调给需要协调运动的view
      handled = b.onDependentViewChanged(this, checkChild, child);
       .....
}

我们再看一种场景,就是使用onNestedPreScroll来实现的协调运动。

看看他的backtrace

![](//upload-images.jianshu.io/upload_images/3858093-d6365a939aaad31b.png?imageMogr2/auto-orient/strip|imageView2/2/w/871/format/webp)

可以看到事件是从recyclerview传过来的,所以说move事件到达recyclerview后,recyclerview作为NestedScrollingChild2,会先循环一下作为NestedScrollingParent2的CoordinatorLayout,你CoordinatorLayout有没有需要滑动的,有没有需要处理move事件的,等CoordinatorLayou处理完之后,recyclerview再继续跑。这样看,像是子view和父view都消费了move事件,挺有意思。

recyclerview调用dispatchNestedPreScroll后,实际用使用NestedScrollingChildHelper来处理,

    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) { //判断是否支持嵌套滑动,我们常常可以在应用中关闭嵌套滑动
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, 
    }

然后,最后就要回调到CoordinatorLayout,然后CoordinatorLayout会遍历每个子view,子view可以根据需要来确定是否处理。

四.分析一下BottomSheetBehavior的绘制流程

![](//upload-images.jianshu.io/upload_images/3858093-62d5114139b92495.png?imageMogr2/auto-orient/strip|imageView2/2/w/1033/format/webp)
layout流程
    @Override
    @SuppressWarnings("unchecked")
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

            final Behavior behavior = lp.getBehavior();
           //获取childview的behavior,然后调用behavior的onLayoutChild去布局。如果没有behavior就走CoordinatorLayout自身的onLayoutChild
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }

可以看到,有配置behavior的view,就走behavior的onLayoutChild方法。所以layout方法被behavior接管了。

  viewDragHelper = ViewDragHelper.create(parent, dragCallback);

最后其实就是用viewdragger来实现拖拉位置的

  @Override
  public boolean onLayoutChild(
      @NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) {
    if (state == STATE_EXPANDED) {
      ViewCompat.offsetTopAndBottom(child, getExpandedOffset());
    } else if (state == STATE_HALF_EXPANDED) {
      ViewCompat.offsetTopAndBottom(child, halfExpandedOffset);
    } else if (hideable && state == STATE_HIDDEN) {
      ViewCompat.offsetTopAndBottom(child, parentHeight);
    } else if (state == STATE_COLLAPSED) {
      ViewCompat.offsetTopAndBottom(child, collapsedOffset);
    } else if (state == STATE_DRAGGING || state == STATE_SETTLING) {
      ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
    }
  }

五.bottomsheet嵌套recyclerview后,recyclerview关闭嵌套滑动之后,滑recyclerview,bottomsheet会动吗?

会动,别以为关闭嵌套滑动,父view就不拦截事件了,实际上,父view会会直接把在slopmove的时候,直接开始拦截move事件了。

![](//upload-images.jianshu.io/upload_images/3858093-87e0374f069389cc.png?imageMogr2/auto-orient/strip|imageView2/2/w/1013/format/webp)

那拦截之后,是怎么拖到整个bottomsheet的呢?

![](//upload-images.jianshu.io/upload_images/3858093-515c05325b57b88f.png?imageMogr2/auto-orient/strip|imageView2/2/w/799/format/webp)

其实就是拦截之后,事件去了CoordinatorLayout,然后再调用behavior,behavior使用ViewDragHelper去拖动配置了behavior的view网上挪。

关键是下面的函数,来确认是否拦截,返回true就拦截,因为touchingScrollingChild为false,所以最后返回了true。如果recyclerview设置了NestedScrollingEnabled,则touchingScrollingChild为true,最后这个函数返回false,父view就不拦截,move事件直接到达recyclerview。

        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
          Log.i("fengfeng", "tryCaptureView start:" + "child=" + child + " pointId=" + pointerId + " state=" + state);
          if (state == STATE_DRAGGING) {
            Log.i("fengfeng", "tryCaptureView false 1");
            return false;
          }
          if (touchingScrollingChild) {
            Log.i("fengfeng", "tryCaptureView false 2");
            return false;
          }
          if (state == STATE_EXPANDED && activePointerId == pointerId) {
            View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
            if (scroll != null && scroll.canScrollVertically(-1)) {
              // Let the content scroll up
              Log.i("fengfeng", "tryCaptureView false 3");
              return false;
            }
          }
          Log.i("fengfeng", "tryCaptureView true 4");
          return viewRef != null && viewRef.get() == child;
        }        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
          Log.i("fengfeng", "tryCaptureView start:" + "child=" + child + " pointId=" + pointerId + " state=" + state);
          if (state == STATE_DRAGGING) {
            Log.i("fengfeng", "tryCaptureView false 1");
            return false;
          }
          if (touchingScrollingChild) {
            Log.i("fengfeng", "tryCaptureView false 2");
            return false;
          }
          if (state == STATE_EXPANDED && activePointerId == pointerId) {
            View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
            if (scroll != null && scroll.canScrollVertically(-1)) {
              // Let the content scroll up
              Log.i("fengfeng", "tryCaptureView false 3");
              return false;
            }
          }
          Log.i("fengfeng", "tryCaptureView true 4");
          return viewRef != null && viewRef.get() == child;
        }

touchingScrollingChild在down事件赋值,代表点中了一个可以scroll的view,例如recyclerview就是可以scroll的。

      case MotionEvent.ACTION_DOWN:
        if (state != STATE_SETTLING) {
          View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
          if (scroll != null && parent.isPointInChildBounds(scroll, initialX, initialY)) {
            touchingScrollingChild = true;
          }
        }

最后

现在都说互联网寒冬,其实无非就是你上错了车,会的技能都是些快淘汰的而已,只要你自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉!我这边专门针对Android开发工程师整理了一套【Android进阶学习视频】、【全套Android面试秘籍】、【Android知识点PDF】 如果有需要资料的朋友,可以点击我GitHub免费领取!