前言
CoordinatorLayout已出来好久了,该知识点是一个android工程师需必会的,其实研读源码还能够开拓思维,大家有时间都看看源码。
以最新v4包25.1.1版本忠的NestedScrollView为例说明。
NestedScroll包含两部分:NestedScrollingParent和NestedScrollingChild。
源码解读
习惯性的,我们从响应触摸事件的子视图说起。
对于子视图,实现的是NestedScrollingChild接口。触摸事件按下,调用子视图的onInterceptTouchEvent方法(先调用的是父视图的onInterceptTouchEvent方法,此处不关注)。
子视图的MotionEvent.ACTION_DOWN
在MotionEvent.ACTION_DOWN代码块下可看到这样一句:startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
该方法:
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
mChildHelper是一个NestedScrollingChildHelper类,是辅助处理滚动事件的。相应的,NestedScrollingParent的辅助类为NestedScrollingParentHelper。具体的操作都在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赋值,
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);//接受嵌套滚动的父视图的相应回调(onNestedScrollAccepted)做一些初始化操作
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
关键代码:ViewParentCompat.onStartNestedScroll(p, child, mView, axes),对于Lollipop以前的:
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
return false;
}
会判断父视图是否实现了NestedScrollingParent。而对于Lollipop之后的:
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
"method onStartNestedScroll", e);
return false;
}
}
其调用的是父视图重写的onStartNestedScroll方法。
对于Lollipop之后的版本,ViewParent中定义了与NestedScrollingParent一样的方法,而NestedScrollingChild中的方法对应View中的方法。
从上面的过程来看:
结论一,子视图接收触摸事件,然后回调父视图的对应NestedScrollingParent中的方法。
如果父视图需要嵌套滚动,则父视图的onStartNestedScroll方法返回true。上面这一过程还回调了父视图的onNestedScrollAccepted方法,一般在该方法中做初始配置。
子视图的MotionEvent.ACTION_MOVE
在onTouchEvent的MotionEvent.ACTION_MOVE代码块下找到关键代码:
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
具体操作都是在NestedScrollingChildHelper中,具体代码:
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;//供父视图使用的消费数组重置
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);//回调父视图的onNestedPreScroll方法,回调方法中对consumed数组做操作
if (offsetInWindow != null) {//因为父视图的onNestedPreScroll方法中做了视图滚动操作(scrollTo或者其他使视图变动的操作),计算偏移量
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//父视图消费了就返回true。
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
如果父视图消费了,再看下上上部分的关键代码:
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
这里会将滑动偏移量deltaY减去父视图消费的部分。mNestedYOffset 加上滚动的偏移量,而在onTouchEvent刚开始时:
MotionEvent vtev = MotionEvent.obtain(ev);//复制一份触摸事件
final int actionMasked = MotionEventCompat.getActionMasked(ev);
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);//设置嵌套滚动的y偏移量
往后看
//overScrollByCompat方法中最终会调用scrollTo方法来滚动
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,0, true) &&!hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = getScrollY() - oldY;//滚动之后的scrollY减去滚动之前的scrollY得到滚动偏移量
final int unconsumedY = deltaY - scrolledDeltaY;//得到没消耗的y值
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
关于该方法的最后一部分,在NestedScrollView嵌套NestedScrollView的情况下,如果子视图在子视图的滚动范围内可滚动,则unconsumedY 为0,当子视图滚动到子视图的边界继续滚动时,scrolledDeltaY为0,unconsumedY 为deltaY,则回调父视图的onNestedScroll方法时父视图做滚动,这样就实现父视图与子视图的无缝滚动。
对于fly方面就不做描述了,其在MotionEvent.ACTION_UP下的代码块里,相信有了上面的分析过程,很轻松就能看懂。
对于父视图而言,如果想继续向上提供触摸事件,可用NestedScrollingChild中的方法继续分发事件,如NestedScrollView中的
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
dispatchNestedPreScroll(dx, dy, consumed, null);
}