AppBarLayout和CoordinatorLayout
下图所示为CoordinatorLayout和AppBarLayout的关系图
image.png
- AppBarLayout是垂直方向的LinearLayout,和CoorinatorLayout配合使用,是CoordinatorLayout的子View。
- AppBarLayout本身不能滑动,要配合NestScrollView(通常是RecycleView)才可能滑动。NestScrollView需要设置AppBarLayout.ScrollingViewBehavior。AppBarLayout默认有一个
AppBarLayout.Behavior
。 - AppBarLayout的子View通过
app:layout_scrollFlags
来区分是否能够滑动以及滑动的效果。参考:https://blog.csdn.net/eyishion/article/details/80282204 - CoorinatorLayout是一个自定义的ViewGroup,实现了NestedScrollingParent2,可以滑动的子View实现了NestedScrollingChild2,两者配合控制子View的嵌套滑动效果。
实现的效果
向下滑动上下按钮全部出现,向上滑动上下按钮全部隐藏。
image.png
如图所示:滑动RecycleView时改变MaskRecommend的位置,实现所要求效果。设置app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed",让Recommend向下滑动时出现,向上滑动时消失。
其中MaskRecommend自定义Behavior如下所示:
class MaskRecommendBehavior :CoordinatorLayout.Behavior<View>{
private var translationY = 0//maskRecommend移动距离
constructor() : super()
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean {
var layout = super.onLayoutChild(parent, child, layoutDirection)
return layout
}
override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
// return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type)
return axes and ViewCompat.SCROLL_AXIS_VERTICAL !=0
}
override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
var navigationHeight = 275//HyNavigation高度,测试时写死
translationY = translationY-dy//计算MaskRecomend移动的距离
Log.d("chao","onNestedPreScroll:"+target.top+":"+translationY+":"+(navigationHeight - child.height))
if(translationY>0){//如果移动距离大于0,则重置距离为0
translationY = 0
}else if(translationY<-child.height){//如果移动距离小于-Recommend的高度,则重置距离为-Recommend的高度
translationY = -child.height
}
if(target.top>navigationHeight+child.height){//如果滑动到了顶部,把MaskRecomend隐藏起来
translationY = -child.height
}
child.translationY = translationY.toFloat()//移动MaskRecommend
}
}
如代码所示,不再计算滑动方向,限制移动的范围:-child.height~0,如果滑动到顶部时,translationY = -child.height
,使MaskRecommendView隐藏起来。
- 为什么要计算滑动的方向?
因为要实现snap自动滑动的效果,需要先判断滑动方向,根据滑动的方向自动滑动固定的距离。
- 左右滑动时,同样会触发y的移动,不断累积,会上下自动滑动。
- 向上或向下缓慢滑动,会来回触发上下自动滑动。
- 之前的计算方法是,先判断滑动方向,然后移动相应的距离,实际上不用判断方向
嵌套滑动的流程
嵌套滑动可以分为两种情况
- 一种是AppBarLayout的滑动触发RecycleView滑动
- 一种是RecycleView的滑动触发AppBarLayout滑动
第一种情况
image.png如图所示是AppBarLayout的滑动引起RecycleView的滑动时序图。
- 从CoordinatorLayout开始事件被拦截。在OnInterceptTouchEvent方法中从最顶层开始遍历,如果第一个子View的Behavior中onInterceptTouchEvent返回true,则事件被CoordinatorLayout拦截,onTouchEvent方法开始执行。
- 在CoordinatorLayout中的onTouchEvent方法,调用子View的Behavior的onTouchEvent方法。在MotionEvent的ActionMove时调用scroll方法
- scroll方法最终会调用Behavior的setHeaderTopBottomOffset方法,该方法最终会改变AppBarLayout的top和bottom位置,实现AppBarLayout的滑动
- AppBarLayout滑动时,会触发Coordinatorlayout的onChildViewChange方法,该方法会触发RecycleView的Behavior中的onDependViewChange方法,进而改变RecycleView的位置
第二种情况
image.png如图所示,RecycleView滑动引起的AppBarLayout滑动的时序图
- RecycleView中的onInterceptTouchEvent方法拦截了事件,最终在onTouchEvent方法中处理滑动事件
- 当RecycleView的onTouchEvent方法ActionDown时调用startNestedScroll方法,最终通过CoordinatorLayout方法调用了AppBarLayout.Behavior的onStartNestedScroll和onNestedScrollAccept方法
- 当RecycleView的onTouchEvent方法中ActionMove时调用dispatchNestedPreScroll方法和dispatchNestedScroll方法,两个方法最终都会调用到AppBarLayoutBehavior中的setHeaderTopBottomOffset方法,最终改变AppBarLayout的位置
- 当RecycleView的onTouchEvent方法中ActionUp时调用stopNestedScroll停止滑动
AppBarLayout滑动的距离
无论是第一种情况还是第二种情况,AppBarLayout的滑动都是通过调用Behavior中的scroll方法然后是setHeaderTopBottomOffset方法。
onTouchEvent方法中的调用
//1.dy>0上滑动,dy<0下滑动。
//2.getMaxDragOffset(child),得到-AppBarLayout的高度,最大值最小值是-appBarHeight~0
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_MOVE: {
final int y = (int) ev.getY(activePointerIndex);
int dy = mLastMotionY - y;
if (mIsBeingDragged) {
mLastMotionY = y;
// We're being dragged so scroll the ABL
scroll(parent, child, dy, getMaxDragOffset(child), 0);
}
break;
}
}
return true;
}
//1. getTopBottomOffsetForScrollingSibling是getTopBottom,是滑动之前AppBarlayout的滑动出的高度,最小是-appBarLayoutHeight
//2.下滑动时,dy<0,getTopBottomOffsetForScrollingSibling() - dy变大,整体向下滑动
//3. 上滑动时,dy>0,getTopBottomOffsetForScrollingSibling() - dy变小,整体向上滑动
//4. getTopBottom,scrollFlag为什么,得到的都是appBarLayout的高度,实际设置setTopBottom方法会做调整。
final int scroll(CoordinatorLayout coordinatorLayout, V header,
int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(coordinatorLayout, header,
getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
}
//真正改变appBarLayout位置的方法
private void updateOffsets() {
ViewCompat.offsetTopAndBottom(this.view, this.offsetTop - (this.view.getTop() - this.layoutTop));
ViewCompat.offsetLeftAndRight(this.view, this.offsetLeft - (this.view.getLeft() - this.layoutLeft));
}
onNesedPreScroll方法的处理
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, T child, View target, int dx, int dy, int[] consumed, int type) {
if (dy != 0) {
int min;
int max;
if (dy < 0) {
min = -child.getTotalScrollRange();//appBarLayout总共能向上滑动的高度,如果是exitUntilCollapsed,则会减去最小高度。
max = min + child.getDownNestedPreScrollRange();//向下滑动的高度,exitUntilCollapsed,则是appbarHeight-minHeight。如果是enterAlways|enterAlwaysCollapsed,则是minHeight
} else {//向上滑动,距离是-appBarLayoutHeight~0或者exitUntilCollapsed时,-appBarLayoutHeight+minHeight~0
min = -child.getUpNestedPreScrollRange();//和getTotalScrollRange相同
max = 0;
}
if (min != max) {
//1.appBarLayout的消耗,>0时向上,<0时向上。appBarLayout不动时为0。
//2.appBarLayout在顶部出现向下滑动出来时时为0。向上滑动时>0。
consumed[1] = this.scroll(coordinatorLayout, child, dy, min, max);
}
}
}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, T child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (dyUnconsumed < 0) {
//.appBarLayout在顶部出现向下滑动出来时时<0。向上滑动时不调用。
this.scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
}
}
setHeaderTopBotttom
int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout, T appBarLayout, int newOffset, int minOffset, int maxOffset) {
int curOffset = this.getTopBottomOffsetForScrollingSibling();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
boolean offsetChanged = this.setTopAndBottomOffset(newOffset);
consumed = curOffset - newOffset;
this.offsetDelta = newOffset - interpolatedOffset;
appBarLayout.dispatchOffsetUpdates(this.getTopAndBottomOffset());
this.updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset, newOffset < curOffset ? -1 : 1, false);//更新appBarLayout中drawable的状态,停止动画,跳到当前的状态,press,nomal等
}
} else {
this.offsetDelta = 0;
}
return consumed;
}
最终调用setTopAndBottomOffse完成appBarLayout位置改变
问答
1.onNestedPreScroll和onNestedScroll中消耗的距离是什么意思?
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed, int type) {
//consumed[1]是AppBarLayout消费掉的距离。向下>0,向上<0
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
int type) {
//dyConsumed,recycleView的消耗,dyUnConsume,recycleView没有消耗的,appBaralayout的消耗的。
}
2.重写AppBarLayout.Behavior来改变AppBarLayout的滑动效果
无论是更改appBarLayout的top,bottom,translationy,都会顶部下滑无法处理的问题。
image.png
如图所示,继续下滑动时,由于已经改动了appBarlayout的translation位移,需要另外相同的一个View来补位才能正常显示。
网友评论