CoordinatorLayout三部曲学习之一:Nest接口的实现

NestScrollChild和NestedScrollingParent

吐槽一下开源中国居然标题字数有限制java

因为项目中使用了CoordinatorLayout来解决联动以及实现炫酷的UI效果,那么必须就要研究一波源码了,毕竟知其然知其因此然。因此这篇文章是CoordinatorLayout 学习的三部曲的开篇。android

在学习CoordinatorLayout以前,首先先来学习一下CoordinatorLayout中实现联动的关键类:NestScrollChild和NestedScrollingParent。spring

简单查看一下两个接口的定义,首先来看下NestedScrollingParent的:app

public interface NestedScrollingParent {
    //当前方法会在嵌套的子View调用ViewCompat.startNestedScroll(View, int)方法的时候会触发,告诉父View我要滑动了,返回值为真说明父View接收当前滑动操做
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
    //当上面的方法返回true后,此方法会回调到
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
	//当子View滑动中止后,该方法会回调到
    void onStopNestedScroll(@NonNull View target);
	//当onStartNestedScroll返回true后,子View在滑动时候会回调这个方法,能够理解为ProgressBar的onProgress方法
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);
	//该方法在onNestedScroll回调以前调用,给个父View一个消费滑动的机会
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
     //子View在进行fling时候的回调,与onNestedScroll相似,行为不一样而已
    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
	//该方法在onNestedFling回调以前调用
    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
    //当前的滑动方向
    //ViewCompat#SCROLL_AXIS_HORIZONTAL
	//ViewCompat#SCROLL_AXIS_VERTICAL
	//ViewCompat#SCROLL_AXIS_NONE
    @ScrollAxis
    int getNestedScrollAxes();
}

NestedScrollingParent须要由实现滚动联动的父类实现,当实现NestedScrollingParent接口的时候,在此父类内部须要维护一个NestedScrollingParentHelper来进行触摸处理以及分发。上述方法的做用在注释中解释。ide

上述注释看起来可能仍是有点抽象,等会跟着源码(NestedScrollView)过一遍就很清晰了。post

接下来再看下NestedScrollingChild的定义:学习

public interface NestedScrollingChild {
	//是否启动联动
    void setNestedScrollingEnabled(boolean enabled);
	//返回是否联动
    boolean isNestedScrollingEnabled();
	//开始滑动
    boolean startNestedScroll(@ScrollAxis int axes);
	//中止滑动滑动
    void stopNestedScroll();
	//是否拥有滑动嵌套的父View
    boolean hasNestedScrollingParent();
	//分发滑动事件
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
	//分发滑动事件前的处理
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow);
	//分发Fling
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
	//分发Fling前的回调
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

咱们以NestedScrollView为联动的父View,NestedScrollView为子View来进行代码逻辑的分析。this

分析完后才以为不应选择上述状况分析,容易混淆,毕竟在一个NestedScrollView中回调来,回调去的spa

xml以下:code

<com.lin.aloha.photo.FatherScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_height="match_parent"
    tools:context=".photo.TestActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_height="match_parent">

        <View
            android:layout_width="match_parent"
            android:background="#dd2288"
            android:layout_margin="20dp"
            android:layout_height="200dp">

        </View>
        <com.lin.aloha.photo.ChildScrollView
            android:layout_width="match_parent"
            android:background="#1d9d29"
            android:layout_height="400dp">
            <LinearLayout
                android:layout_width="match_parent"
                android:orientation="vertical"
                android:layout_height="wrap_content">
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>

                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
            </LinearLayout>
        </com.lin.aloha.photo.ChildScrollView>
        <View
            android:layout_width="match_parent"
            android:background="#dd2288"
            android:layout_margin="20dp"
            android:layout_height="200dp">

        </View>
    </LinearLayout>
</com.lin.aloha.photo.FatherScrollView>

咱们使用这个就能直接实现联动的效果,效果图以下:

NestedScrollView实现了NestedScrollingParent以及NestedScrollingChild2接口,NestedScrollingChild2就是NestedScrollingChild一些方法的拓展,核心是不变的。因为实现了这两个接口,能够说明联动的时候其能当作父View也能当作子View来处理。这里咱们首先把它当作父View来学习,既然是父View的话,天然要从dispatchTouchEvent()开始,可是一查发现并无复写该方法,因此咱们从onInterceptTouchEvent()看起:

public boolean onInterceptTouchEvent(MotionEvent ev) {
 	//mIsBeingDragged标识当前View是否在移动,包括咱们触摸移动以及正在进行fling操做的时候。
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }

        switch (action & MotionEvent.ACTION_MASK) {
				case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
				...
					//记录触摸的y点
                mLastMotionY = y;
                mActivePointerId = ev.getPointerId(0);
				//初始化或者重置VelocityTracker
                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                mScroller.computeScrollOffset();
                mIsBeingDragged = !mScroller.isFinished();//若是还在滑动,则为true
				//调用mChildHelper.startNestedScroll()
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                final int activePointerId = mActivePointerId;
                ...
                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) {
					//主动告诉parent不要调用parent的interceptTouchEvent,意思是我要来消费触摸事件
                     parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                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()事件处理比较简单:

  • 首先在ACTION_DOWN中会去调用startNestedScroll()方法,然而该NestedScrollView因为没父View,因此该方法没意义。
  • 在ACTION_MOVE将mIsBeingDragged设置为true,说明要把事件拦截并传递给自身的onTouch()来处理事件。
  • 在ACTION_UP或者ACTION_CANCEL中调用stopNestedScroll(),这个方法也没意义(缘由同第一条),而后把mIsBeingDragged从新设为false。

咱们再看下onTouchEvent()事件,

public boolean onTouchEvent(MotionEvent ev) {
       ...
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
               ...
              	//获取坐标
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                //2.调用的NestedScrollingChild的startNestedScroll()方法
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
            }
            case MotionEvent.ACTION_MOVE:
                ...
                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
				//父类View不执行dispatchNestedPreScroll()方法,下面分析子View的NestedScrollView会用到
                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;//标识为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();//默认为OVER_SCROLL_ALWAYS
                    boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
                            || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
					//最终调用scrollTo方法进行滑动
                    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)) {
                        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;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                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;
          
          ...
        return true;//返回值为true
    }

从上面的步骤来看,父View彻底没有涉及到联动的操做,那么点击第一个紫红色的View,这个View在父NestedScrollView中,而且没任何触摸事件的处理,那么最终会调到父View的onTouchEvent()中,能够看下面的log,也正是这种状况。

抛出个问题:在log中看到调用了两次的startNestedScroll()方法,这个会不会有什么影响呢,答案是不会的,具体的在子NestedScrollView分析

那么接下来分析一会儿NestedScrollView的行为,在父NestedScrollView中分析过,onInterceptEvent(...)方法会调用到NestedScrollingChildHelper.startNestedScroll(...)方法,对于父NestedScrollView来讲,是没多大意义的,但对于子NestedScrollView来讲那就有意义了,NestedScrollingChildHelper.startNestedScroll(...)实现以下所示:

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
    	//是否支持嵌套滑动
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            //从子View向外查询第一个接收滑动的父View,在当前例子中就是父NestedScrollView
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    //调用onNestedScrollAccepted()通知父View
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

首先看下hasNestedScrollingParent()方法:

public boolean hasNestedScrollingParent(@NestedScrollType int type) {
        return getNestedScrollingParentForType(type) != null;
    }
 private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
        switch (type) {
            case TYPE_TOUCH:
                return mNestedScrollingParentTouch;
            case TYPE_NON_TOUCH:
                return mNestedScrollingParentNonTouch;
        }
        return null;
    }

第一次调用时候,因为mNestedScrollingParentTouch为null,因此会接着往下走,而后在setNestedScrollingParentForType()绑定对应的mNestedScrollingParentTouch,第二次调用时候,mNestedScrollingParentTouch不为null,天然就不会往下走,(并且在咱们中止滑动的时候,mNestedScrollingParentTouch会设置为Null的)也就不会调用到ViewParentCompat.onStartNestedScroll()方法了。所以上面提的问题就解决了,虽然调用了两次startNestedScroll()方法,对于代码逻辑是没有大影响的。

回到startNestedScroll()方法中,接下来的主要有三部分组成:

  • 调用isNestedScrollingEnabled()判断是否支持嵌套滑动,支持则往下走,不然返回false
  • 调用ViewParentCompat.onStartNestedScroll()方法通知父View开始滑动了
  • 调用 ViewParentCompat.onNestedScrollAccepted()方法

上述的onStartNestedScroll(...)以及onNestedScrollAccepted()最终都会回调到实现NestedScrollingParent接口的相同方法名的回到当中,在这里就是回调到父NestedScrollView的对应方法中:

//NestedScrollView
    @Override //判断是不是垂直滑动
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override//
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);//三级联动时会有用,这里Demo中不考虑
    }
		//NestedScrollingParentHelper
	    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
            @ScrollAxis int axes) {
        onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
    }

上述能够看到,咱们在触摸子NestedScrollView的时候,经过NestScrollChild2.startNestScroll(),会调用到父NestedScrollView实现NestedScrollingParent的onNestedScrollAccepted()和onStartNestedScroll()。因为子NestedScrollView的子View们不涉及触摸事件的处理,因此正当会回调到子View的onTouchEvent()中,从新看下ACTION_DOWN方法:

case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    final ViewParent parent = getParent();
                    if (parent != null) {//这里能够看到直接通知父NestedScrollView不拦截触摸事件
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }

                /*
                 * If being flinged and user touches, stop the fling. isFinished
                 * will be false if being flinged.
                 */
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }

                // Remember where the motion event started
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
            }

在子NestedScrollView中的onTouchEvent()中调用了requestDisallowInterceptTouchEvent(true)通知父NestedScrollView不拦截触摸事件,且子NestedScrollView的onTouchEvent()永远返回true,则父NestedScrollView在当前情境下的onTouchEvent()是永远不会被调用到的。

接着继续往下看,因为onInterceptTouchEvent()中没有涉及到对应联动的操做,这里就不看了,直接看onTouchEvent()的ACTION_MOVE操做吧,从新贴下代码:

case MotionEvent.ACTION_MOVE:
                ...
                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
				//1
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                        ViewCompat.TYPE_TOUCH)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
               ...
			   //mIsBeingDragged为true
                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);
							//滑动该NestedScrollView
					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;
					//2
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    } else if (canOverscroll) {
                       ...//处理边缘事件
                    }
                }
                break;

首先在1中, 调用mChildHelper.dispatchNestedPreScroll()方法:

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            ...
            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;
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
				...
				return consumed[0] != 0 || consumed[1] != 0;
        }
        return false;
    }
	
	 @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        dispatchNestedPreScroll(dx, dy, consumed, null);//三层联动会调用到,可忽略
    }

上述代码主要调用到了ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type)方法通知父ScrollView经过consumed参数决定是否消费滑动事件,最终仍是回调会NestedScrollView.onNestedPreScroll()回调中,因为父NestedScrollView没有parent,因此dispatchNestedPreScroll()逻辑不会调用,至关于父NestedScrollView什么都不作。

而后在2中,调用dispatchNestedScroll()方法,方法中的uncomsued表明父NestedScrollView消费后剩下的须要子NestedScrollView滑动的距离。

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                ...
                ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed, type);

               ...
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

最终回调会父NestedScrollView的onNestedScroll()中,调用scrollBy()方法进行滑动:

@Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed) {
        final int oldScrollY = getScrollY();
        scrollBy(0, dyUnconsumed);
      ...
    }

接下来看下最后的ACTION_UP方法:

case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                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;
    private void endDrag() {
        mIsBeingDragged = false;

        recycleVelocityTracker();
        stopNestedScroll(ViewCompat.TYPE_TOUCH);//调用stopNestedScroll中止滑动

        if (mEdgeGlowTop != null) {
            mEdgeGlowTop.onRelease();
            mEdgeGlowBottom.onRelease();
        }
    }

这个ACTION_UP中相关的就只有stopNestedScroll()方法,经过调用该方法通知父NestedScrollView的onStopNestScroll()方法中:

@Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
       ...
    }
	
	//重置方向参数
	public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
        mNestedScrollAxes = 0;
    }

当咱们手指离开屏幕时候,就是fling滑动开始的时候了,也就是说在ACTION_UP方法中进行,再看下ACTION_UP方法:

case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
				//速度大于最小速度,则执行Nest下发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;
				
    private void flingWithNestedDispatch(int velocityY) {
        final int scrollY = getScrollY();
        final boolean canFling = (scrollY > 0 || velocityY > 0)
                && (scrollY < getScrollRange() || velocityY < 0);
        if (!dispatchNestedPreFling(0, velocityY)) {//若是父NestScrollView不消费fling,则由子NestedScrollView进行消费,通知回调onNestedFling()方法给父NestScrollView
           dispatchNestedFling(0, velocityY, canFling);
            fling(velocityY);
        }
    }

fling操做比较简单,这里就不叙述了。


总体的流程算是分析完了一遍,下面整理一张对应的流程图表示一下:

源码走一遍后发现,子View赋予了父View处理滑动的机会,这个触摸传递的模式彻底不一样,触摸事件传递是从上到下的,而上述的两个接口则提供了一种从下到上的方式,使得处理触摸事件更加的灵活。举个例子体验一下:

当子NestedScollView还没滑动到顶部或者底部时候,因此触摸事件由子NestedScrollView处理,当到顶部或者底部的时候,则借由NestedScrollChild.dispatchNestedScroll()传递给NestedScrollParent.onNestedScroll()方法处理,也就解释为何子NestedScrollView滑动到顶部时,手指再往下滑,就会滑动父NestedScrollView。

能够直接看onTouchEvent()的ACTION_MOVE方法验证正确与否:

...
 final int oldY = getScrollY();
...
 final int scrolledDeltaY = getScrollY() - oldY;
 final int unconsumedY = deltaY - scrolledDeltaY;
 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
 } else if (canOverscroll) {
					
					...

当咱们滑动子NestedScrollView时候因为子NestedScrollView没有到头,scrolledDeltaY计算获取子NestedScrollView滑动距离,这时候是与deltaY的值,相等的,因此unconsumedY=0,也就是说父NestedScrollView没有消费的距离,当子NestedScrollView到头时候,scrolledDeltaY就等于0了,滑动的距离就由unconsumedY记录下来传递给父NestedScrollView,父NestedScrollView最终调用scrollTo()方法完成了滑动。

有时间的话再写一个实现Nest的例子

相关文章
相关标签/搜索