相关使用可查看以下博客 :https://www.jianshu.com/p/0a7383e0ad0f

原文出处:https://github.com/android-cjj/SourceAnalysis/blob/master/README.md 

2月25日早上,Android官网更新了Support Lirary 23.2版本,其中Design Support Library库新加一个新的东西:Bottom Sheets。然后,第一时间写了篇Teach you how to use Design Support Library: Bottom Sheets,只是简单的讲了它的使用和使用的一些规范。

blob.png

这篇文章我带大家看看BottomSheetBehavior的源码,能力有限,写的不好的地方,请尽力吐槽。好了,不说废话,直接主题

我们先简单的看下用法

 
  1.         // The View with the BottomSheetBehavior
  2.         View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
  3.         final BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
  4.         behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
  5.             @Override
  6.             public void onStateChanged(@NonNull View bottomSheet, int newState) {
  7.                 //这里是bottomSheet 状态的改变回调
  8.             }
  9.  
  10.             @Override
  11.             public void onSlide(@NonNull View bottomSheet, float slideOffset) {
  12.                 //这里是拖拽中的回调,根据slideOffset可以做一些动画
  13.             }
  14.         });

对于切换状态,你也可以手动调用behavior.setState(int state); state 的值你可以看我的上一篇戳我

BottomSheetBehavior的定义如下

 
  1.     public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V>

继承自CoordinatorLayout.Behavior,BottomSheetBehavior.from(V view)方法获得了BootomSheetBehavior的实例,我们进去看看它怎么实现的。

 
  1.     public static <V extends View> BottomSheetBehavior<V> from(V view) {
  2.         ViewGroup.LayoutParams params = view.getLayoutParams();
  3.         if (!(params instanceof CoordinatorLayout.LayoutParams)) {
  4.             throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
  5.         }
  6.         CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
  7.                 .getBehavior();
  8.         if (!(behavior instanceof BottomSheetBehavior)) {
  9.             throw new IllegalArgumentException(
  10.                     "The view is not associated with BottomSheetBehavior");
  11.         }
  12.         return (BottomSheetBehavior<V>) behavior;
  13.     }

源码中看出根据传入的参数view的LayoutParams是不是 CoordinatorLayout.LayoutParams,若不是,将抛出"The view is not a child of CoordinatorLayout"的异常,通过 ((CoordinatorLayout.LayoutParams) params).getBehavior()获得一个behavior并判断是不是BottomSheetBehavior,若不是,就抛出异常"The view is not associated with BottomSheetBehavior",都符合就返回了BottomSheetBehavior的实例。这里我们可以知道behavior保存在 CoordinatorLayout.LayoutParams里,那它是 怎么保存的呢,怀着好奇心,我们去看看CoordinatorLayout.LayoutParams中的源码,在LayoutParams的构造函数中,有这么一句:

 
  1.             if (mBehaviorResolved) {
  2.                 mBehavior = parseBehavior(context, attrs, a.getString(
  3.                         R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
  4.             }

顺藤摸瓜,我们在跟进去看看parseBehavior做了什么

 
  1.      static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
  2.         Context.class,
  3.         AttributeSet.class
  4.     };
  5.  
  6.     static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
  7.        /*
  8.         *省略部分代码
  9.         */
  10.         try {
  11.            /*
  12.             *省略部分代码
  13.             */
  14.             Constructor<Behavior> c = constructors.get(fullName);
  15.             if (c == null) {
  16.                 final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
  17.                         context.getClassLoader());
  18.                 c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
  19.                 c.setAccessible(true);
  20.                 constructors.put(fullName, c);
  21.             }
  22.             return c.newInstance(context, attrs);
  23.         } catch (Exception e) {
  24.             throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
  25.         }
  26.     }

这里做的事情很简单,就是在实例化CoordinatorLayout.LayoutParams时反射生成Behavior实例,这就是为什么自定义behavior需要重写如下的构造函数

 
  1.     public class CjjBehavior extends CoordinatorLayout.Behavior{
  2.         public CjjBehavior(Context context, AttributeSet attrs) {
  3.             super(context, attrs);
  4.         }
  5.     }

不然就会看到"Could not inflate Behavior subclass ..."异常 。

目前为止,我们只是了解了CoordinatorLayout.Behavior相关的东西,还是不知道BottomSheetBehavior实现的原理,别急,这就和你说说。

view布局

当你的View持有Behavior的时候, CoordinatorLayout 在 onLayout 的时候会调用Behavior.onLayoutChild方法进行布局.

注意:我们将持有的Behavior 的View 叫做BehaviorView

我们查看onLayoutChild 的源码

 
  1.     @Override
  2.     public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
  3.         // First let the parent lay it out
  4.         if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {
  5.             parent.onLayoutChild(child, layoutDirection);
  6.         }
  7.         // Offset the bottom sheet
  8.         mParentHeight = parent.getHeight();
  9.         mMinOffset = Math.max(0, mParentHeight - child.getHeight());
  10.         mMaxOffset = mParentHeight - mPeekHeight;
  11.         if (mState == STATE_EXPANDED) {
  12.             ViewCompat.offsetTopAndBottom(child, mMinOffset);
  13.         } else if (mHideable && mState == STATE_HIDDEN) {
  14.             ViewCompat.offsetTopAndBottom(child, mParentHeight);
  15.         } else if (mState == STATE_COLLAPSED) {
  16.             ViewCompat.offsetTopAndBottom(child, mMaxOffset);
  17.         }
  18.         if (mViewDragHelper == null) {
  19.             mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
  20.         }
  21.         mViewRef = new WeakReference<>(child);
  22.         mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
  23.         return true;
  24.     }

这里主要做了几件事情:

  1. 对BehaviorView 的摆放:先调用父类 对 BehaviorView 进行布局,根据 PeekHeight 和 State 对 BehaviorView 位置的进行偏移,偏移到合适的位置.

  2. 对mMinOffset,mMaxOffset的计算,根据mMinOffset 和mMaxOffset 可以确定BehaviorView 的偏移范围.即 距离CoordinatorLayout 原点 Y轴mMinOffset 到mMaxOffset;

  3. 始化了ViewDragHelper 类.ViewDragHelper是一个非常厉害的组件.我们这边使用它处理进行拖拽和滑动事件.

  4. 存储BehaviorView 的软引用和递归找到第一个NestedScrollingChild组件,当然NestedScrollingChild也可以为空.下面的逻辑对于NestedScrollingChild为空的情况做了处理的.

onLayoutChild做的事情还是挺少的.算是一些初始化的东西

因为State 默认为STATE_COLLAPSED,偏移量为ParentHeight - PeekHeight, 这时候BehaviorView 被往下调整了,露出屏幕的高度为PeekHeight 的大小.

在Android 5.0上可能是因为优化的原因还是别的因素. 当一开始的 PeekHeight为0的时候 整个BehaviorView 被移到屏幕外, 它就不会被绘制上去.导致你看不到BehaviorView的画面,但是它是存在的.实实在在存在着

我的好基友dim给出了解决方案Android support 23.2 使用BottomSheetBehavior 的坑

事件拦截

touch 事件会先被onInterceptTouchEvent()捕获,进行判断是否拦截.

 
  1. @Override
  2. public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
  3.     if (!child.isShown()) {
  4.         return false;
  5.     }
  6.     int action = MotionEventCompat.getActionMasked(event);
  7.     // Record the velocity
  8.     if (action == MotionEvent.ACTION_DOWN) {
  9.         reset();
  10.     }
  11.     if (mVelocityTracker == null) {
  12.         mVelocityTracker = VelocityTracker.obtain();
  13.     }
  14.     mVelocityTracker.addMovement(event);
  15.     switch (action) {
  16.         case MotionEvent.ACTION_UP:
  17.         case MotionEvent.ACTION_CANCEL:
  18.             mTouchingScrollingChild = false;
  19.             mActivePointerId = MotionEvent.INVALID_POINTER_ID;
  20.             // Reset the ignore flag
  21.             if (mIgnoreEvents) {
  22.                 mIgnoreEvents = false;
  23.                 return false;
  24.             }
  25.             break;
  26.         case MotionEvent.ACTION_DOWN:
  27.             int initialX = (int) event.getX();
  28.             mInitialY = (int) event.getY();
  29.             View scroll = mNestedScrollingChildRef.get();
  30.             if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
  31.                 mActivePointerId = event.getPointerId(event.getActionIndex());
  32.                 mTouchingScrollingChild = true;
  33.             }
  34.             mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
  35.                     !parent.isPointInChildBounds(child, initialX, mInitialY);
  36.             break;
  37.     }
  38.     if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) {
  39.         return true;
  40.     }
  41.     // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
  42.     // it is not the top most view of its parent. This is not necessary when the touch event is
  43.     // happening over the scrolling content as nested scrolling logic handles that case.
  44.     View scroll = mNestedScrollingChildRef.get();
  45.     return action == MotionEvent.ACTION_MOVE && scroll != null &&
  46.             !mIgnoreEvents && mState != STATE_DRAGGING &&
  47.             !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) &&
  48.             Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop();
  49. }

onInterceptTouchEvent 做了几件事情:

  1. 判断是否拦截事件.先使用mViewDragHelper.shouldInterceptTouchEvent(event)拦截.

  2. 使用mVelocityTracker 记录手指动作,用于后期计算Y 轴速率.

  3. 判断点击事件是否在NestedChildView 上,将 boolean 存到mTouchingScrollingChild 标记位中,这个主要是用于ViewDragHelper.Callback 中的判断.

  4. ACTION_UP 和ACTION_CANCEL 对标记位进行复位,好在下一轮 Touch 事件中使用.

onTouchEvent处理

 
  1.  @Override
  2.     public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
  3.         if (!child.isShown()) {
  4.             return false;
  5.         }
  6.         int action = MotionEventCompat.getActionMasked(event);
  7.         if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
  8.             return true;
  9.         }
  10.         mViewDragHelper.processTouchEvent(event);
  11.         // Record the velocity
  12.         if (action == MotionEvent.ACTION_DOWN) {
  13.             reset();
  14.         }
  15.         if (mVelocityTracker == null) {
  16.             mVelocityTracker = VelocityTracker.obtain();
  17.         }
  18.         mVelocityTracker.addMovement(event);
  19.         // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
  20.         // to capture the bottom sheet in case it is not captured and the touch slop is passed.
  21.         if (action == MotionEvent.ACTION_MOVE) {
  22.             if (Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop()) {
  23.                 mViewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex()));
  24.             }
  25.         }
  26.         return true;
  27.     }

onTouchEvent 主要做了几件事情:

  1. 使用mVelocityTracker 记录手指动作.用于后期计算Y 轴速率.

  2. 使用mViewDragHelper 处理Touch 事件.可能会产生拖动效果.

  3. mViewDragHelper 在滑动的时候对BehaviorView 的再一次捕获.再一次明确告诉ViewDragHelper 我要移动的是BehaviorView 组件.什么情况需要主动告诉ViewDragHelper ?比如:当你点击在BehaviorView 的区域,但是BehaviorView 的视图的层级不是最高的,或者你点击的区域不在 BehaviorView 上,ViewDragHelper 在做处理滑动的时候找不到BehaviorView, 这个时候你要手动告知它现在要移动的是BehaviorView,情景类似ViewDragHelper处理EdgeDrag 的样子.

注意

即使你的onInterceptTouchEvent 返回false,也可能因为下面的View 没有处理这个Touch事件,而导致Touch 事件上发被Behavior的onTouchEvent 被截取.

NestedScrolling事件处理

当 CoordinatorLayout 的子控件有 NestedScrollingChild 产生 Nested 事件的时候.会调用onStartNestedScroll 这个方法

 
  1.     @Override
  2.     public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
  3.             View directTargetChild, View target, int nestedScrollAxes) {
  4.             return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//滑动Y轴方向的判断
  5.     }

返回值 true :表示 BehaviorView 要和NestedScrollingChild 配合消耗这个 NestedScrolling 事件,这里可以看出只要是纵向的滑动都会返回true.

onNestedPreScroll

NestedScrollingChild的在滑动的时候会触发onNestedPreScroll 方法,询问BehaviorView消耗多少Y轴上面的滑动.

 
  1.   @Override
  2.     public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx,
  3.             int dy, int[] consumed) {
  4.         View scrollingChild = mNestedScrollingChildRef.get();
  5.         if (target != scrollingChild) {
  6.             return;
  7.         }
  8.         int currentTop = child.getTop();
  9.         int newTop = currentTop - dy;
  10.         if (dy > 0) { // Upward
  11.             if (newTop < mMinOffset) {
  12.                 consumed[1] = currentTop - mMinOffset;
  13.                 ViewCompat.offsetTopAndBottom(child, -consumed[1]);
  14.                 setStateInternal(STATE_EXPANDED);
  15.             } else {
  16.                 consumed[1] = dy;
  17.                 ViewCompat.offsetTopAndBottom(child, -dy);
  18.                 setStateInternal(STATE_DRAGGING);
  19.             }
  20.         } else if (dy < 0) { // Downward
  21.             if (!ViewCompat.canScrollVertically(target, -1)) {
  22.                 if (newTop <= mMaxOffset || mHideable) {
  23.                     consumed[1] = dy;
  24.                     ViewCompat.offsetTopAndBottom(child, -dy);
  25.                     setStateInternal(STATE_DRAGGING);
  26.                 } else {
  27.                     consumed[1] = currentTop - mMaxOffset;
  28.                     ViewCompat.offsetTopAndBottom(child, -consumed[1]);
  29.                     setStateInternal(STATE_COLLAPSED);
  30.                 }
  31.             }
  32.         }
  33.         dispatchOnSlide(child.getTop());
  34.         mLastNestedScrollDy = dy;
  35.         mNestedScrolled = true;
  36.     }

onNestedPreScroll 方法主要做几件事情:

  1. 判断发起NestedScrolling 的 View 是否是我们在onLayoutChild 找到的那个控件.不是的话,不做处理.不处理就是不消耗y 轴,把所有的Scroll 交给发起的 View 自己消耗.

  2. 根据dy 判断方向,根据之前的偏移范围算出偏移量.使用ViewCompat.offsetTopAndBottom 对BehaviorView 进行偏移操作.

  3. 消耗Y轴的偏移量.发起 NestedScrollingChild 会自动响应剩下的部分

其中comsume[]是个数组,consumed[1]表示 Parent 在 Y 轴消耗的值, NestedScrollingChild 会消耗除BehaviorView消耗剩下的那部分( 比如: NestedScrollingChild 要滑动20像素,因为BehaviorView消耗了10像素,最后NestedScrollingChild 只滑动了10像素);

onStopNestedScroll在Nestd事件结束触发. 主要做的事情: 根据BehaviorView当前的状态对它的最终位置的确定,有必要的话调用ViewDragHelper.smoothSlideViewTo 进行滑动.

注意

当你是往下滑动且Hideable 为 true ,他会 使用上面计算的Y轴的速率的判断.是否应该切换到Hideable 的状态.

onNestedPreFling

这个是 NestedScrollingChild 要滑行时候触发的,询问 BehaviorView是否消耗这个滑行.

 
  1. @Override
  2. public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
  3.                                 float velocityX, float velocityY) {
  4.     return target == mNestedScrollingChildRef.get() &&
  5.             (mState != STATE_EXPANDED ||
  6.                     super.onNestedPreFling(coordinatorLayout, child, target,
  7.                             velocityX, velocityY));
  8. }

处理逻辑是:发起Nested事件要与onLayoutChild 找到的那个控件一致且当前状态是一个STATE_EXPANDED状态.

返回值: true表示BehaviorView 消耗滑行事件,那么NestedScrollingChild就不会有滑行了

ViewDragHelper.Callback

ViewDragHelper网上教程挺多的,就不多讲了,他主要是处理滑动拖拽的.

小技巧

在说说一个小技巧,Android官网中有这样一句话:Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android,就是说枚举比静态常量更加耗费内存,我们应该避免使用,然后我看BottomSheetBehavior源码中 mState 是这样定义的:

 
  1.     public static final int STATE_DRAGGING = 1;
  2.     public static final int STATE_SETTLING = 2;
  3.     public static final int STATE_EXPANDED = 3;
  4.     public static final int STATE_COLLAPSED = 4;
  5.     public static final int STATE_HIDDEN = 5;
  6.  
  7.     @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN})
  8.     @Retention(RetentionPolicy.SOURCE)
  9.     public @interface State {}
  10.  
  11.     @State
  12.     private int mState = STATE_COLLAPSED;

弥补了Android不建议使用枚举的缺陷。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐