dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
这种分发机制有一个漏洞:
如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。
事件分发是这样的:子View首先得到事件处理权,处理过程中,父View可以对其拦截,但是拦截了以后就无法再还给子View(本次手势内)。 NestedScrolling机制是这样的:内部View在滚动的时候,首先将dx,dy交给NestedScrollingParent,NestedScrollingParent可对其进行部分消耗,剩余的部分还给内部View。NestedScrolling机制主要和NestedScrollingParent有关,看源码:
public class NestedScrollView extends FrameLayout implements NestedScrollingParent, NestedScrollingChild, ScrollingView {NestedScrolling实现了NestedScrollingParent,NestedScrollingChild,ScrollingView 接口。 那么NestedScrollingParent,NestedScrollingChild是什么?
NestedScrollingParent
public interface NestedScrollingParent { // 参数child:ViewParent包含触发嵌套滚动的view的对象 // 参数target:触发嵌套滚动的view (在这里如果不涉及多层嵌套的话,child和target)是相同的 // 参数nestedScrollAxes:就是嵌套滚动的滚动方向了. public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); public void onStopNestedScroll(View target); // 参数target:同上 // 参数dxConsumed:表示target已经消费的x方向的距离 // 参数dyConsumed:表示target已经消费的x方向的距离 // 参数dxUnconsumed:表示x方向剩下的滑动距离 // 参数dyUnconsumed:表示y方向剩下的滑动距离 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed); // 参数dx:表示target本次滚动产生的x方向的滚动总距离 // 参数dy:表示target本次滚动产生的y方向的滚动总距离 // 参数consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离. public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); public boolean onNestedPreFling(View target, float velocityX, float velocityY); public int getNestedScrollAxes(); }1,onStartNestedScroll.当子view的调用NestedScrollingChild的方法startNestedScroll时,会调用该方法.
2,onNestedScrollAccepted.如果onStartNestedScroll方法返回的是true的话,那么紧接着就会调用该方法.它是让嵌套滚动在开始滚动之前,让布局容器(viewGroup)或者它的父类执行一些配置的初始化的.下面是原文: (It offers an opportunity for the view and its superclasses to perform initial configuration for the nested scroll.)
3,onStopNestedScroll停止滚动了,当子view调用stopNestedScroll时会调用该方法.
4,onNestedScroll,当子view调用dispatchNestedScroll方法时,会调用该方法.
5,onNestedPreScroll,当子view调用dispatchNestedPreScroll方法是,会调用该方法.
6,dispatchNestedFling和dispatchNestedPreFling对应的就是滑动了.
onStartNestedScroll该方法,一定要按照自己的需求返回true,该方法决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数;假设你只涉及到纵向滑动,这里可以根据nestedScrollAxes这个参数, 进行纵向判断。 onNestedPreScroll该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数 consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2 onNestedFling你可以捕获对内部View的fling事件,如果return true则表示拦截掉内部View的事件NestedScrollingChild
public interface NestedScrollingChild { // 参数enabled:true表示view使用嵌套滚动,false表示禁用. public void setNestedScrollingEnabled(boolean enabled); public boolean isNestedScrollingEnabled(); // 参数axes:表示滚动的方向如:ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向滚动)和 // ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向滚动) // 返回值:true表示本次滚动支持嵌套滚动,false不支持 public boolean startNestedScroll(int axes); public void stopNestedScroll(); public boolean hasNestedScrollingParent(); // 参数dxConsumed: 表示view消费了x方向的距离长度 // 参数dyConsumed: 表示view消费了y方向的距离长度 // 参数dxUnconsumed: 表示滚动产生的x滚动距离还剩下多少没有消费 // 参数dyUnconsumed: 表示滚动产生的y滚动距离还剩下多少没有消费 // 参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少 public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); // 参数dx: 表示view本次x方向的滚动的总距离长度 // 参数dy: 表示view本次y方向的滚动的总距离长度 // 参数consumed: 表示父布局消费的距离,consumed[0]表示x方向,consumed[1]表示y方向 // 参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少 public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); // 这个是滑动的就不详细分析了 public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); public boolean dispatchNestedPreFling(float velocityX, float velocityY); }1,setNestedScrollingEnabled 实现该结构的View要调用setNestedScrollingEnabled(true)才可以使用嵌套滚动.
2,isNestedScrollingEnabled判断当前view能否使用嵌套滚动.
3,startNestedScroll和stopNestedScroll.是配对使用的.startNestedScroll表示view开始滚动了,一般是在ACTION_DOWN中调用,如果返回true则表示父布局支持嵌套滚动.在事件结束比如ACTION_UP或者ACTION_CANCLE中调用stopNestedScroll,告诉父布局滚动结束.
4,dispatchNestedScroll,把view消费滚动距离之后,把剩下的滑动距离再次传给父布局.
5,dispatchNestedPreScroll,在view消费滚动距离之前把总得滑动距离传给父布局.
6,dispatchNestedFling和dispatchNestedPreFling就是view传递滑动的信息给父布局的.
首先来说 NestedScrollingChild。如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口。在此 View 中,包含一个 NestedScrollingChildHelper 辅助类。NestedScrollingChild 接口的实现,基本上就是调用本 Helper 类的对应的函数即可,因为 Helper 类中已经实现好了 Child 和 Parent 交互的逻辑。原来的 View 的处理 Touch 事件,并实现滑动的逻辑大体上不需要改变。
需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用 startNestedScroll()。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用dispatchNestedPreScroll()。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用 dispatchNestedScroll()。
#NestedScrollingChildHelper public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { 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; }Helper 类中已经实现好了 Child 和 Parent 交互的逻辑 可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。 ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。
那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:
一、startNestedScroll 首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent
二、dispatchNestedPreScroll 在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。 如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。 这个函数一般在子view处理scroll前调用。
三、dispatchNestedScroll 向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。 如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。 这个函数一般在子view处理scroll后调用。
四、stopNestedScroll 结束整个流程。
整个对应流程是这样
子view startNestedScroll 父view onStartNestedScroll、onNestedScrollAccepted 子view dispatchNestedPreScroll 父view onNestedPreScroll 子view dispatchNestedScroll 父view onNestedScroll 子view stopNestedScroll 父view onStopNestedScroll 一般是子view发起调用,父view接受回调。
我们最需要关注的是dispatchNestedPreScroll中的consumed参数。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。
fling中会回调onNestedPreFling和onNestedFling方法。
调用的方法名都是dispatchNestedXXX方法,实际内部都是通过NestedScrollingChildHelper实现的。
所以如果你需要实现和NestedScrollingParent协作的内部View,记得实现NestedScrollingChild,然后内部借助NestedScrollingChildHelper这个辅助类,核心的方法都封装好了,你只需要在恰当的实际去传入参数调用方法即可。
作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现 NestedScrollingParent,这个接口方法和 NestedScrollingChild 大致有一一对应的关系。同样,也有一个 NestedScrollingParentHelper辅助类来默默的帮助你实现和 Child 交互的逻辑。滑动动作是 Child 主动发起,Parent 就收滑动回调并作出响应。
从上面的 Child 分析可知,滑动开始的调用 startNestedScroll(),Parent 收到 onStartNestedScroll() 回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted()。
每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll(),这就回调到 Parent 的 onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。
Child 滑动以后,会调用 onNestedScroll(),回调到 Parent 的 onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。
最后,滑动结束,调用 onStopNestedScroll() 表示本次处理结束。
其实,除了上面的 Scroll 相关的调用和回调,还有 Fling 相关的调用和回调,处理逻辑基本一致。
eg:子View实现 NestedScrollingChild接口,重写onTouchEvent,
public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 按下事件调用startNestedScroll startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); break; case MotionEvent.ACTION_MOVE: // 移动事件调用dispatchNestedPreScroll dispatchNestedPreScroll(0,20,mConsumed,mOffset); // 输出一下偏移 Log.d(TAG, "offset--x:" + mOffset[0] + ",offset--y:" + mOffset[1]); dispatchNestedScroll(50,50,50,50,mOffset); break; case MotionEvent.ACTION_UP: // 弹起事件调用startNestedScroll stopNestedScroll(); break; ...... // NestedScrollingChild @Override public void setNestedScrollingEnabled(boolean enabled) { mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); } ......eg:父View实现 NestedScrollingParent接口
// NestedScrollingParent @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); } @Override public void onStopNestedScroll(View target) { stopNestedScroll(); } //一般父View 需要重写的方法,实现不同交互 onNestedScroll @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { final int myConsumed = moveBy(dyUnconsumed); final int myUnconsumed = dyUnconsumed - myConsumed; dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); } //onNestedPreScroll @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (dy > 0 && mHeaderController.canScrollUp()) { final int delta = moveBy(dy); consumed[0] = 0; consumed[1] = delta; //dispatchNestedScroll(0, myConsumed, 0, consumed[1], null); } } //onNestedFling @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { if (!consumed) { flingWithNestedDispatch((int) velocityY); return true; } return false; } private boolean flingWithNestedDispatch(int velocityY) { final boolean canFling = (mHeaderController.canScrollUp() && velocityY > 0) || (mHeaderController.canScrollDown() && velocityY < 0); if (!dispatchNestedPreFling(0, velocityY)) { dispatchNestedFling(0, velocityY, canFling); if (canFling) { fling(velocityY); } } return canFling; } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return flingWithNestedDispatch((int) velocityY); } @Override public int getNestedScrollAxes() { return mParentHelper.getNestedScrollAxes(); }调用过程如下: ————–子View开始滚动 startNestedScroll—————— —-父布局onStartNestedScroll—————- —-父布局onNestedScrollAccepted————— ———–子View把总的滚动距离传给父布局 dispatchNestedPreScroll——– —-父布局onNestedPreScroll—————- —offset–x:0,offset–y:20 ———–子View把剩余的滚动距离传给父布局 dispatchNestedScroll——- —-父布局onNestedScroll—————- ———–子View停止滚动 stopNestedScroll————— —-父布局onStopNestedScroll—————-