在开始看源码之前,咱们先来重温一下RecycleView是怎么将触摸事件传到CoordinatorLayout问它消不消耗事件的!根据事件的分发机制是从Activity的(dispatchTouchEvent)事件开始传入给PhoneWindow的DecorView的(dispatchTouchEvent),传入口如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如果DecorView里面有消耗事件的控件存在,则由它消耗接下来的事件,如果没有,最后事件交个Activity的onTouchEvent来消耗,传入口如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
最后通过DecorView的superDispatchTouchEvent进入ViewGroup的dispatchTouchEvent分发事件中开始分发事件
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
那么接下里就开始从View树的根部一级一级的开始传分发事件,如果点击事件是在ViewGroup类型的View的范围内将会触发自身分发事件中去调用onInterceptTouchEvent,如果返回true的话事件将会被当前的ViewGroup消耗,当然子View可以调用requestDisallowInterceptTouchEvent方法让父View不要拦截事件,接下来的事件直接传到它的onTouchEvent方法里面进行处理,如果拦截事件返回false则接着向下传递,最终执行到最小的叶子节点的onTouchEvent方法处理,如果返回true则这个叶子View消耗接下来的事件,如果返回false,则传给它的父View进行onTouchEvent进行处理,以此类推,最终没有View消耗事件,则传到Activity的onTouchEvent处理。
那么接下来看一下RecycleView怎么将事件传递给CoordinatorLayout,因为CoordinatorLayout 包裹RecycleView,所以事件首先传到CoordinatorLayout,CoordinatorLayout没有重写父类ViewGroup的dispatchTouchEvent方法,则事件分发到它的时候走正常的分发事件,那么接下来它会判断onInterTouchEvent是否会拦截
public boolean onInterceptTouchEvent(MotionEvent ev) {
MotionEvent cancelEvent = null;
省略若干行...
执行绑定了behavier的子View的拦截事件
**/
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
省略若干行...
return intercepted;
}
它的拦截事件完全交给它的子View的Behavior行为接口来处理,也就是说CoordinatorLayout是否会消耗掉事件完全由用户自定义的BeHavior来处理,MD风格的组件库自带的两个BeHavior是BottomSheetBehavior和ScrollingViewBehavior,BottomSheetBehavior实现了onInterceptTouchEvent,在生明了BottomSheetBehavior的View,如果它实现了NestedScrollingChild接口的话,或者它有子类实现了这个接口的话并且触摸事件在这个子View内将不会拦截这个事件,也就是说RecycleView直接实现BottomSheetBehavior行为,分发事件将交由RecycleView处理,或者它的父类实现的话,触摸事件只要在RecycleView控件内都分发事件都将进入RecycleView中,换句话说如果实现BottomSheetBehavior行为的不是RecycleView的话,事件将会交于BottomSheetBehavior处理,(这里指的是移动事件的拦截,子View设置点击事件还是可以正常触发的);ScrollingViewBehavior并未实现拦截事件所以最终滑动事件还是交给RecycleView来处理。
那么只要是NestedScrollingChild接口的对象,滑动事件首先都是经过自身,也就是说滑动事件是RecycleView-》CoordinatorLayout,怎么到的?这里是通过NestedScrollingParent(现在源码中用NestedScrollingParent2(它继承NestedScrollingParent))接口来回调的,也就是CoordinatorLayout实现了这个接口,RecycleView在滑动的时候利用这个接口将滑动距离交给CoordinatorLayout,CoordinatorLayout再交给它所有子View的Behavier来处理这个距离,取所有处理距离的最大值返回给RecycleView,RecycleView滑动剩下的距离。NestedScrollingParent和NestedScrollingChild用法,大家请查一下资料这里不再详细讨论。下面列出NestedScrollingParent接口的方法的用法
public interface NestedScrollingParent {
/**在Recycleview拦截事件down当中就调用了(onTouchevent事件的down也会调用)
nestedScrollAxes:1是水平滑动,2是垂直滑动澹(滑动标记)
target是RecycleView,而child是继承NestedScrollingParent接口的直接子View**/
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
onNestedScrollAccepted方法在onStartNestedScroll之后调用,可以做一些初始化的工作
**/
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
拦截事件中MotionEvent.ACTION_UP调用,Recylview拦截事件处理的时候调用,ACTION_CANCEL的时候调用**/
public void onStopNestedScroll(View target);
/**
onNestedScroll:recycleView滑动之后调用的方法
target:recycleView,dxConsumed:RecycleView已经滚动的距离 ,dxUnconsumed:还未滚动的距离**/
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
velocityX,velocityY:速度
target:RecycleView
**/
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
/**
返回滚动类型是垂直还是水平
**/
public int getNestedScrollAxes();
只要是实现了BottomSheetBehavior行为的子View,都可以快速的实现底部抽屉的功能,那么通过上面的分析咱们可以了解到,
如果实现的View是RecycleView或NestedScrollView等实现NestedScrollingChild接口的时候,滑动的View会先将事件交给CoordinatorLayout,CoordinatorLayout再交给BottomSheetBehavior;相反的,实现BottomSheetBehavior的View是普通的View的话,事件的处理直接是CoordinatorLayout通过onInterceptTouchEvent和onTouchEvent交给BottomSheetBehavior,当然Behavior这个类已经生明了这些方法,你要自定义的话,只要实现就好了。那么接下来的重点就是直接看BottomSheetBehavior就好了。
现在先来分析第一条线:RecycleView-》CoordinatorLayout-》BottomSheetBehavior;首先第一个问题为什么生明了BottomSheetBehavior的View会自动跑到底部?CoordinatorLayout在布局子View的时候(在onLayout方法中)会触发Behavior的onLayoutchild方法看子View是否能确定自己的位置,否则CoordinatorLayout帮它确定位置,这里先来了解一下这面的属性
BottomSheetDialog的app:behavior_hideable和app:behavior_skipCollapsed属性都为true,所以说可以滑出屏幕。
接下来看一下onLayoutchild方法
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) {
ViewCompat.setFitsSystemWindows(child, true);
}
//这里的child是实现了BottomSheetBehavior的child
int savedTop = child.getTop();
// First let the parent lay it out
//先调用CoordinatorLayout默认的排版布局
parent.onLayoutChild(child, layoutDirection);
// Offset the bottom sheet
mParentHeight = parent.getHeight();
//计算最小的偏移量CoordinatorLayout的高度减去child的高度》=0
mMinOffset = Math.max(0, mParentHeight - child.getHeight());
//计算最大的偏移量CoordinatorLayout高度减去你设置的属性的高度mPeekHeight
mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);
//mState默认状态是折叠,而BottomSheetDilog的默认状态是隐藏(STATE_HIDDEN)
//如果是展开设置最小偏移量
if (mState == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, mMinOffset);
} else if (mHideable && mState == STATE_HIDDEN) {
//如果是隐藏直接将top设置为CoordinatorLayout的高度
ViewCompat.offsetTopAndBottom(child, mParentHeight);
}
//如果是折叠状态top的值为mParentHeight - mPeekHeight
else if (mState == STATE_COLLAPSED) {
ViewCompat.offsetTopAndBottom(child, mMaxOffset);
} else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
//如果是拖动或滑动惯性不移动
ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
}
//创建ViewDragHelper
if (mViewDragHelper == null) {
mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
}
//将child保存到弱引用中
mViewRef = new WeakReference<>(child);
//将child中的滑动view或自己保存到弱引用mNestedScrollingChildRef
mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
return true;
}
这里介绍一下BottomSheetBehavior的所有状态:
如果不设置BottomSheetBehavior的状态的话,默认是STATE_COLLAPSED状态,那么onLayoutChild方法就会将子View的Top设置为你设置的CoordinatorLayout的高度减去app:behavior_peekHeight的高度,最终我们在手机上看到的View就是只显示了app:behavior_peekHeight高度的View,源码通过ViewCompat.offsetTopAndBottom(child, mParentHeight);实现。
ok,第一个问题解决了,接下来是手指滑动RecycleView先滑动到展开位置,然后再滑动自身的效果是怎么产生的?
首先在RecycleView监听的down的时候先通过onStartNestedScroll回调到CoordinatorLayout的onStartNestedScroll最后传给子View的Behavior的onStartNestedScroll,如下:
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
View directTargetChild, View target, int nestedScrollAxes) {
mLastNestedScrollDy = 0;
mNestedScrolled = false;
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
BottomSheetBehavior的这个方法只要RecycleView的是垂直滚动的的那么就会返回true,如果这个方法不返回true接下来的回调方法就不会执行,如下代码注释处
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
//这里将onStartNestedScroll返回的true设置给LayoutParams做为接下来的判断
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
正常情况下调用完onStartNestedScroll之后,接下来回调的就是onNestedScrollAccepted方法,这个方法可以设置自己需要用到的变量,而BottomSheetBehavior没有实现这个方法,假如触发了垂直滑动的最小距离则会回调BottomSheetBehavior的onNestedPreScroll方法
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx,
int dy, int[] consumed) {
View scrollingChild = mNestedScrollingChildRef.get();
if (target != scrollingChild) {
return;
}
int currentTop = child.getTop();
int newTop = currentTop - dy;
//朝上滑动
if (dy > 0) { // Upward
if (newTop < mMinOffset) {
consumed[1] = currentTop - mMinOffset;
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
setStateInternal(STATE_EXPANDED);
} else {
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
setStateInternal(STATE_DRAGGING);
}
} else if (dy < 0) { // Downward
//朝下滑的时候,先判断邋RecycleView是否滑动到底部
if (!ViewCompat.canScrollVertically(target, -1)) {
if (newTop <= mMaxOffset || mHideable) {
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
setStateInternal(STATE_DRAGGING);
} else {
consumed[1] = currentTop - mMaxOffset;
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
setStateInternal(STATE_COLLAPSED);
}
}
}
dispatchOnSlide(child.getTop());
mLastNestedScrollDy = dy;
mNestedScrolled = true;
}
这个方法的意思就是,如果手指向上滑动的话,如果当前的View的top没有到扩展的状态的话(CoordinatorLayout的高度减去自己的高度的话将消耗掉手指滑动的距离,从而RecycleView将不滑动只改变他的Top值);如果手指朝下滑动并且当前的子View没的top没有到关闭状态则消耗掉手指滑动的距离(看起来RecycleView没有滑动,当时位置改变了);其他的情况则执行RecycleView的滑动,这就是上面说的那个问题的原因了。
在手指抬起来之后RecycleView会判断手速如果速度达到它认为的最低速度的话将会回调onNestedPreFling方法,onNestedPreFling方法返回false才会回调onNestedFling方法,也就是说这两个方法你都可以在里面实现惯性滑动逻辑,BottomSheetBehavior的onNestedPreFling方法如下:
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
float velocityX, float velocityY) {
return target == mNestedScrollingChildRef.get() &&
(mState != STATE_EXPANDED ||
super.onNestedPreFling(coordinatorLayout, child, target,
velocityX, velocityY));
}
/**
这个方法只有在扩展的状态和mNestedScrollingChildRef中的滑动View不为null时返回true,BottomSheetBehavior没有实现onNestedFling方法,也就是说BottomSheetBehavior不处理自然滑动。
当自然滑动也处理完了之后,会调用onStopNestedScroll这个方法,这里需要在这个方法最终确定BottomSheetBehavior状态,代码如下:
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
//如果子View的top值已经是mMinOffset,则直接返回,设置当前状态为展开
if (child.getTop() == mMinOffset) {
setStateInternal(STATE_EXPANDED);
return;
}
//如果没有滑动的View则直接返回,或者view已经被回收了
if (mNestedScrollingChildRef == null || target != mNestedScrollingChildRef.get()
|| !mNestedScrolled) {
return;
}
int top;
int targetState;
//mLastNestedScrollDy最后一次滑动的距离>0向上滑动
if (mLastNestedScrollDy > 0) {
top = mMinOffset;
targetState = STATE_EXPANDED;
}
//只有设置了那两个参数的情况下,状态才设置为隐藏
else if (mHideable && shouldHide(child, getYVelocity())) {
top = mParentHeight;
targetState = STATE_HIDDEN;
}
else if (mLastNestedScrollDy == 0) {
//子View离那边近就设置那种状态
int currentTop = child.getTop();
if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
top = mMinOffset;
targetState = STATE_EXPANDED;
} else {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
} else {
//朝下滑动则设置状态为关闭状态
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
//进行子View的Top偏移
if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
setStateInternal(STATE_SETTLING);
ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
} else {
setStateInternal(targetState);
}
mNestedScrolled = false;
}
这个方法也很简单,就是根据mLastNestedScrollDy(记录最后滑动的距离)来确定BottomSheetBehavior的最后的状态,那么很
如果mLastNestedScrollDy大于0(向上滑动),那么最后的状态就是展开状态,子View将top变为展开的值,同理如果小于0则变为关闭状态,如果等于0的话状态就变成离那个状态近变成那个状态。
好了,现在第一条线已经走完了,接下来走第二条线CoordinatorLayout通过onInterceptTouchEvent和onTouchEvent交给BottomSheetBehavior。这种场景就是你实现的BottomSheetBehavior的View不是滑动的View(实现ScrollingView接口的View),或者子类中没有这种View,或者触发事件不在这个View区域内。BottomSheetBehavior的拦截实现方法如下:
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
省略若干行....
case MotionEvent.ACTION_DOWN:
int initialX = (int) event.getX();
mInitialY = (int) event.getY();
//如果是普通的View的话scroll为null
View scroll = mNestedScrollingChildRef != null
? mNestedScrollingChildRef.get() : null;
if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
mActivePointerId = event.getPointerId(event.getActionIndex());
mTouchingScrollingChild = true;
}
//但是触摸事件又在当前的View中的话mIgnoreEvents还是为false
mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
!parent.isPointInChildBounds(child, initialX, mInitialY);
break;
}
//不是recycleView等滑动事件直接返回true拦截,这里滑动通过ViewDragHelper来实现
if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) {
return true;
}
// We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
// it is not the top most view of its parent. This is not necessary when the touch event is
// happening over the scrolling content as nested scrolling logic handles that case.
View scroll = mNestedScrollingChildRef.get();
//不管那种情况走到这都返回false
return action == MotionEvent.ACTION_MOVE && scroll != null &&
!mIgnoreEvents && mState != STATE_DRAGGING &&
!parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) &&
Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop();
}
这里利用了ViewDragHelper工具类来实现拖动,如果持有BottomSheetBehavior的不是ScrollingView那么mViewDragHelper.shouldInterceptTouchEvent(event)方法返回true的话将拦截事件交给BottomSheetBehavior的onTouchEvent的方法来实现拖动,先来看一下shouldInterceptTouchEvent方法实现:
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
//省略若干行.....
case MotionEvent.ACTION_MOVE: {
//省略若干行.....
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
//省略若干行.....
return return mDragState == STATE_DRAGGING;
这里只要满足ViewDragHelper状态为STATE_DRAGGING就会进行拦截,然而要满足这个状态最重要的就是tryCaptureViewForDrag方法是否满足,代码如下:
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
//当前的View就是拖扯的View直接返回true
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
//toCapture为当前触摸事件所在区域的子View
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
这边如果还没有可拖扯的控件则通过mCallback.tryCaptureView来判断是否可以拖扯,也就是是否可以拦截,那么只要它满足条件事件将交于BottomSheetBehavior处理,代码如下:
public boolean tryCaptureView(View child, int pointerId) {
if (mState == STATE_DRAGGING) {
return false;
}
if (mTouchingScrollingChild) {
return false;
}
if (mState == STATE_EXPANDED && mActivePointerId == pointerId) {
View scroll = mNestedScrollingChildRef.get();
if (scroll != null && scroll.canScrollVertically(-1)) {
// Let the content scroll up
return false;
}
}
return mViewRef != null && mViewRef.get() == child;
}
可以看出如果此View是ScrollingView直接返回false,不拦截,如果已经是拖扯状态不拦截,那么只有当前View不是ScrollingView的情况下将实现拦截,从中我们得出一个结论,如果自己使用ViewDragHelper的时候,重写方法之一就是tryCaptureView方法(确定什么情况下该view可拖扯),最后来看一下BottomSheetBehavior的OntouchEvent事件:
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
//省略若干行....
/**
事件处理交给ViewDragHelper来处理
**/
if (mViewDragHelper != null) {
mViewDragHelper.processTouchEvent(event);
}
// Record the velocity
if (action == MotionEvent.ACTION_DOWN) {
reset();
}
return !mIgnoreEvents;
}
很显然事件处理它完全交给了ViewDragHelper.processTouchEvent(event)来处理。接下来介绍一下ViewDragHelper的使用
class ViewDragHelperCallback extends Callback {
/**
* 此方法返回子view是否可以拖动 true可拖动
*/
@Override
public boolean tryCaptureView(View arg0, int arg1) {
// TODO Auto-generated method stub
return true;
}
/**
* clampViewPositionHorizontal调用后调用onViewPositionChanged(移动距离由DragViewHelp自己来移动),当view的位置变化的时候的调用,可以在一个View中带动另外一个View
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
}
// 手指拖动的时候先调用它,来确定最大的Top的边界
public int clampViewPositionVertical(View child, int left, int dx) {
}
// 手指拖动的时候先调用它,来确定最大的left的边界
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// TODO Auto-generated method stub
//边界检查假如左边可以拖车的最大位置
return left;
}
/**
* 这两个方法不影响子控件的点击事件
*/
/*public int getViewHorizontalDragRange(View child)
{
return getMeasuredWidth()-child.getMeasuredWidth();
}
//返回垂直拖动的范围
@Override
public int getViewVerticalDragRange(View child)
{
return getMeasuredHeight()-child.getMeasuredHeight();
}*/
// 手指抬起时的()回调方法,ACTION_UP和ACTION_CANCEL后的回调方法来处理惯性滑动(最终确定View的状态)
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// TODO Auto-generated method stub
}
}
}
接下来看一下BottomSheetBehavior实现的ViewDragHelper的回调方法,怎么实现拖动的,先确定子View可以拖扯的边界:
public int clampViewPositionVertical(View child, int top, int dy) {
return MathUtils.clamp(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
}
很显然和最大值和最小值做比较,也就是展开的高度和收缩的高度做比较,给出需要移动的合理值,那么ViewDragHelper就帮你自动移动了,最后手指释放时确定BottomSheetBehavior所属的状态:
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int top;
@State int targetState;
if (yvel < 0) { // Moving up
top = mMinOffset;
targetState = STATE_EXPANDED;
} else if (mHideable && shouldHide(releasedChild, yvel)) {
top = mParentHeight;
targetState = STATE_HIDDEN;
} else if (yvel == 0.f) {
int currentTop = releasedChild.getTop();
if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
top = mMinOffset;
targetState = STATE_EXPANDED;
} else {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
} else {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) {
setStateInternal(STATE_SETTLING);
ViewCompat.postOnAnimation(releasedChild,
new SettleRunnable(releasedChild, targetState));
} else {
setStateInternal(targetState);
}
}
这个代码就是根据当前的y速度的正负值来判断偏向与那边滑动,假如偏向上方滑动,则最后是展开状态,偏向下方滑动最后是关闭状态。如果y=0则看最后View的top是偏向与展开状态的值还是关闭状态的值从而确定状态,最后通过mViewDragHelper.settleCapturedViewAt来实现最后的惯性滚动,这就是非ScrollingView实现BottomSheetBehavior的滚动效果实现的(第二条线的实现)逻辑了。
ok,到这里BottomSheetBehavior源码就介绍完了。