透过NestedScrollView源码解析嵌套滑动原理
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
透过NestedScrollView 源码解析嵌套滑动原理
NestedScrollView 是⽤于替代 ScrollView 来解决嵌套滑动过程中的滑动事件的冲突。
作为开发者,你会发现很多地⽅会⽤到嵌套滑动的逻辑,⽐如下拉刷新页⾯,京东或者淘宝的各种商品页⾯。
那为什么要去了解 NestedScrollView 的源码呢?那是因为 NestedScrollView 是嵌套滑动实现的模板范例,通过研读它的源码,能够让你知道如何实现嵌套滑动,然后如果需求上 NestedScrollView ⽆法满⾜的时候,你可以⾃定义。
嵌套滑动
说到嵌套滑动,就得说说这两个类了:NestedScrollingParent3 和 NestedScrollingChild3 ,当然同时也存在后⾯不带数字的类。
之所以后⾯带数字了,是为了解决之前的版本遗留的问题:fling 的时候涉及嵌套滑动,⽆法透传到另⼀个View 上继续 fling ,导致滑动效果⼤打折扣。
其实 NestedScrollingParent2 相⽐ NestedScrollingParent 在⽅法调⽤上多了⼀个参数 type ,⽤于标记这个滑动是如何产⽣的。
type 的取值如下:
/**
* Indicates that the input type for the gesture is from a user touching the screen. 触摸产⽣的滑动
*/
public static final int TYPE_TOUCH = 0;
/**
* Indicates that the input type for the gesture is caused by something which is not a user
* touching a screen. This is usually from a fling which is settling. 简单理解就是fling
*/
public static final int TYPE_NON_TOUCH = 1;
嵌套滑动,说得通俗点就是⼦ view 和 ⽗ view 在滑动过程中,互相通信决定某个滑动是⼦view 处理合适,还是 ⽗view 来处理。
所以,Parent 和 Child
之间存在相互调⽤,遵循下⾯的调⽤关系:上图可以这么理解:
ACTION_DOWN 的时候⼦ view 就要调⽤ startNestedScroll( ) ⽅法来告诉⽗ view ⾃⼰要开始滑动了(实质上是寻找能够配合 child 进⾏嵌套滚动的 parent ),parent 也会继续向上寻找能够配合⾃⼰滑动的 parent ,可以理解为在做⼀些准备⼯作 。
⽗ view 会收到 onStartNestedScroll 回调从⽽决定是不是要配合⼦ view 做出响应。
如果需要配合,此⽅法会返回 true 。
继
⽽ onStartNestedScroll ()回调会被调⽤。
在滑动事件产⽣但是⼦ view 还没处理前可以调⽤ dispatchNestedPreScroll(0,dy,consumed,offsetInWindow) 这个⽅法把事件传给⽗view ,这样⽗ view 就能在onNestedPreScroll ⽅法⾥⾯收到⼦ view 的滑动信息,然后做出相应的处理把处理完后的结果通过
consumed 传给⼦ view 。
dispatchNestedPreScroll ()之后,child 可以进⾏⾃⼰的滚动操作。
如果⽗ view 需要在⼦ view 滑动后处理相关事件的话可以在⼦ view 的事件处理完成之后调⽤ dispatchNestedScroll 然后⽗ view 会在
onNestedScroll 收到回调。
最后,滑动结束,调⽤ onStopNestedScroll() 表⽰本次处理结束。
但是,如果滑动速度⽐较⼤,会触发 fling, fling 也分为 preFling 和 fling 两个阶段,处理过程和 scroll 基本差不多。
NestedScrollView
⾸先是看类的名字
class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView {
可以发现它继承了 FrameLayout,相当于它就是⼀个 ViewGroup,可以添加⼦ view , 但是需要注意的事,它只接受⼀个⼦ view,否则会报错。
@Override
public void addView(View child) {
if (getChildCount() > 0) {
throw new IllegalStateException("ScrollView can host only one direct child");
}
super.addView(child);
}
@Override
public void addView(View child, int index) {
if (getChildCount() > 0) {
throw new IllegalStateException("ScrollView can host only one direct child");
}
super.addView(child, index);
}
@Override
public void addView(View child, youtParams params) {
if (getChildCount() > 0) {
throw new IllegalStateException("ScrollView can host only one direct child");
}
super.addView(child, params);
}
@Override
public void addView(View child, int index, youtParams params) {
if (getChildCount() > 0) {
throw new IllegalStateException("ScrollView can host only one direct child");
}
super.addView(child, index, params);
}
add view
对于 NestedScrollingParent3,NestedScrollingChild3 的作⽤,前⽂已经说了,如果还是不理解,后⾯再对源码的分析过程中也会分析到。
其实这⾥还可以提⼀下 RecyclerView:
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
这⾥没有继承 NestedScrollingParent3 是因为开发者觉得 RecyclerView 适合做⼀个⼦类。
并且它的功能作为⼀个列表去展⽰,也就是不适合再 RecyclerView 内部去做⼀些复杂的嵌套滑动之类的。
这样 RecycylerView 外层就可以再嵌套⼀个 NestedScrollView 进⾏嵌套滑动了。
后⾯再分析嵌套滑动的时候,也会把 RecycylerView 当作⼦类来进⾏分析,这样能更好的理解源码。
内部有个接⼝,使⽤者需要对滑动变化进⾏监听的,可以添加这个回调:
public interface OnScrollChangeListener {
/**
* Called when the scroll position of a view changes.
*
* @param v The view whose scroll position has changed.
* @param scrollX Current horizontal scroll origin.
* @param scrollY Current vertical scroll origin.
* @param oldScrollX Previous horizontal scroll origin.
* @param oldScrollY Previous vertical scroll origin.
*/
void onScrollChange(NestedScrollView v, int scrollX, int scrollY,
int oldScrollX, int oldScrollY);
}
构造函数
下⾯来看下构造函数:
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initScrollView();
final TypedArray a = context.obtainStyledAttributes(
attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0);
// 是否要铺满全屏
setFillViewport(a.getBoolean(0, false));
a.recycle();
// 即是⼦类,⼜是⽗类
mParentHelper = new NestedScrollingParentHelper(this);
mChildHelper = new NestedScrollingChildHelper(this);
// ...because why else would you be using this widget? 默认是滚动,不然你使⽤它就没有意义了
setNestedScrollingEnabled(true);
ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE);
}
这⾥我们⽤了两个辅助类来帮忙处理嵌套滚动时候的⼀些逻辑处理,NestedScrollingParentHelper,NestedScrollingChildHelper。
这个是和前⾯的你实现的接⼝ NestedScrollingParent3,NestedScrollingChild3 相对应的。
下⾯看下 initScrollView ⽅法⾥的具体逻辑:
private void initScrollView() {
mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
// 会调⽤ ViewGroup 的 onDraw
setWillNotDraw(false);
// 获取 ViewConfiguration 中⼀些配置,包括滑动距离,最⼤最⼩速率等等
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
setFillViewport
在构造函数中,有这么⼀个设定:
setFillViewport(a.getBoolean(0, false));
与 setFillViewport 对应的属性是 android:fillViewport="true"。
如果不设置这个属性为 true,可能会出现如下图⼀样的问题:
xml 布局:
<?xml version="1.0" encoding="utf-8"?>
<NestedScrollView xmlns:android="/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#fff000">
<Button
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</NestedScrollView>
效果:
可以发现这个没有铺满全屏,可是 xml 明明已经设置了 match_parent 了。
这是什么原因呢?
那为啥设置 true 就可以了呢?下⾯来看下它的 onMeasure ⽅法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// false 直接返回
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
View child = getChildAt(0);
final youtParams lp = (LayoutParams) child.getLayoutParams();
int childSize = child.getMeasuredHeight();
int parentSpace = getMeasuredHeight()
- getPaddingTop()
- getPaddingBottom()
- lp.topMargin
- lp.bottomMargin;
// 如果⼦ view ⾼度⼩于⽗ view ⾼度,那么需要重新设定⾼度
if (childSize < parentSpace) {
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin,
lp.width);
// 这⾥⽣成 MeasureSpec 传⼊的是 parentSpace,并且⽤的是 MeasureSpec.EXACTLY
int childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(parentSpace, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
当你将 mFillViewport 设置为 true 后,就会把⽗ View ⾼度给予⼦ view 。
可是这个解释了设置 mFillViewport 可以解决不能铺满屏幕的问题,可是没有解决为啥 match_parent ⽆效的问题。
在回到类的继承关系上,NestedScrollView 继承的是 FrameLayout,也就是说,FrameLayout 应该和 NestedScrollView 拥有⼀样的问题。
可是当你把 xml 中的布局换成 FrameLayout 后,你发现竟然没有问题。
那么这是为啥呢?
原因是 NestedScrollView ⼜重写了 measureChildWithMargins 。
⼦view 的 childHeightMeasureSpec 中的 mode 是
MeasureSpec.UNSPECIFIED 。
当被设置为这个以后,⼦ view 的⾼度就完全是由⾃⾝的⾼度决定了。
@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);
// 在⽣成⼦ view 的 MeasureSpec 时候,传⼊的是 MeasureSpec.UNSPECIFIED
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
⽐如⼦ view 是 LinearLayout ,这时候,它的⾼度就是⼦ view 的⾼度之和。
⽽且,这个 MeasureSpec.UNSPECIFIED 会⼀直影响着后⾯的⼦⼦孙孙 view 。
我猜这么设计的⽬的是因为你既然使⽤了 NestedScrollView,就没必要在把⼦ View 搞得跟屏幕⼀样⼤了,它该多⼤就多⼤,不然你滑动的时候,看见⼀⼤⽚空⽩体验也不好啊。
⽽ ViewGroup 中,measureChildWithMargins 的⽅法是这样的:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
由于⼀般使⽤ NestedScrollView 的时候,都是会超过屏幕⾼度的,所以不设置这个属性为 true 也没有关系。
绘制
既然前⾯已经把 onMeasure 讲完了,那索引把绘制这块都讲了把。
下⾯是 draw ⽅法,这⾥主要是绘制边界的阴影:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mEdgeGlowTop != null) {
final int scrollY = getScrollY();
// 上边界阴影绘制
if (!mEdgeGlowTop.isFinished()) {
final int restoreCount = canvas.save();
int width = getWidth();
int height = getHeight();
int xTranslation = 0;
int yTranslation = Math.min(0, scrollY);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
width -= getPaddingLeft() + getPaddingRight();
xTranslation += getPaddingLeft();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
height -= getPaddingTop() + getPaddingBottom();
yTranslation += getPaddingTop();
}
canvas.translate(xTranslation, yTranslation);
mEdgeGlowTop.setSize(width, height);
if (mEdgeGlowTop.draw(canvas)) {
ViewCompat.postInvalidateOnAnimation(this);
}
canvas.restoreToCount(restoreCount);
}
// 底部边界阴影绘制
if (!mEdgeGlowBottom.isFinished()) {
final int restoreCount = canvas.save();
int width = getWidth();
int height = getHeight();
int xTranslation = 0;
int yTranslation = Math.max(getScrollRange(), scrollY) + height;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
width -= getPaddingLeft() + getPaddingRight();
xTranslation += getPaddingLeft();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
height -= getPaddingTop() + getPaddingBottom();
yTranslation -= getPaddingBottom();
}
canvas.translate(xTranslation - width, yTranslation);
canvas.rotate(180, width, 0);
mEdgeGlowBottom.setSize(width, height);
if (mEdgeGlowBottom.draw(canvas)) {
ViewCompat.postInvalidateOnAnimation(this);
}
canvas.restoreToCount(restoreCount);
}
}
}
onDraw 是直接⽤了⽗类的,这个没啥好讲的,下⾯看看 onLayout:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mIsLayoutDirty = false;
// Give a child focus if it needs it
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null;
if (!mIsLaidOut) { // 是否是第⼀次调⽤onLayout
// If there is a saved state, scroll to the position saved in that state.
if (mSavedState != null) {
scrollTo(getScrollX(), mSavedState.scrollPosition);
mSavedState = null;
} // mScrollY default value is "0"
// Make sure current scrollY position falls into the scroll range. If it doesn't,
// scroll such that it does.
int childSize = 0;
if (getChildCount() > 0) {
View child = getChildAt(0);
youtParams lp = (LayoutParams) child.getLayoutParams();
childSize = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
int parentSpace = b - t - getPaddingTop() - getPaddingBottom();
int currentScrollY = getScrollY();
int newScrollY = clamp(currentScrollY, parentSpace, childSize);
if (newScrollY != currentScrollY) {
scrollTo(getScrollX(), newScrollY);
}
}
// Calling this with the present values causes it to re-claim them
scrollTo(getScrollX(), getScrollY());
mIsLaidOut = true;
}
onLayout ⽅法也没什么说的,基本上是⽤了⽗类 FrameLayout 的布局⽅法,加⼊了⼀些 scrollTo 操作滑动到指定位置。
嵌套滑动分析
如果对滑动事件不是很清楚的⼩伙伴可以先看看这篇⽂章:。
在分析之前,先做⼀个假设,⽐如 RecyclerView 就是 NestedScrollView 的⼦类,这样去分析嵌套滑动更容易理解。
这时候,⽤户点击RecyclerView 触发滑动。
需要分析整个滑动过程的事件传递。
dispatchTouchEvent
这⾥,NestedScrollView ⽤的是⽗类的处理,并没有添加⾃⼰的逻辑。
onInterceptTouchEvent
当事件进⾏分发前,ViewGroup ⾸先会调⽤ onInterceptTouchEvent 询问⾃⼰要不要进⾏拦截,不拦截,就会分发传递给⼦ view。
⼀般来说,对于 ACTION_DOWN 都不会拦截,这样⼦类有机会获取事件,只有⼦类不处理,才会再次传给⽗ View 来处理。
下⾯来看看其具体代
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
// 如果已经在拖动了,说明已经在滑动了,直接返回 true
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content. 不是⼀个有效的id
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
// 计算垂直⽅向上滑动的距离
final int yDiff = Math.abs(y - mLastMotionY);
// 确定可以产⽣滚动了
if (yDiff > mTouchSlop
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
// 可以获取滑动速率
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
final ViewParent parent = getParent();
if (parent != null) {
// 让⽗ view 不要拦截,这⾥应该是为了保险起见,因为既然已经⾛进来了,只要你返回 true,⽗ view 就不会拦截了。
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
// 如果点击的范围不在⼦ view 上,直接break,⽐如⾃⼰设置了很⼤的 margin,此时⽤户点击这⾥,这个范围理论上是不参与滑动的if (!inChild((int) ev.getX(), y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
// 在收到 DOWN 事件的时候,做⼀些初始化的⼯作
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*/
puteScrollOffset();
// 如果此时正在fling, isFinished 会返回 flase
mIsBeingDragged = !mScroller.isFinished();
// 开始滑动
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
// ⼿抬起后,停⽌滑动
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
onInterceptTouchEvent 事件就是做⼀件事,决定事件是不是要继续交给⾃⼰的 onTouchEvent 处理。
这⾥需要注意的⼀点是,如果⼦ view 在 dispatchTouchEvent 中调⽤了:
parent.requestDisallowInterceptTouchEvent(true)
那么,其实就不会再调⽤ onInterceptTouchEvent ⽅法。
也就是说上⾯的逻辑就不会⾛了。
但是可以发现,down 事件,⼀般是不会拦截的。
但是如果正在fling,此时就会返回 true,直接把事件全部拦截。
那看下 RecyclerView 的dispatchTouchEvent 是⽗类的,没啥好分析的。
⽽且它的 onInterceptTouchEvent 也是做了⼀些初始化的⼀些⼯作,和 NestedScrollView ⼀样没啥可说的。
onTouchEvent
再说 NestedScrollView 的 onTouchEvent。
对于 onTouchEvent 得分两类进⾏讨论,如果其⼦ view 不是 ViewGroup ,且是不可点击的,就会把事件直接交给 NestedScrollView 来处理。
但是如果点击的⼦ view 是 RecyclerView 的 ViewGroup 。
当 down 事件来的时候,ViewGroup 的⼦ view 没有处理,那么就会交给ViewGroup 来处理,你会发现ViewGroup 的 onTouchEvent 是默认返回 true 的。
也就是说事件都是由 RecyclerView 来处理的。
这时候来看下 NestedScrollView 的 onTouchEvent 代码:
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
// 需要有⼀个⼦类才可以进⾏滑动
if (getChildCount() == 0) {
return false;
}
// 前⾯提到如果⽤户在 fling 的时候,触碰,此时是直接拦截返回 true,⾃⼰来处理事件。
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.处理结果就是停⽌ fling
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
// 寻找嵌套⽗View,告诉它准备在垂直⽅向上进⾏ TOUCH 类型的滑动
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
// 滑动前先把移动距离告诉嵌套⽗View,看看它要不要消耗,返回 true 代表消耗了部分距离if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
// 滑动距离⼤于最⼤最⼩触发距离
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
// 触发滑动
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = getScrollY();
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
|| (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollByCompat will call onOverScrolled, which
// calls onScrollChanged if applicable.
// 该⽅法会触发⾃⾝内容的滚动
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
// 通知嵌套的⽗ View 我已经处理完滚动了,该你来处理了
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
// 如果嵌套⽗View 消耗了滑动,那么需要更新
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
ensureGlows();
final int pulledToY = oldY + deltaY;
// 触发边缘的阴影效果
if (pulledToY < 0) {
EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex)
/ getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
puteCurrentVelocity(1000, mMaximumVelocity);
// 计算滑动速率
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
// ⼤于最⼩的设定的速率,触发fling
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
mActivePointerId = INVALID_POINTER;
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
mActivePointerId = INVALID_POINTER;
endDrag();
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
ACTION_DOWN
先看 down 事件,如果处于 fling 期间,那么直接停⽌ fling, 接着会调⽤ startNestedScroll,会让 NestedScrollView 作为⼦ view 去通知嵌套⽗ view,那么就需要找到有没有可以嵌套滑动的⽗ view 。
public boolean startNestedScroll(int axes, int type) {
// 交给 mChildHelper 代理来处理相关逻辑
return mChildHelper.startNestedScroll(axes, type);
}
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
// 找到嵌套⽗ view 了,就直接返回
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
// 是否⽀持嵌套滚动
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) { // while 循环,将⽀持嵌套滑动的⽗ View 找出来。
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
// 把⽗ view 设置进去
setNestedScrollingParentForType(type, p);
// 找到后,通过该⽅法可以做⼀些初始化操作
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
可以看到,这时候主要就是为了找到嵌套⽗ view。
当 ViewParentCompat.onStartNestedScroll 返回 true,就表⽰已经找到嵌套滚动的⽗View 了。
下⾯来看下这个⽅法的具体逻辑:
// ViewParentCompat
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
这⾥其实没啥好分析,就是告诉⽗类当前是什么类型的滚动,以及滚动⽅向。
其实这⾥可以直接看下 NestedScrollView
的 onStartNestedScroll 的逻辑。
// NestedScrollView
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
int type) {
// 确保触发的是垂直⽅向的滚动
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
当确定了嵌套⽗ View 以后,⼜会调⽤⽗ view 的 onNestedScrollAccepted ⽅法,在这⾥可以做⼀些准备⼯作和配置。
下⾯我们看到的是Ns ⾥⾯的⽅法,注意不是⽗ view 的,只是当作参考。
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
mParentHelper.onNestedScrollAccepted(child, target, axes, type);
// 这⾥ Ns 作为⼦ view 调⽤该⽅法去寻找嵌套⽗ view。
注意这个⽅法会被调⽤是 NS 作为⽗ view 收到的。
这样就
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
}
到这⾥,down 的作⽤就讲完了。
ACTION_MOVE
⾸先是会调⽤ dispatchNestedPreScroll,讲当前的滑动距离告诉嵌套⽗ View。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
// Ns 作为⼦ view 去通知⽗View
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
下⾯看下 mChildHelper 的代码逻辑:
/**
* Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent.
*
* <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
* method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same
* signature to implement the standard policy.</p>
*
* @return true if the parent consumed any of the nested scroll
*/
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
// 获取之前找到的嵌套滚动的⽗ View
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
// 滑动距离肯定不为0 才有意义
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
// 调⽤嵌套⽗ View 的对应的回调
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
这⾥主要是将滑动距离告诉⽗ view,有消耗就会返回 true 。
// ViewParentCompat
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed) {
onNestedPreScroll(parent, target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
}
其实下⾯的 onNestedPreScroll 跟前⾯的 onStartNestedScroll 逻辑很像,就是层层传递。
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
parent.onNestedPreScroll(target, dx, dy, consumed);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onNestedPreScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
}
下⾯为了⽅便,没法查看 NS 的嵌套⽗ View 的逻辑。
直接看 Ns 中对应的⽅法。
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {。