- CompoundButton 源码分析
- LinearLayout 源码分析
- SearchView 源码解析
- LruCache 源码解析
- ViewDragHelper 源码解析
- BottomSheets 源码解析
- Media Player 源码分析
- NavigationView 源码解析
- Service 源码解析
- Binder 源码分析
- Android 应用 Preference 相关及源码浅析 SharePreferences 篇
- ScrollView 源码解析
- Handler 源码解析
- NestedScrollView 源码解析
- SQLiteOpenHelper/SQLiteDatabase/Cursor 源码解析
- Bundle 源码解析
- LocalBroadcastManager 源码解析
- Toast 源码解析
- TextInputLayout
- LayoutInflater 和 LayoutInflaterCompat 源码解析
- TextView 源码解析
- NestedScrolling 事件机制源码解析
- ViewGroup 源码解析
- StaticLayout 源码分析
- AtomicFile 源码解析
- AtomicFile 源码解析
- Spannable 源码分析
- Notification 之 Android 5.0 实现原理
- CoordinatorLayout 源码分析
- Scroller 源码解析
- SwipeRefreshLayout 源码分析
- FloatingActionButton 源码解析
- AsyncTask 源码分析
- TabLayout 源码解析
3. NestedScrollView 之 ScrollView
言归正传,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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论