CoordinatorLayout 中的触摸滑动事件是如何传递给子控件的,子控件是如何处理的,又是如何反馈给 CoordinatorLayout 的?我们以 RecyclerView 作为 CoordinatorLayout 的子控件为例子,看看源码,是如何调用的。
触摸滑动,说白了,都是要看 dispatchTouchEvent() 、 onInterceptTouchEvent() 、 onTouchEvent() 这三个方法的逻辑,RecyclerView 中有重写后两个方法,我们就来看看 ACTION_DOWN 逻辑
public boolean onInterceptTouchEvent(MotionEvent e) {
...
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mIgnoreMotionEventTillDown) {
mIgnoreMotionEventTillDown = false;
}
mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
}
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis);
break;
...
}
return mScrollState == SCROLL_STATE_DRAGGING;
}
public boolean startNestedScroll(int axes) {
return mScrollingChildHelper.startNestedScroll(axes);
}
private void setScrollState(int state) {
if (state == mScrollState) {
return;
}
if (DEBUG) {
Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
new Exception());
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
stopScrollersInternal();
}
dispatchOnScrollStateChanged(state);
}
ACTION_DOWN 时,判断如果是滑动状态,请求父容器不拦截,然后把状态设置为拖拽状态,停止RecyclerView 的滑动,触发状态改变的回调;下面是关键,根据 canScrollHorizontally 和 canScrollVertically 属性,来给 nestedScrollAxis 赋值,标识是否可以横向或竖向滑动,然后调用了 startNestedScroll() 方法,它里面又调用了 NestedScrollingChildHelper的方法。看名字,它是一个滑动的辅助类,它是在 RecyclerView 的构造方法中创建的,
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
...
mScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
public void setNestedScrollingEnabled(boolean enabled) {
mScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
并设置了允许滑动的标识,看看 NestedScrollingChildHelper 的代码
public NestedScrollingChildHelper(View view) {
mView = view;
}
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
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;
}
重点关注 startNestedScroll() 方法,如果已经获取到了可以滑动的父容器 mNestedScrollingParent,则 return true; 如果可以滑动,则 p = mView.getParent(),这一步是获取父容器,然后重点来了,看看 while 循环中,如果满足if中的条件,则把当前 p 赋值给 mNestedScrollingParent,我们看看主要的两个方法 onStartNestedScroll() 和 onNestedScrollAccepted(),
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
int nestedScrollAxes) {
IMPL.onNestedScrollAccepted(parent, child, target, nestedScrollAxes);
}
这里是做了版本判断,我们找个比较低的版本,看看具体是怎么判断的
static class ViewParentCompatStubImpl implements ViewParentCompatImpl {
@Override
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
return false;
}
@Override
public void onNestedScrollAccepted(ViewParent parent, View child, View target,
int nestedScrollAxes) {
if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
nestedScrollAxes);
}
}
...
}
看到这就明白了,还是调用父容器的 onStartNestedScroll() 方法,我们看看 CoordinatorLayout 的方法
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
原来如此,按下 RecyclerView,最终会调到 CoordinatorLayout 中的 onStartNestedScroll() 方法,此方法中会再次把事件传递给 Behavior,这样就形成了一串调用。再看看另外一个方法,调用了 CoordinatorLayout 中的 onNestedScrollAccepted() 方法
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
mNestedScrollingDirectChild = child;
mNestedScrollingTarget = target;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
}
}
}
和前一个方法一样,也是会调用 Behavior 中的方法。看看 RecyclerView 中 onTouchEvent() 方法
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
case MotionEvent.ACTION_DOWN: {
...
startNestedScroll(nestedScrollAxis);
}
break;
case MotionEvent.ACTION_MOVE: {
...
final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
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) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0, vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
}
break;
...
}
...
return true;
}
ACTION_DOWN 中方法和之前的一样,就不分析了。onInterceptTouchEvent() 中 ACTION_MOVE 没什么重要的,重点看 onTouchEvent() 中的 ACTION_MOVE,这个才是精髓所在。老样子,看看 dispatchNestedPreScroll() 方法
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
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);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
重点关注 int[] consumed 这个形参,是从 RecyclerView 中传递进来的数组,记录的是消费的值。dx 和 dy 是偏移量,这里判断的是如果有偏移量,会把 consumed 输入内容清零,然后调用 ViewParentCompat 的 onNestedPreScroll() 方法,返回的值是判断 consumed 中内容是否为零,我们看看调用的方法
@Override
public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed) {
if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
果然,继续看 CoordinatorLayout 中的方法
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
dispatchOnDependentViewChanged(true);
}
}
最终还是回到这里,我们注意了,int[] consumed 这里所操作的地方,在 Behavior 中传入的 mTempIntPair 数组,比较一番值后,最终决定是否赋值给 consumed,重新回到上一步,dispatchNestedPreScroll() 方法中最终判断的是 consumed[0] != 0 || consumed[1] != 0,如果此时返回为true,再看看 RecyclerView 中,onTouchEvent() 中,dx 和 dy 会减去 consumed 数组中的值;如果 consumed 中都为0,说明父容器没有消费,那么 dx 和 dy 值不变,继续往下看。 if (mScrollState != SCROLL_STATE_DRAGGING) 这个判断只会执行一次,第一次的话,防止位移量偏大,会对 dx 和 dy 做一些修正,然后就把 mScrollState 值给改变了,下次就不会走进if判断了。最后看看 scrollByInternal() 方法,这个是自己滑动及调用了 dispatchNestedScroll() 方法,这个最终会调用 CoordinatorLayout 的 onNestedScroll() 方法,这个对应着已经消费的方法。
CoordinatorLayout 中 onNestedPreScroll() 这个方法中的 int[] consumed 数组,以及传递给 Behavior 中的 onNestedPreScroll() 方法中的数组,它告知我们子view将要滑动的距离,交割给 CoordinatorLayout,如果 CoordinatorLayout 或 Behavior 不消费,或者没消费完,还有剩余,这时候, RecyclerView 才会继续消费,我们自定义控件容器或自定义 Behavior 时,可以通过 onNestedPreScroll() 方法中 consumed 的赋值,来控制自身消费的值,同时做出相应的操作。
ACTION_UP 中,手指抬起时,计算手指的速率,然后是 fling((int) xvel, (int) yvel)) 方法,这个就是控件自身滑动的方法,和上面一样,最终会回调到 CoordinatorLayout 中,这里就不多分析了,剩余的两个方法也一样。最终记住,
自定义Behavior可以选择重写以下的几个方法有:
● onStartNestedScroll():嵌套滑动开始(ACTION_DOWN),确定Behavior是否要监听此次事件
● onStopNestedScroll():嵌套滑动结束(ACTION_UP或ACTION_CANCEL)
● onNestedPreScroll():嵌套滑动进行中,要监听的子 View将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制)
● onNestedScroll():嵌套滑动进行中,要监听的子 View的滑动事件已经被消费
● onNestedPreFling():要监听的子View即将快速滑动
● onNestedFling():要监听的子 View在快速滑动中