1. 引言
BottomSheetBehavior能实现怎样的效果,一图胜千言。
如果仅仅是实现上下拖动和隐藏的功能。抛开BottomSheetBehavior自己实现也不难,在没有CoordinatorLayout的年代,这种效果往往是纯手工打造。既然如此为何Google要专门设计BottomSheetBehavior呢?为了搞清楚这个问题,我查阅源码探究了一番,确实发现了一些隐秘的角落。我将从以下几个方面讲解BottomSheetBehavior的设计思路
- 讲解BottomSheetBehavior的几种状态
- 讲解BottomSheetBehavior的事件分发
- 讲解BottomSheetBehavior如何处理嵌套滑动
- 实现高德地图首页效果,欢迎关注字节小站微信公众号号
2. BottomSheetBehavior的几种状态
BottomSheetBehavior一共有6种状态
- STATE_EXPANDED 全部展开状态
- STATE_COLLAPSED 收起状态
- STATE_DRAGGING 拖动状态
- STATE_SETTLING
- STATE_HIDDEN 隐藏状态
- STATE_HALF_EXPANDED 半展开状态
系统通过哪种方式实现每种状态不同的偏移量呢?
- layout阶段通过ViewCompat.offsetTopAndBottom(child, offset)实现偏移量
- 用户触摸交互阶段通过ViewDragHelper.dragTo(left,top,dx,dy)实现偏移量
2.1 Layout阶段
Layout阶段最后会通过findScrollingChild方法,寻找开启了嵌套滑动的后代View。其实这就是Google单独研发出BottomSheetBehavior的主要考量。满足支持嵌套滑动的BottomSheet效果。
2.2 用户触摸交互阶段
2.3 状态对应的偏移量
状态 | 偏移量 |
STATE_COLLAPSED | collapsedOffset |
STATE_EXPANDED | getExpandedOffset() |
STATE_HALF_EXPANDED | halfExpandedOffset |
STATE_HIDDEN | parentHeight |
1. 计算 collapsedOffset
变量名 | 默认值 |
PEEK_HEIGHT_AUTO | 常量值-1 |
peekHeightMin | 默认值64dp,用户不可修改 |
peekHeightAuto | 默认值true,用户可设置 |
peekHeight | 默认值0,如果设置为PEEK_HEIGHT_AUTO peekHeightAuto为true否则为false,如果设置小于-1则为0 |
fitToContents | 默认值true,用户可设置 |
fitToContentOffset | Math.max(0, parentHeight - child.getHeight()) |
peekHeight默认值为0。设置逻辑如下
- height为-1,则peekHeightAuto设置为true。
- 否则peekHeightAuto为false,而且peekHeight最小值为0。
计算collapsedOffset值有四种情况
Case | peekHeightAuto | fitToContents |
case1 | true | true |
case2 | true | false |
case3 | false | true |
case4 | false | false |
返回值
Case | 返回值 |
Case1 | Math.max(parentHeight - Math.max(peekHeightMin, parentHeight - parentWidth * 9 / 16), fitToContentsOffset) |
Case2 | parentHeight-Math.max(peekHeightMin, parentHeight - parentWidth * 9 / 16) |
Case3 | Math.max(parentHeight - peekHeight, fitToContentsOffset) |
Case4 | parentHeight - peekHeight |
2. 计算 halfExpandedOffset
3. 计算 expandedOffset
4.如何固定BottomSheetBehavior的高度?
了解这些值的计算有什么好处。假设我想让BottomSheetBehavior,固定高度,不能向上滑也不能向下滑。那我们则需要将collapsedOffset和expandedOffset设置为一样的值才行。
代码如下
为了良好的阅读体验没有使用代码块呈现代码,如果你想获取代码请访问github代码库
3. 讲解BottomSheetBehavior的事件分发
学习Android事件分发是有方法的。我总结为"三板斧"分析法
- 源码分析
- 场景化
- 树形图分析
3.1 三板斧之源码分析
从onInterceptTouchEvent的代码中,可以看到viewDragHelper.shouldInterceptTouchEvent(event),说明拦截方法会让ViewDragHelper方法处理。
ViewDragHelper的初始化,会传入ViewDragHelper.Callback dragCallback对象,该对象的boolean tryCaptureView(View child, int pointerId)方法决定viewDragHelper.shouldInterceptTouchEvent的返回值。
onInterceptTouchEvent的拦截逻辑如下
onTouchEvent主要交由ViewDragHelper#processTouchEvent处理,如果是Move事件最终会调用dragTo方法进行移动
3.2 三板斧之场景化和树形图分析
假设有场景如下,用户可以在HeadLayout、NestedScrollingChild,TopMostLayout区域内上下滑动。这三个case,事件的处理路径如何呢?
转化成树形图如下
设置BottomSheetBehavior为LinearLayout的Behavior
3.2.1. 在HeadLayout区域内上下滑动
- Down事件处理,初始状态,在ViewDragHelper的shouldInterceptTouchEvent方法中不会调用tryCaptureViewForDrag方法,该方法返回false。在BottomSheetBehavior onInterceptTouchEvent中完整事件路径如下,红线表示事件的分发路径
结合树形图分析。由于BottomSheetBehavior不拦截事件。Down事件分发流程如下
最终会调用到BottomSheetBehavior的onTouchEvent方法,会调用到ViewDragHelper的processTouchEvent方法
最终会将ViewDragHelper的dragState设置为STATE_DRAGGING
- MOVE事件在BottomSheetBehavior onInterceptTouchEvent分发流程如下
接下来直接调用 BottomSheetBehavior 的onTouchEvent方法。同样调用到ViewDragHelper的processTouchEvent方法
3.2.2. 在NestedScrollingView区域内上下滑动
1.Down事件分发到BottomSheetBehavior的onInterceptTouchEvent分发流程如下
由于不拦截。Down事件分发给NestedScrollingChild,NestedScrollingChild会启动嵌套滑动,与BottomSheetBehavior配合完成嵌套滑动
2.Move事件分发流程比较复杂,当在NSC中Move的距离没达到阈值时,MOVE会继续分发到BottomSheetBehavior的onInterceptTouchEvent中,当在NSC中MOVE距离达到阈值时,会调用parent.requestDisallowInterceptTouchEvent(true)从此直达NSC,就是纯粹的嵌套滑动了。
接下来事件交由NSC分发,当MOVE距离大于阈值时,事件直接交由NSC处理。
3.2.2. 在TopMostLayout区域内上下滑动,该区域与NSC区域没有交集
Down事件同上
MOVE事件,当距离>ViewDragHelper阈值时
由于MOVE事件拦截了,会交由BottomSheetBehavior onTouchEvent处理,如下图
4. 结束
至此已基本讲解完BottomSheetBehavior的事件分发机制。具体细节未能尽善尽美。纸上得来终觉浅,希望读者可以结合文章去探索源码。下次我将用BottomSheetBehavior来实现高德地图首页效果。欢迎持续关注