NestedScrolling常用在嵌套滚动的场景,比较常见的是使用CoordinateLayout实现比较炫酷的联合滚动效果,其内部也是借助了NestedScrollingChild和NestedScrollingParent这套机制。
NestedScrollingChild
NestedScrollingParent
startNestedScroll
onStartNestedScroll
dispatchNestedPreScroll
onNestedPreScroll
dispatchNestedScroll
onNestedScroll
dispatchNestedPreFling
onNestedPreFling
dispatchNestedFling
onNestedFling
stopNestedScroll
onStopNestedScroll
NestedScrollingChild接口中的方法均为主动方法,需要我们在实现类中主动调用,而NestedScrollingParent的方法基本都是回调方法。这也是NestedScrolling机制的一个体现,子View作为NestedScrolling事件传递的主动方,父View作为接收方。父View决定是移动子View控件,还是把移动偏移量交给子View,让其滚动内容。
子View的事件处理过程都在onTouchEvent()。
子View在action down中执行startNestedScroll()启动联动流程,并设置滚动的方向;
父View在onStartNestedScroll()中根据传来的方向,决定是否联动,返回结果;
子View在action move中计算移动偏移量,执行dispatchNestedPreScroll(),将偏移情况告诉父View;
父View在onNestedPreScroll()中接收子View传来的偏移量,计算需要消耗的偏移量,即移动子View的距离;
子View计算父View消费后剩下的偏移量,在这个余量基础上计算子View还能消费多少,并把消费情况通过dispatchNestedScroll()告诉父View;
父View在onNestedScroll()中根据偏移量进行相应处理;
事件结束,子View在action up中执行stopNestedScroll()结束联动流程,父View的onStopNestedScroll()得到响应,事件传递完成。
上述联动过程的传递,通过NestedScrollingChildHelper和NestedScrollingParentHelper这爷俩就可简单实现,里面封装了很多实现细节,让我们开发过程更高效。
fling过程和上述相同,可以通过示例代码了解。
下面通过代码讲一讲上述流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
class ChildView implements NestedScrollingChild { @Override public boolean onTouchEvent(MotionEvent event) { boolean result = false; MotionEvent trackedEvent = MotionEvent.obtain(event); final int action = MotionEventCompat.getActionMasked(event); if (action == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } int y = (int) event.getY(); event.offsetLocation(0, mNestedYOffset); switch (action) { case MotionEvent.ACTION_DOWN: mLastMotionY = y; // 1.开始嵌套滚动,并确定滚动方向 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); result = super.onTouchEvent(event); break; case MotionEvent.ACTION_MOVE: int deltaY = mLastMotionY - y; // 3.子View计算偏移量传给父View if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { // mScrollConsumed 保存被parent消费的尺寸 deltaY -= mScrollConsumed[1]; trackedEvent.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } mLastMotionY = y - mScrollOffset[1]; // 5.根据父View消费后的偏移余量,计算自己还能用多少,还能再剩下多少给父View继续使用 int oldY = getScrollY(); int newScrollY = Math.max(0, oldY + deltaY); int dyConsumed = newScrollY - oldY; int dyUnconsumed = deltaY - dyConsumed; if (dispatchNestedScroll(0, dyConsumed, 0, dyUnconsumed, mScrollOffset)) { mLastMotionY -= mScrollOffset[1]; trackedEvent.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } result = super.onTouchEvent(trackedEvent); trackedEvent.recycle(); break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // 7.结束流程 stopNestedScroll(); result = super.onTouchEvent(event); break; } return result; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
class ParentView implements NestedScrollParent { // 2.父View根据传来的方向,决定是否联动 @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } // 4.计算需要消耗的偏移量,移动子View @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (dy > 0) { int dyCanConsumed = mTargetCurOffset - mTargetEndOffset; if (dy >= dyCanConsumed) { consumed[1] = dyCanConsumed; moveTo(mTargetEndOffset); } else { consumed[1] = dy; moveBy(-dy); } } } // 6.父View根据子View的计算结果,做相关处理 @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if (dyUnconsumed < 0) { moveBy(-dyUnconsumed); } } }
相关问题:
1.子View主动触发一个事件,父View的对应方法就能响应,那父子View的联动关系是如何确定的? 看下NestScrollingChildHelper#startNestedScroll()的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; // 向上遍历找parent,直到找到实现NestScrollParentHelper.onStartNestScroll()返回true的parent // 注意,ViewGroup也实现了NestScrollParentHelper,但是onStartNestScroll()返回false 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; }
相关知识点:
1.View#canScrollVertically()
判断View在垂直方向是否可以上下滚动。 其中: View#canScrollVertically(1),return true ——可以向上滚动,反之,不可以 View#canScrollVertically(-1),return true ——可以向下滚动,反之,不可以
2.View#computeVerticalScrollOffset()
判断View的内容在垂直方向上滚动的距离。返回值:0~***,均为正值。 比如WebView,起始状态为0,内容向上滚动后,为某个正值。
3.View#computeVerticalScrollRange()
View内容的总高度。
4.View#computeVerticalScrollExtent() View在屏幕区域内显示的高度。
近期评论