在上篇文章自定义View事件之进阶篇(三)-CoordinatorLayout与Behavior中,咱们介绍了CoordainatorLayout下的Behavior机制,为了帮助你们更好的理解并运用Behavior,如今咱们经过一个Demo,来巩固咱们以前学习的知识点。java
该博客中涉及到的示例,在NestedScrollingDemo项目中都有实现,你们能够按需自取。git
先看一下咱们须要实现的效果吧,以下图所示:github
友情提示:Demo中涉及到的控件为CoordinatorLayout、TextView、RecyclerView。文章都会围绕这三个控件进行讲解。windows
从Demo效果来看,这是很是简单的嵌套滑动。若是采用咱们以前所学的NestedScrollingParent2
与NestedScrollingChild2
实现接口的方式。咱们能很是迅速的解决问题。可是若是采用自定义Behavior的话,那么就稍微有点难度了。不过不用担忧,只要一步一步慢慢分析,就总能解决问题的。markdown
在Demo中,RecyclerView与TextView开始的布局关系以下图所示:app
根据在文章自定义View事件之进阶篇(三)-CoordinatorLayout与Behavior中咱们所学的知识点,咱们知道CoordinatorLayout对子控件的布局是相似于FrameLayout的,因此为了保证RecyclerView在TextView的下方显示,咱们须要建立属于RecyclerView的Behavior,并在该Behavior的onLayoutChild
方法中处理RecyclerView与TextView的位置关系。ide
除了解决RecyclerView的位置关系之外,在该Demo中,咱们还能够看出,RecyclerView与TextView之间有着一个联动的关系(这里指的是RecyclerView与TextView之间的位置关系,而不是RecyclerView中的内容)。随着TextView逐渐上移的时候,下方的RecyclerView也跟着往上移动。那么咱们能够肯定的是RecyclerView必然是依赖TextView的。也就是说咱们须要重写Behavior的layoutDependsOn
与onDependentViewChanged
方法。函数
肯定一个控件(childView1)依赖另一个控件(childView2)的时候,是经过
layoutDependsOn(CoordinatorLayout parent, V child, View dependency)
这个方法。其中child是依赖对象(childView1),而dependency是被依赖对象(childView2),该方法的返回值是判断是否依赖对应view。若是返回true。那么表示依赖。反之不依赖。通常状况下,在咱们自定义Behavior时,咱们须要重写该方法。当layoutDependsOn
方法返回true时,后面的onDependentViewChanged
与onDependentViewRemoved
方法才会调用。oop
除了考虑以上因数之外,咱们还须要考虑RecyclerView的高度。观察Demo,咱们能够看出,RecylerView在移动先后,始终都是填充整个屏幕的。为了保证RecylerView在移动过程当中,屏幕中不会出现空白(以下图所示)。咱们也须要在CoordinatorLayout测量该控件的高度以前,让控件自主的去测量高度。也就是重写RecylerView对应Behavior中的onMeasureChild
方法。布局
分析了RecyclerView的Behavior须要重写的内容后,咱们来看看具体的Behavior实现类HeaderScrollingViewBehavior
。为了帮助你们理解,我将RecyclerView的Behavior拆成了几个部分,代码以下所示:
查看该Behavior完整代码,请点击--->HeaderScrollingViewBehavior
public class HeaderScrollingViewBehavior extends CoordinatorLayout.Behavior<View> { public HeaderScrollingViewBehavior() {} public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) { super(context, attrs); } /** * 依赖TextView */ @Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { return dependency instanceof TextView; } //省略部分代码… } 复制代码
注意:在xml引用自定义Behavior时,必定要声明构造函数。否则在程序的编译过程当中,会提示知道不到相应的Behavior。
layoutDependsOn
方法的逻辑很是简单。就是判断依赖的对象是不是TextView。咱们继续查看该类中的onMeasureChild
方法。代码以下所示:
@Override public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { //获取当前滚动控件的测量模式 final int childLpHeight = child.getLayoutParams().height; //只有当前滚动控件为match_parent/wrap_content时才从新测量其高度,由于固定高度不会出现底部空白的状况 if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { //获取当前child依赖的对象集合 final List<View> dependencies = parent.getDependencies(child); final View header = findFirstDependency(dependencies); if (header != null) { if (ViewCompat.getFitsSystemWindows(header) && !ViewCompat.getFitsSystemWindows(child)) { // If the header is fitting system windows then we need to also, // otherwise we'll get CoL's compatible measuring ViewCompat.setFitsSystemWindows(child, true); if (ViewCompat.getFitsSystemWindows(child)) { // If the set succeeded, trigger a new layout and return true child.requestLayout(); return true; } } //获取当前父控件中可用的距离, int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec); if (availableHeight == 0) { // If the measure spec doesn't specify a size, use the current height availableHeight = parent.getHeight(); } //计算当前滚动控件的高度。 final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header); final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT ? View.MeasureSpec.EXACTLY : View.MeasureSpec.AT_MOST); //测量当前滚动的View的正确高度 parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); return true; } } return false; } 复制代码
测量逻辑的基本步骤:
match_parent
或者wrap_content
。(对于精准模式,咱们不用考虑,控件是否填充屏幕)-
TextView的高度+
TextView的滚动范围)在onMeasureChild
方法中,我省略了部分方法的介绍,如findFirstDependency
、getScrollRange
方法。这些方法在NestedScrollingDemo项目中都有实现。你们能够按需自取。
咱们继续查看HeaderScrollingViewBehavior
类中的onLayoutChild
方法,代码以下所示:
@Override public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { final List<View> dependencies = parent.getDependencies(child); final View header = findFirstDependency(dependencies); if (header != null) { final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams(); final Rect available = mTempRect1; //获得依赖控件下方的坐标。 available.set(parent.getPaddingLeft() + lp.leftMargin, header.getBottom() + lp.topMargin, parent.getWidth() - parent.getPaddingRight() - lp.rightMargin, parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin); //拿到上面计算的坐标后,根据当前控件在父控件中设置的gravity,从新计算并获得控件在父控件中的坐标 final Rect out = mTempRect2; GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), available, out, layoutDirection); //拿到坐标后从新布局 child.layout(out.left, out.top, out.right, out.bottom); } else { //若是没有依赖,则调用父控件来处理布局 parent.onLayoutChild(child, layoutDirection); } return true; } 复制代码
onLayoutChild
方法逻辑也不算复杂,根据当前所依赖的header(TextView)的位置,将RecyclerView设置在TextView下方。咱们继续查看RecyclerView与TextView的联动处理。也就是onDependentViewChanged
方法。代码以下所示:
@Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { final CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior(); if (behavior instanceof NestedHeaderBehavior) { ViewCompat.offsetTopAndBottom(child, dependency.getBottom() - child.getTop() + ((NestedHeaderBehavior) behavior).getOffset()); } //若是当前的控件的位置发生了改变,该返回值必定要返回为true return true; } 复制代码
在该方法中,咱们须要经过TextView的Behavior(NestedHeaderBehavior
),并得到TextView的实际偏移量(上述代码中的getOffset()
)。经过该偏移量咱们能够从新设置RecyclerView的位置。固然,改变控件位置的方式有不少种,咱们可使用setTransationY
或View.offsetTopAndBottom
及其余方式,你们能够采用本身喜欢的方式。由于涉及到TextView中Behavior的偏移量。那下面咱们就来看看TextView对应Behavior的分析与实现吧。
在整个Demo中,TextView的嵌套滑动效果并不复杂。这里咱们就从向上与向下两个方向来介绍。
在讲解TextView的Behavior的代码实现以前,咱们须要回顾一下在CooordinatoLayout下嵌套方法的传递过程,以下图所示:
经过回顾流程,在结合本文例子中展现的效果,咱们须要重写Behavior中的onStartNestedScroll
与onNestedPreScroll
和onNestedScroll
三个方法。来看TextView的NestedHeaderBehavior
实现。代码以下所示:
查看该Behavior完整代码,请点击--->NestedHeaderBehavior
public class NestedHeaderBehavior extends CoordinatorLayout.Behavior<View> { private WeakReference<View> mNestedScrollingChildRef; private int mOffset;//记录当前布局的偏移量 public NestedHeaderBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(parent)); return super.onLayoutChild(parent, child, layoutDirection); } //省略部分代码… } 复制代码
TextView中NestedHeaderBehavior类的声明与RecyclerView中的Behavior基本同样。由于咱们须要将偏移量传递给RecyclerView,因此在NestedHeaderBehavior
的onLayoutChild方法中,咱们去建立了关于RecyclerView的弱引用,并设置了mOffset
变量来记录TextViwe每次滑动的偏移量。如何获取RecyclerView,能够查看项目中源码的实现。接下来,咱们继续查看相关嵌套方法实现。
@Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { //只要竖直方向上就拦截 return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } 复制代码
在onStartNestedScroll
方法中,咱们设置了当前控件,只能拦截竖直方向上的嵌套滑动事件。继续查看onNestedPreScroll
方法。代码以下所示:
@Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { View scrollingChild = mNestedScrollingChildRef.get(); if (target != scrollingChild) { return; } int currentTop = child.getTop(); int newTop = currentTop - dy; if (dy > 0) {//向上滑动 //处理在范围内的滚动与fling if (newTop >= -child.getHeight()) { Log.i(TAG, "onNestedPreScroll:向上移动" + "currentTop--->" + currentTop + " newTop--->" + newTop); consumed[1] = dy; mOffset = -dy; ViewCompat.offsetTopAndBottom(child, -dy); coordinatorLayout.dispatchDependentViewsChanged(child); } else { //当超事后,单独处理 consumed[1] = child.getHeight() + currentTop; mOffset = -consumed[1]; ViewCompat.offsetTopAndBottom(child, -consumed[1]); coordinatorLayout.dispatchDependentViewsChanged(child); } } if (dy < 0) {//向下滑动 if (newTop <= 0 && !target.canScrollVertically(-1)) { Log.i(TAG, "onNestedPreScroll:向下移动" + "currentTop--->" + currentTop + " newTop--->" + newTop); consumed[1] = dy; mOffset = -dy; ViewCompat.offsetTopAndBottom(child, -dy); coordinatorLayout.dispatchDependentViewsChanged(child); } } } 复制代码
onNestedPreScroll
方法中的逻辑较为复杂。不急咱们慢慢分析:
currentTop
)。而后根据当前偏移距离dy
,计算出TextView新的Top高度(newTop
)。dy>0
,也就是向上滑动。咱们判断偏移后的Top(newTop
)高度是否大于负
的TextView的测量的高度。由于是向上滑动,当TextView移出屏幕后,经过调用getTop方法获取的高度确定为负数。这里判断是否大于等于
-child.getHeight
,表示的是当前TextView没有超过它的滚动范围(-child.getHeight到0)。
newTop >= -child.getHeight()
,则TextView消耗掉dy
,经过ViewCompat.offsetTopAndBottom(child, -dy)
来移动当前TextView,接着记录TextView位置的偏移量(mOffest
),最后经过调用CoordinatorLayout下的dispatchDependentViewsChanged
方法,通知控件RecyclerView所依赖的TextView发生了改变。那么RecyclerView收到通知后,就能够拿着这个偏移量和TextView一块儿联动了。newTop< - child.getHeight()
,表示在当前偏移距离dy
下,若是TextView会超过它的滚动范围。那么咱们就不能使用当前dy
来移动TextView。咱们只能滚动剩下的范围,也就是child.getHeight() +
currentTop,(这里使用加号,是由于滚动范围为-child.getHeight
到0
)。dy<0
,表示向下滑动,只有在target(RecyclerView)不能向下滑动且TextView已经部分移出屏幕时,咱们的TextView才能向下滑动。这里的处理方式基本和上滑同样,这里就再也不进行介绍了。咱们继续查看最后的方法onNestedScroll
方法。@Override public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { if (dyUnconsumed < 0) {//表示已经向下滑动到头。 int currentTop = child.getTop(); int newTop = currentTop - dyUnconsumed; if (newTop <= 0) {//若是当前的值在滚动范围以内。 Log.i(TAG, "onNestedScroll: " + "dyUnconsumed--> " + dyUnconsumed + " currentTop--->" + currentTop + " newTop--->" + newTop); ViewCompat.offsetTopAndBottom(child, -dyUnconsumed); mOffset = -dyUnconsumed; } else {//若是当前的值大于最大的滚动范围(0),那么就直接滚动到-currentTop就好了 ViewCompat.offsetTopAndBottom(child, -currentTop); mOffset = -currentTop; } coordinatorLayout.dispatchDependentViewsChanged(child); } } 复制代码
onNestedScroll
方法中,咱们须要处理RecyclerView向下方向上未消耗的距离(dyUnconsumed
)。一样根据当前偏移记录计算出TextVie新的Top高度,计算出是否超出其滚功范围范围。若是没有超过,则TextView向下偏移距离为-dyUnconsumed
,同时记录偏移量(mOffset=-dyUnconsumed
),最后通知RecyclerView,TextView的位置发生了改变。反之,当前TextView的top的值是多少,那么TextView就向下偏移多少。
在该文章中,我着重讲解了相应Behavior中比较重要的一些方法。一些不是那么重要的辅助方法,我并无作过多的介绍。建议你们配合NestedScrollingDemo项目中的源码理解该篇文章,我相信确定是事半功倍的。
关于嵌套滑动、CoordinatorLayout、Behavior的知识点基本介绍完毕了。我相信你们之后再碰见一些嵌套滑动的问题。都可以轻松的解决了。可能不少小伙伴会好奇,为何没有接着讲AppBarLayout与CollapsingTollbarLayout的原理及使用。其实缘由很是简单,由于上述的两个控件的实现原理,实际上是依托于CoordinatorLayout与自定义Behavior罢了。授人以鱼,不如授人以渔。AppBarLayout与CollapsingTollbarLayout的使用及原理。就算给你们留的课后思考题吧。谢谢你们对这系列的关注。Thanks。