返回介绍

3. NestedScrollView 之 ScrollView

发布于 2024-12-23 21:38:51 字数 9632 浏览 0 评论 0 收藏 0

言归正传,NestedScrollView 具备滑动功能,此处你需要知道的是:NestedScrollView 的父类是 FrameLayout,FrameLayout 对 TouchEvent 的处理没有任何定制,FrameLayout 所有的 TouchEvent 处理都交给了它的父类 ViewGroup。NestedScrollView 对 TouchEvent 的两个入口做了定制:onInterceptTouchEvent 和 onTouchEvent。

先看一下 onInterceptTouchEvent,这个函数的字面意思是:Touch 事件拦截。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  // MotionEvent 拦截,如果返回 true,MotionEvent 交给 TouchEvent 去处理
  // 如果返回 false,MotionEvent 传递给子 View   

  final int action = ev.getAction();
  if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
    // 如果正在 move 而且被定性为正在拖拽中,直接返回 true,将 MotionEvent 交给自己的 onTouchEvent 去处理
    return true;
  }

  switch (action & MotionEventCompat.ACTION_MASK) {
    case MotionEvent.ACTION_MOVE: {
      // 下面这么多代码大多是为了给 mIsBeingDragged 定性

	      /************ 若干代码略去 ************/

      final int y = (int) MotionEventCompat.getY(ev, pointerIndex);
      final int yDiff = Math.abs(y - mLastMotionY); // 垂直滑动的距离
      if (yDiff > mTouchSlop
          && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
        // 如果垂直拖动距离大于 mTouchSlop,就认定是正在 scroll
        mIsBeingDragged = true;
  	      // 保存一些变量,速度跟踪初始化
        mLastMotionY = y;
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(ev);
        mNestedYOffset = 0;
        final ViewParent parent = getParent();
        if (parent != null) {
          // 如果认定了是 scrollView 滑动,则不让父类拦截,后续所有的 MotionEvent 都会有 NestedScrollView 去处理
          parent.requestDisallowInterceptTouchEvent(true);
        }
      }
      break;
    }

    case MotionEvent.ACTION_DOWN: {
	  
      /************ 若干代码略去 ************/

      // 速度跟踪
      initOrResetVelocityTracker();
      mVelocityTracker.addMovement(ev);
      mScroller.computeScrollOffset();
      mIsBeingDragged = !mScroller.isFinished(); // mIsBeingDragged 跟是否 fling 有关
      // 请格外关注下,因为 startNestedScroll 跟,因为它跟 Behavior 的一个成员函数重名
      startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
      break;
    }

    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP:
      // 手指松开
      mIsBeingDragged = false;
      mActivePointerId = INVALID_POINTER;
      recycleVelocityTracker();
      if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
        ViewCompat.postInvalidateOnAnimation(this);
      }
      // stopNestedScroll 跟 Behavior 的一个成员函数重名
      stopNestedScroll();
      break;
    case MotionEventCompat.ACTION_POINTER_UP:
      onSecondaryPointerUp(ev);
      break;
  }

  // 返回值也好理解:如果正在拖拽中,则返回 true,告诉系统 MotionEvent 交给 NestedScrollView 的 OnTouchEvent 去处理
  // 如果没有拖拽,比如 ACTION_DOWN、ACTION_UP 内部 Button 点击的 MotionEvent,返回 false,MotionEvent 传递给子 View
  return mIsBeingDragged;
}

上面的 onInterceptTouchEvent 函数,贴上了关键的代码。这个函数也还比较容易理解。毕竟该函数负责拦截,不会将 Scroll/Fling 效果的功能代码写在这里。该函数主要是给 mIsBeingDragged 这个 flag 定性。一旦定性为上下拖动,就不再将 MotionEvent 传递给子 View。

然而,此处我们应该格外关注的是上面出现了 startNestedScroll 和 stopNestedScroll 这两个看起来比较敏感的函数调用。因为它们跟 Behavior 的两个函数重名。此处,我主观猜测它们会跟 Behavior 纠缠不清,以其中的 startNestedScroll 函数为例,贴上代码:

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

居然就这一行代码!mChildHelper 是 NestedScrollingChildHelper 对象,在最初的最初,我在构造函数中提到了它,还有印象么?我敢打包票,mChildHelper 一定做了非常多的事情,否则 Behavior 怎么会跟它那么像。注意到 NestedScrollView 调用 startNestedScroll 的时候并没有关心返回值,此处我们也不关心返回的 true 还是 false。下面载入 mChildHelper 的 startNestedScroll 函数:

public boolean startNestedScroll(int axes) {
  if (hasNestedScrollingParent()) {
    // 如果已经设置了 NestedParent,啥都不用做了
    return true;
  }
  // 此处 isNestedScrollingEnabled 依赖于一个全局变量 mIsNestedScrollingEnabled
  // 在 NestedScrollView 的构造函数中,这个 flag 被设置成了 true,这个 if 分支一定能进得去
  if (isNestedScrollingEnabled()) {
    // mView 就是 NestedScrollView,构造函数中被初始化
    ViewParent p = mView.getParent();
    View child = mView; 
    while (p != null) {
      // 这个 for 循环,就是一直不断的寻找支持 nested 功能的 ancestorView
      // 卧槽,如果外层 View 有一个 CoordinatorLayout,则这个 NestedScrollView 就能勾搭上 CoordinatorLayout 了
      // 下面的函数 onStartNestedScroll 和 onNestedScrollAccepted,应该和 Behavior 不远了
      if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
  	      // 找到了支持 Nested 功能的 ancestorView,保存一下
        mNestedScrollingParent = p;
        ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
        return true;
      }
      if (p instanceof View) {
        child = (View) p;
      }
      p = p.getParent();
    }
  }
  return false;
}

这个函数的主要功能是找到祖先 View 中最近的 mNestedScrollingParent,mNestedScrollingParent 是一个支持 Nested 滑动的 ancestorView。mNestedScrollingParent 一旦找到,目测 onStartNestedScroll 和 onNestedScrollAccepted 已经跟 Behavior 不远了。

此处我们先暂停,后面我们再回来。因为我们的第一个目标是看 NestedScrollView 怎么实现滑动的。况且,当 layout 文件中根 View 就是 NestedScrollView 时,startNestedScroll 函数是找不到 mNestedScrollingParent 的。

NestedScrollView 实现滑动效果,当然要看 OnTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent ev) {
  initVelocityTrackerIfNotExists();
  MotionEvent vtev = MotionEvent.obtain(ev);
  final int actionMasked = MotionEventCompat.getActionMasked(ev);
  if (actionMasked == MotionEvent.ACTION_DOWN) {
    // 这个是一个很重要的参数,视差值初始化为 0
    mNestedYOffset = 0;
  }
  // 我们知道 CoordinatorLayout 和 AppbarLayout 视差滑动的时候,有悬停效果
  // mNestedYOffset 记录的是悬停时候的 scroll 视差值
  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) {
          // 如果按下的时候还在 fling 动画,就直接受理这个 MotionEvent
 	          // 告诉祖先 view 不用拦截了,后续的 TouchEvent 事件统一由 NestedScrollView 来消费 
          parent.requestDisallowInterceptTouchEvent(true);
        }
      }

      // 手指按下,如果正在 fling 中,就停止 fling
      if (!mScroller.isFinished()) {
        mScroller.abortAnimation();
      }

      mLastMotionY = (int) ev.getY();
      mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
	      // 下面这个函数,在 onInterceptTouchEvent 中已经介绍过了,就是去勾搭支持 Nested 功能的祖先 view
      startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
      break;
    }
    case MotionEvent.ACTION_MOVE:
      
	      /************ 若干代码略去 ************/

      final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
      int deltaY = mLastMotionY - y;
      // dispatchNestedPreScroll 格外关注下
	      // 祖先 view 会根据 deltaY 和 mScrollOffset 来决定是否消费这个 touch 事件
      // 如果祖先 view 决定消费这个 MotionEvent,会把结果写在 mScrollConsumed 和 mScrollOffset 中
      if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
        deltaY -= mScrollConsumed[1];
        // 祖先 View 消费了 MotionEvent,引入视差值
 	        // 根据视差值,调整 MotionEvent etev
        vtev.offsetLocation(0, mScrollOffset[1]);
        mNestedYOffset += mScrollOffset[1];
      }
      if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
	        // 给 mIsBeingDragged 定性
        final ViewParent parent = getParent();
        if (parent != null) {
          // 被定性为滑动了,就不让父 View 拦截了
          parent.requestDisallowInterceptTouchEvent(true);
        }
        mIsBeingDragged = true;
        if (deltaY > 0) {
          deltaY -= mTouchSlop;
        } else {
          deltaY += mTouchSlop;
        }
      }
      if (mIsBeingDragged) {
        // 已被定性为拖拽
	        /************ 若干代码略去 ************/

	        // 根据当前的 scrollY 和 deltaY,scroll 到某一个特定的位置
        if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
           0, true) && !hasNestedScrollingParent()) {
		        // 如果没有 overScroll 且没有支持 nested 功能的父 View,速度追踪重置
  		      mVelocityTracker.clear();
	        }

        final int scrolledDeltaY = getScrollY() - oldY;
        final int unconsumedY = deltaY - scrolledDeltaY;
        // 每一次拖动都需要 NestedParentView 去计算是否视差了
        if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
	          // 父 View 为了视差消费了这次 MotionEvent
          mLastMotionY -= mScrollOffset[1];
          vtev.offsetLocation(0, mScrollOffset[1]);
          mNestedYOffset += mScrollOffset[1];
        }
	        /************ 若干代码略去 ************/

      }
      break;
    case MotionEvent.ACTION_UP:
      if (mIsBeingDragged) {
        // 手指松开,根据 fling 的速度滑动下去
        final VelocityTracker velocityTracker = mVelocityTracker;
        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
        int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
            mActivePointerId);

        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
          // 该函数内部调用了 dispatchNestedPreFling 和 dispatchNestedFling 跟 Behavior 挂钩
          // 同时也用 mScroller 实现了 fling 功能
          flingWithNestedDispatch(-initialVelocity);
        } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
            getScrollRange())) {
          ViewCompat.postInvalidateOnAnimation(this);
        }
      }
      mActivePointerId = INVALID_POINTER;
      endDrag(); // 此处调用了 stopNestedScroll 函数
      break;
  }

  /************ 若干代码略去 ************/

  // 默认情况下,如果 NestedScrollView 有机会消费 MotionEvent,就一定会消费掉的
  return true;
}

上述的代码中,ACTION_MOVE 实现 scroll 滑动功能比较隐晦,在一个 if 语句中,一方面做了是否 OverScroll 的判断,另一方面又做了 scrollTo 的工作。在 ACTION_UP 的代码段中,NestedScrollView 根据当前的滑动速度,使用 mScroller 将 NestedScrollView 的元素 fling 到目标位置。

NestedScrollView 的滑动功能,应该大致如此了。有些细节的知识点,限于篇幅问题,我并没有跟进去一探究竟。

然而 NestedScrollView,这个单词一分为二是 Nested 和 ScrollView,上面的一坨分析是有关 ScrollView 的,却一直回避了这个更靠前的单词:Nested。不过,还好我们之前做了一个铺垫。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文