作为嵌套滑动的父控制项,AdjustableHeaderLinearLayout(后面简写AHLLayout)需要实现parent相关的介面。网上非常多的资料还是实现的是NestedScrollingParent介面,但推荐实现的应该是NestedScrollingParent2。因为使用NestedScrollingParent的话,fling操作会变成在onNestedPreFling和onNestedFling进行判断是否消费的一锤子买卖,如果父控制项决定不消费fling事件,那么它后面在子控制项消费了部分fling距离后也不能接著处理剩下的fling距离,导致列表进行fling操作时会出现生硬的中断,具体可以阅读这篇文章[4]。
如果滚动下方的RecyclerView(RV)的,相关的调用流程如下: (RV)startNestedScroll → (AHLLayout)onStartNestedScroll → (AHLLayout)onNestedScrollAccepted→ (RV)dispatchNestedPreScroll → (AHLLayout)onNestedPreScroll→ (RV)dispatchNestedScroll→ (AHLLayout)onNestedScroll → (RV)stopNestedScroll → (AHLLayout)onStartNestedScroll。如上RV在自己滚动都会先通过onNestedPreScroll询问AHLLayout要不消费相应的滚动量以及具体消费多少。所以我们先看最关键的onNestedPreScroll方法。
consumed数组表示的parent分别在x,y方向上需要消费多少距离,consumed[1]表示的就是y方向要消费的距离。scrollBy方法会改变View的scrollY从而整体移动View的内容且不会引起View的重新layout和measure,整体移动AHLLayout其实就相当于滚动了header,所以我们用这个来实现上滑头部的功能。这样再看实现逻辑还是很清晰的,如果是向上滚的情况,应该先判断是否需要上滚头部,向下滚动时先让RV滚动,如果RV不能滚动时再向下移动header。 还要注意的是通过fling产生的滚动也会通过这个方法先询问AHLLayout要不要消费滚动距离,我们这里对两种滚动采用相关处理。
上面已经实现了基本的效果,不过还有很多细节需要考虑,有时候这些细节比功能的实现还让人头疼,就好比功能实现一小时,UI调整大半天。在详情页的设计中应该是可以滑动头部的header的,但在上面的实现中header只是一个普通的ViewGroup,而且它和RV是处于同级的关系,不存在父子关系也没法传递touch事件。当然这里可以手动拦截各种事件再进行各种处理再进行分发,方法还是有很多,我们这里选了一个简单的方案,我们在AHLLayout dispatchTouchEvent接受到Down事件是先判断是否在头部header之内,接著在通过后续事件判断发生了滚动动作时,先补发一个Down事件并将后续事件的y坐标添加一个等于头部header高度的偏移,这样正好落在下方的RV的处理范围等价于滚动了下方的RV,这样做还有一个好处是只在确定是滚动动作才hook事件的分发不会吃掉头部一些按钮等的点击事件。 下面是dispatchTouchEvent中的示意代码:
obtainNewMotionEvent方法是通过将接受到的MotionEvent的y坐标增加一个偏移量构造一个新的MotionEvent,这里有个注意点就是需要考虑多指触摸的场景,所以要使用对应的MotionEvent.obtain方法构造,不然多指拖动下方的RV时会出现IndexOutOfRangeException。
详情页头部一般都会使用图片作为背景,通常交互为了用户更好的使用体验都会支持下拉放大图片。以前的方案是下拉时动态调整header的高度,导致不停地重新requestLayout,那有没有更合理和高效的实现吗?
我们知道高效移动的关键是尽量使用支持直接修改渲染线程的RenderProperty的相关API,比如translation和scale等属性动画API,随著这个思路我们想出了如下方案,核心是使用translationY整体下移和使用scale放大头部的背景图。