Android NestedScrolling 嵌套滚动原理解析

Android NestedScrolling 嵌套滚动原理解析

  • 一.原有问题
  • 二.解决方案
    • 1.实现原理
    • 2.方案设计
      • (1)android的support-v4包提供了两个接口来实现NestedScroll框架
      • (2)android的support-v4包也提供了两个相应的Helper类来实现通用功能
      • (3)兼容性
      • (4)流程
  • 三.源码分析
    • NestedScrollView/ScrollView
  • 四.NestedScrollView+RecyclerView解析

一.原有问题

众所周知,android的触摸事件传递有局限性,当比较复杂的可滚动控件嵌套在一起的时候,总会有各种各样的滑动问题,这与android的触摸事件传递机制(View触摸事件机制)密不可分:

android的触摸事件传递是从上至下的递归传递,如果某次DOWN事件,有子view消费了,则之后的所有事件都只可能交由该子view处理,其父view没有机会再去处理(只能拦截);

并且很多ViewGroup,比如ViewPager、ScrollView,直接在onInterceptTouchEvent方法中,将move事件拦截,为的是交由自己处理,而没有兼顾到其子view可能的滑动;

所以原有的触摸传递问题的局限性就是:一旦子view处理事件后,父view就不能处理了,并且因此导致许多ViewGroup为了自己处理,直接拦截事件并没有交由子view处理事件,导致滑动效果的局限性很大

二.解决方案

1.实现原理

android在5.0后,对这块的问题做了很大的改善,其根本解决办法就是:既然原有问题是子view处理事件后父view就处理不了,那么就想办法在子view处理前,给父view一个处理的机会就好了。

这样有三个好处:

  1. 不影响核心的触摸事件传递机制,还是从上至下递归执行;一个view处理后不会交由其他view处理(onTouchEvent)

  2. 父view不用为了自己处理而武断的拦截事件,因为自己也会有一个处理事件的机会

  3. 可以让一次触摸事件在多层view中都应用上去,即可以实现滚动view内部嵌套滚动view的效果

2.方案设计

(1)android的support-v4包提供了两个接口来实现NestedScroll框架

NestedScrollingChild:提供了作为内部嵌套类应该实现的方法

public interface NestedScrollingChild {
    //是否可以嵌套滚动
    public void setNestedScrollingEnabled(boolean enabled);
	
	//自身是否支持嵌套滚动
	public boolean isNestedScrollingEnabled();

    //开始内部嵌套滚动,返回值为是否可以内部嵌套滚动,参数为滚动方向
    public boolean startNestedScroll(int axes);

    //停止内部嵌套滚动
    public void stopNestedScroll();

    //是否已经有支持其内部嵌套滚动的父view
    public boolean hasNestedScrollingParent();

    //在内部嵌套滚动时,派发给支持其嵌套滚动的parent,使其有机会做一些滚动的处理
	//参数为x/y轴已消费的和未消费的距离
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    //在内部嵌套滚动前,派发给支持其嵌套滚动的parent,使其有机会做一些滚动的预处理
	//dx,dy为可以消耗的距离,consumed为已经消耗的距离
	//返回值为支持其嵌套滚动的parent是否消费了部分距离
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    //在内部嵌套自由滑动时,派发给支持其嵌套滚动的parent,使其有机会做一些自由滑动的处理
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    //在内部嵌套自由滑动前,派发给支持其嵌套滚动的parent,使其有机会做一些自由滑动的预处理
	//返回值为支持其嵌套滚动的parent是否消费了fling事件
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

NestedScrollingParent:提供了作为支持内部嵌套滚动的view的方法

public interface NestedScrollingParent {
    //是否接受此次的内部嵌套滚动
	//target是想要内部滚动的view,child是包含target的parent的直接子view
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    //接受内部滚动后,做一些预处理工作
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    //停止了内部滚动
    public void onStopNestedScroll(View target);

    //内部嵌套滚动开始,根据已消费和未消费的距离参数进行应用
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    //内部嵌套滚动开始前做一些预处理,主要是根据dx,dy,将自己要消费的距离计算出来,告知target(通过consumed一层层记录实现)
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    //内部嵌套滑动开始,consumed参数为target是否消费了fling事件,parent可以根据此来做出自己的选择
	//返回值为parent自己是否消费了fling事件
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    //内部嵌套滑动开始前做一些预处理
	//返回值为parent自己是否消费此次fling事件
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
} 

(2)android的support-v4包也提供了两个相应的Helper类来实现通用功能

NestedScrollingChildHelper:内部嵌套滚动view实现NestedScrollingChild的一些通用实现

public class NestedScrollingChildHelper {
    private final View mView;
    private ViewParent mNestedScrollingParent;
    private boolean mIsNestedScrollingEnabled;

    //view作为内部嵌套滚动的view
    public NestedScrollingChildHelper(View view) {
        mView = view;
    }

    //开始嵌套滚动的方法
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            //之前已经找到了支持嵌套滚动的parent,说明正在进行嵌套滚动,则返回true即可
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
				//向上找到支持内部嵌套滚动的parent,并记录下来
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

    //派发滚动事件
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
			//已经消费部分距离或者还有未消费距离时,需要派发给parent进行处理
            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }
				//派发,即调用到NestedScrollingParent的onNestedScroll中进行处理
                ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed);

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

    //派发预滚动事件
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            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;
				//派发,即调用到NestedScrollingParent的onNestedPreScroll中进行处理
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
				//parent有消费距离则返回true
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
	...
} 

NestedScrollingParentHelper:支持内部嵌套滚动的parent实现NestedScrollingParent的一些通用实现

public class NestedScrollingParentHelper {
    private final ViewGroup mViewGroup;
    private int mNestedScrollAxes;

    //支持内部嵌套滚动的view
    public NestedScrollingParentHelper(ViewGroup viewGroup) {
        mViewGroup = viewGroup;
    }

    //接受内部滚动时,记录下内部滚动方向
    public void onNestedScrollAccepted(View child, View target, int axes) {
        mNestedScrollAxes = axes;
    }
	...
}

(3)兼容性

我们可以注意到,在Helper类里面的方法调用,使用了ViewParentCompat类,顾名思义是用来做兼容的,即不同版本系统支持的功能不一样,需要用不同的方式去实现一样的效果,在Compat类里做处理,那么这块功能为什么需要兼容呢?

  1. 因为NestedScrolling这一套接口虽然是support包里的,但是原有的控件并没有实现这些接口(support包里的控件以及自定义的可以实现这些接口)

  2. android5.0开始,framework在ViewParent里面实现了这些接口的同名方法,使得可以直接调用ViewParent的这些方法;但是5.0之前没有这些方法

综上所述,Compat包里需要做的就是,5.0之后的系统,可以直接调用ViewParent的相关方法;5.0之前只得调用实现了这些接口的类的相关方法

//ViewParentCompat.onStartNestedScroll(p, child, mView, axes)
 
//Compat调用方法
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
 
//IMPL通过系统版本生成不同的兼容类
static final ViewParentCompatImpl IMPL;
static {
    final int version = Build.VERSION.SDK_INT;
    if (version >= 21) {
        IMPL = new ViewParentCompatLollipopImpl();
    } else if (version >= 19) {
        IMPL = new ViewParentCompatKitKatImpl();
    } else if (version >= 14) {
        IMPL = new ViewParentCompatICSImpl();
    } else {
        IMPL = new ViewParentCompatStubImpl();
    }
}
 
//ViewParentCompatStubImpl(5.0之前的处理方式)
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
	//需要ViewParent实现NestedScrollingParent接口才能调用
    if (parent instanceof NestedScrollingParent) {
        return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                nestedScrollAxes);
    }
    return false;
}
 
//ViewParentCompatLollipopImpl(5.0之后的处理方式)
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    return ViewParentCompatLollipop.onStartNestedScroll(parent, child, target,
            nestedScrollAxes);
}
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    try {
		//5.0后ViewParent实现了该方法,可以直接调用
        return parent.onStartNestedScroll(child, target, nestedScrollAxes);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onStartNestedScroll", e);
        return false;
    }
} 

(4)流程

上面就是整个支持内部嵌套滚动的框架,通过这些方法和Helper的调度逻辑,可以大致看出应有的实现流程
Android NestedScrolling 嵌套滚动原理解析_第1张图片

三.源码分析

通过上面的叙述,应该已经对内部嵌套滚动的实现有了大概的轮廓,也对实现的流程有了大致的逻辑,现在就来通过具体的例子来看看,已经实现了内部嵌套滚动的View具体是如何来把这些流程实现的

NestedScrollView/ScrollView

这里我们先通过NestedScrollView的实现,并与原有的5.0之前的ScrollView作对比,看看是有何差别的吧!

(1)首先来看看5.0之前的ScrollView

public boolean onInterceptTouchEvent(MotionEvent ev) {
	...
    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {//移动时
            final int pointerIndex = ev.findPointerIndex(activePointerId);
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop) {
                mIsBeingDragged = true;//直接进行ScrollView的拖拽,即拦截了事件传递
                ...
            }
            break;
        }
		...
    }
    return mIsBeingDragged;
}

可见,5.0之前,ScrollView(包括大多数可滑动的控件)都在onInterceptTouchEvent里将事件直接拦下,没有交由child处理。

(2)再来看看5.0之后的ScrollView,因为support包提供了和ScrollView类似的NestedScrollView,且不用考虑版本兼容问题,所以我们直接看NestedScrollView就好

首先NestedScrollView实现了Child和Parent两个接口,说明其既可以作为内部嵌套滚动的View,也可以作为支持内部嵌套滚动的容器

//1.拦截与否
 public boolean onInterceptTouchEvent(MotionEvent ev) {
    ...
    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {//移动时
            ...
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop
                    && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {//如果当前没有内部嵌套滚动,才会进行拦截,这样就会有交由child处理的机会
                mIsBeingDragged = true;
                ...
            }
            break;
        }
        ...
    }
    return mIsBeingDragged;
}

上面这段代码说明了NestedScrollView不会武断的拦截事件,而是对于可嵌套滑动的child,将事件优先传给他做处理,自己作为parent也有机会处理,这样互不影响,是正确的思路

//1.自己处理
public boolean onTouchEvent(MotionEvent ev) {
    ...
    switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {//按下时开始尝试内部嵌套滚动(找到接受内部嵌套滚动的parent并记录状态)
            ...
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
            break;
        }
        case MotionEvent.ACTION_MOVE:
            ...
            final int y = (int) ev.getY(activePointerIndex);
            int deltaY = mLastMotionY - y;
            if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {//计算出总共的deltaY,开始派发内部滚动预处理事件给parent,统计parent总共消费的距离
				//如果parent有所消费,则计算自己当前还能消费的距离
                deltaY -= mScrollConsumed[1];
                vtev.offsetLocation(0, mScrollOffset[1]);
                mNestedYOffset += mScrollOffset[1];
            }
            ...
            if (mIsBeingDragged) {
                ...//应用变化
				//计算消费掉的距离和未消费的距离
                final int scrolledDeltaY = getScrollY() - oldY;
                final int unconsumedY = deltaY - scrolledDeltaY;
				//派发给parent内部滚动事件,使parent可以有机会处理剩余的未消费距离
                if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                    mLastMotionY -= mScrollOffset[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }...
            }
            break;
        case MotionEvent.ACTION_UP:
			...
            flingWithNestedDispatch(-initialVelocity);//是否触发fling事件以及如何派发fling的统一方法
            endDrag();//endDrag()内部尝试停止内部嵌套滚动stopNestedScroll()
            break;
        case MotionEvent.ACTION_CANCEL:
            ...
            endDrag();//endDrag()内部尝试停止内部嵌套滚动stopNestedScroll()
            break;
        ...
    }
    return true;
}

//2.处理fling事件的统一方法
private void flingWithNestedDispatch(int velocityY) {
    final int scrollY = getScrollY();
    final boolean canFling = (scrollY > 0 || velocityY > 0)
            && (scrollY < getScrollRange() || velocityY < 0);//自身是否可以滑动
    if (!dispatchNestedPreFling(0, velocityY)) {//向parent派发预滑动事件,给parent一个处理机会,看parent是否会处理fling事件
		//parent没有处理的话,再去派发fling事件,自己也许处理也许不处理,也通知给parent,让parent有机会根据child是否消费而决定如何处理
        dispatchNestedFling(0, velocityY, canFling);
        if (canFling) {//执行自己的fling
            fling(velocityY);
        }
    }
}

上面的onTouchEvent的处理,展示了NestedScrollView里面是如何在不同的时机调用内部嵌套滚动的方法(相应接口的方法),开始整个流程,使事件从源头处,让处理者和parent都有机会去处理一次触摸事件

上面所说都是作为Child角度来说的,那么再来看看NestedScrollView作为Parent,相应的嵌套滚动方法是做了什么

@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);//调用helper做基础处理
    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);//作为Child也会去通知parent进行内部滚动
}

@Override
public void onStopNestedScroll(View target) {
    mParentHelper.onStopNestedScroll(target);//调用helper做基础处理
    stopNestedScroll();//作为Child也会去通知parent进行停止内部滚动
}

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed) {
    final int oldScrollY = getScrollY();
    scrollBy(0, dyUnconsumed);//自己应用Child未消费的距离
    final int myConsumed = getScrollY() - oldScrollY;
    final int myUnconsumed = dyUnconsumed - myConsumed;
    dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);//作为Child将剩余距离派发给Parent
}

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    dispatchNestedPreScroll(dx, dy, consumed, null);//自己并不主动消耗,只是作为Child派发给Parent
}

@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
    if (!consumed) {//如果target没有消费掉
        flingWithNestedDispatch((int) velocityY);//则交由自己处理
        return true;//自己处理
    }
    return false;//自己未处理
}

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    return dispatchNestedPreFling(velocityX, velocityY);//自己不会主动消费fling,直接派发给Parent
}

由代码可知,NestedScrollView作为Parent,不会主动消费距离,但是会消费掉Child未消费的距离

(3)综上所述,NestedScrollView既支持内部嵌套滚动,也支持可以内部嵌套滚动的View作为child,那么我们是不是可以实现这种效果:两个(甚至多个)NestedScrollView套在一起,滑动内部的NestedScrollView,内部的NestedScrollView会正常滚动,滚动结束后,外部的NestedScrollView会继续滚动?

答案是可以的,这就是内部嵌套滚动要解决的问题,包括其他实现了这些功能的控件,比如NestedScrollView+RecyclerView,SwipeRefreshLayout+RecyclerView,CoordinatorLayout等等都可以的

下面我们通过看一个比较简单使用的组合来看下具体实现流程,NestedScrollView+RecyclerView

四.NestedScrollView+RecyclerView解析

(1)xml:一个NestedScrollView里面有一个RecyclerView,上下还有两个Button用作可滚动范围

<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

       <android.support.v7.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    LinearLayout>

android.support.v4.widget.NestedScrollView>

(2)DOWN事件触摸RecyclerView

//1.NestedScrollView不会拦截DOWN事件,交由child(RecyclerView)处理
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ...
    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
    		...
    		mIsBeingDragged = !mScroller.isFinished();
    		break;
		}
        ...
    }
    return mIsBeingDragged;
}
 
//2.RecyclerView尝试开启内部嵌套滚动
public boolean onInterceptTouchEvent(MotionEvent e) {
    ...
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            ...
            if (mScrollState == SCROLL_STATE_SETTLING) {
                getParent().requestDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);//交由自己onTouchEvent处理事件
            }
			...
            startNestedScroll(nestedScrollAxis);//尝试开启内部嵌套滚动
            break;

        ...
    }
    return mScrollState == SCROLL_STATE_DRAGGING;
}
 
//3.找到(ChildHelper)NestedScrollingParent(NestedScrollView)通知其开启内部嵌套滚动
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);//记录内部滚动信息(ParentHelper)
    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}

至此,RecyclerView已经处理了DOWN事件,NestedScrollView也记录了内部嵌套滚动的开始

(3)MOVE事件滑动RecyclerView

//1.NestedScrollView不会拦截,直接交由RecyclerView继续处理
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ...
    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop
                    && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {//记录过内部嵌套滚动的开始后,getNestedScrollAxes为VERTICAL了,就不会拦截了
                mIsBeingDragged = true;
                ...
            }
            break;
        }
        ...
    }
    return mIsBeingDragged;
}
 
//2.RecyclerView派发和处理滚动事件
public boolean onTouchEvent(MotionEvent e) {
    ...
    switch (action) {
        ...
        case MotionEvent.ACTION_MOVE: {
            ...
			//计算消费距离
            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
			//派发预滚动事件给NestedParent
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
				//如若parent消费部分距离,则此view少消费相应距离
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // Updated the nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }
            ...
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
				//执行滚动事件,并派发内部嵌套滚动事件给parent
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        } break;
		...
    }
	...
    return true;
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
    ...
	//派发内部滚动事件,告知parent消费的距离和未消费的距离
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
        // Update the last touch co-ords, taking any scroll offset into account
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        if (ev != null) {
            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        }
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    }...
    return consumedX != 0 || consumedY != 0;
}
 
//3.NestedScrollView处理未消费距离
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed) {
    final int oldScrollY = getScrollY();
	//消费未消费的距离
    scrollBy(0, dyUnconsumed);
    final int myConsumed = getScrollY() - oldScrollY;
    final int myUnconsumed = dyUnconsumed - myConsumed;
	//将剩余距离继续派发通知其parent
    dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
	//预滚动事件不做处理,继续往上派发(NestedScrollView本身也是NestedChild)
    dispatchNestedPreScroll(dx, dy, consumed, null);
}

(4)UP事件处理Fling或者stop事件类似,就不做赘述

以上就是NestedScrollView+RecyclerView的嵌套滚动使用,效果就是可以在NestedScrollView里正常滑动RecyclerView,并且在RecyclerView滑动到边界后继续正常滑动NestedScrollView

除此之外,还有很多support包提供的新控件实现了内部嵌套滚动的逻辑,其中最为复杂且实用的是CoordinatorLayout,其通过对各个Child分发NestedScroll相关事件,将各个child的滚动可以依赖起来,用以实现复杂的组合控件滚动效果。

你可能感兴趣的:(原理解析,android相关)