美文网首页
CoordinatorLayout嵌套滑动过程分析

CoordinatorLayout嵌套滑动过程分析

作者: Allenlll | 来源:发表于2019-12-17 19:36 被阅读0次

AppBarLayout和CoordinatorLayout

下图所示为CoordinatorLayout和AppBarLayout的关系图


image.png
  1. AppBarLayout是垂直方向的LinearLayout,和CoorinatorLayout配合使用,是CoordinatorLayout的子View。
  2. AppBarLayout本身不能滑动,要配合NestScrollView(通常是RecycleView)才可能滑动。NestScrollView需要设置AppBarLayout.ScrollingViewBehavior。AppBarLayout默认有一个AppBarLayout.Behavior
  3. AppBarLayout的子View通过app:layout_scrollFlags来区分是否能够滑动以及滑动的效果。参考:https://blog.csdn.net/eyishion/article/details/80282204
  4. 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自动滑动的效果,需要先判断滑动方向,根据滑动的方向自动滑动固定的距离。
  1. 左右滑动时,同样会触发y的移动,不断累积,会上下自动滑动。
  2. 向上或向下缓慢滑动,会来回触发上下自动滑动。
  3. 之前的计算方法是,先判断滑动方向,然后移动相应的距离,实际上不用判断方向

嵌套滑动的流程

嵌套滑动可以分为两种情况

  • 一种是AppBarLayout的滑动触发RecycleView滑动
  • 一种是RecycleView的滑动触发AppBarLayout滑动
第一种情况
image.png

如图所示是AppBarLayout的滑动引起RecycleView的滑动时序图。

  1. 从CoordinatorLayout开始事件被拦截。在OnInterceptTouchEvent方法中从最顶层开始遍历,如果第一个子View的Behavior中onInterceptTouchEvent返回true,则事件被CoordinatorLayout拦截,onTouchEvent方法开始执行。
  2. 在CoordinatorLayout中的onTouchEvent方法,调用子View的Behavior的onTouchEvent方法。在MotionEvent的ActionMove时调用scroll方法
  3. scroll方法最终会调用Behavior的setHeaderTopBottomOffset方法,该方法最终会改变AppBarLayout的top和bottom位置,实现AppBarLayout的滑动
  4. AppBarLayout滑动时,会触发Coordinatorlayout的onChildViewChange方法,该方法会触发RecycleView的Behavior中的onDependViewChange方法,进而改变RecycleView的位置
第二种情况
image.png

如图所示,RecycleView滑动引起的AppBarLayout滑动的时序图

  1. RecycleView中的onInterceptTouchEvent方法拦截了事件,最终在onTouchEvent方法中处理滑动事件
  2. 当RecycleView的onTouchEvent方法ActionDown时调用startNestedScroll方法,最终通过CoordinatorLayout方法调用了AppBarLayout.Behavior的onStartNestedScroll和onNestedScrollAccept方法
  3. 当RecycleView的onTouchEvent方法中ActionMove时调用dispatchNestedPreScroll方法和dispatchNestedScroll方法,两个方法最终都会调用到AppBarLayoutBehavior中的setHeaderTopBottomOffset方法,最终改变AppBarLayout的位置
  4. 当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来补位才能正常显示。

相关文章

网友评论

      本文标题:CoordinatorLayout嵌套滑动过程分析

      本文链接:https://www.haomeiwen.com/subject/jjsenctx.html