美文网首页
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