二、 嵌套滑动机制(NestedScroll)
上面看了Col的使用和自定义Behavior,下面我们来说一下这个效果实现原理,这里加入NestedScroll这个概念,是因为Col实质也是通过NestedScroll实现的各种效果。
在Android嵌套滑动机制出现之前,Android UI的滑动的组件有ListView和ScrollView,ViewPager就会有很多需求是要:ListView和ScrollView自己嵌套,或者互相嵌套。这时经常会遇到一些问题:
- 点击事件被屏蔽
- view的滑动不流畅
- view的滑动条件不符合设计
图片1 滑动图片
以上面图片滑动的效果为例,看到parent滑动到一定距离后,子view开始滑动,中间连贯不间断。我们分别从传统事件分发机制和NestedScroll机制对比去看怎么处理:
按照传统的事件分发机制:
开始我们滑动的是子view的内容区域,移动却是外部的Parent,这种实现一定是外部的Parent拦截了内部的子view事件,那么这次Down-->Move–>Move–>Up的所有事件都会交给Parent处理。
不了解事件分发机制的,可以参考:http://www.cnblogs.com/linjzong/p/4191891.html
NestedScroll机制:
子view开始滚动时,将dx,dy交给NestedScrollingParent,NestedScrollingParent进行部分消耗,剩余的还给内部的子view。
那么我分三步详细看下NestedScroll机制怎么实现的上面效果:
1 NestedScrollingParent都提供了什么方法?
2 parent如何实现 NestedScrollingParent这个接口?
3 子view如何拦截了事件并回调NestedScrollingParent的各种方法?
1.NestedScrollingParent接口
public interface NestedScrollingParent {
/**
* 有嵌套滑动到来了,问下该parent是否接受嵌套滑动
*
* @param child parent的其它子view
* @param target parent中滑动的子View
* @param nestedScrollAxes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
* @return 是否接受该嵌套滑动
*/
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
...
/**
* 子view在未滑动之前传过来的准备滑动情况
*
* @param dx 水平方向子view想要滑动的距离
* @param dy 垂直方向子view想要滑动的距离
* @param consumed 告诉子view,当前父view消耗的距离:consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离
*/
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
/**
* 子view在fling之后传过来的fling情况
* @param velocityX 水平方向速度
* @param velocityY 垂直方向速度
* @param consumed 子view是否fling
* @return true parent是否消耗了fling
*/
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
...
}
NestedScrollingParent一共提供了8个方法,这里我们只说上面3个:
onStartNestedScroll方法:
改方法决定Parent是否能接受到其内部子view(子view可以不是直接子view)滑动时的参数,假如只涉及到垂直方向滑动的嵌套,nestedScrollAxes指定垂直方向。
onNestedPreScroll方法:
传入子view移动的dx,dy,如果parent需要消耗部分dx,dy,就通过参数consumed进行指定,例如,parent消耗一半的dy,就可以写consumed[1]= dy/2。
onNestedFling方法:
传入子view的fling事件,return true:parent拦截子view的fling,false:不拦截
2. Parent实现NestedScrollingParent
public class ParentLauyout extends LinearLayout implements NestedScrollingParent
{
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
{
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
{
boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight;
boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);
if (hiddenTop || showTop)
{
scrollBy(0, dy);
consumed[1] = dy;
}
}
...
}
onStartNestedScroll方法:
在什么方向上我们选择接受嵌套滑动,和子view配合处理滑动。
onNestedPreScroll方法:
我们判断,如果是上滑且顶部控件未完全隐藏,则消耗掉dy,即consumed[1]=dy;如果是下滑且内部View已经无法继续下拉,则消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去执行scrollBy,实际上就是我们的ParentLauyout滑动。
3. 子view回调的NestedScrollingParent的各种方法
知道了这些,我们去看下子view如何拦截的事件:(这里以NestedscrollView为例)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
mLastMotionY = y;
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
...
return mIsBeingDragged;
}
在ACTION_DOWN时,调用startNestedScroll
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
调用的是NestedScrollingChildHelper的startNestedScroll:
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
//循环向上寻找parent中在onStartNestedScroll方法中返回true(嵌套滑动到来时,该parent接受嵌套滑动)
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
I }
p = p.getParent();
}
}
return false;
}
从子view循环向上层找parent中是不是有实现了NestedScrollingParent接口的,找到这个与之合作的父父…父view的时候,并且通过该parent对应的onStartNestedScroll方法返回的true(接受嵌套滚动)或者 false(不接受嵌套滚动)。
这样子view通过回调父view的NestedScrollingParent方法可以获知父view情况~
以上总结:
-
parent 实现 NestedScrollingParent接口, 子view 实现 NestedScrollingChild接口,这两个接口 有两个辅助类NestedScrollingParentHelper(NestedScrollingChildHelper)来帮助实现parent和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() 表示本次处理结束。
详细了解嵌套滑动机制中的NestedScrollingParent和NestedScrollingChild参考资料:http://blog.csdn.net/x87648510/article/details/51882040