- 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。不过,还好我们之前做了一个铺垫。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论