Android嵌套滑动讲解

作者: kisass | 来源:发表于2019-03-24 15:06 被阅读10次

    在Android的事件分发机制里面,当一个View决定消耗事件流时,其它的View就不能再处理这个事件流的了,所以对于有嵌套滑动的地方就要用到NestedScrollingParent和NestedScrollingChild。最新的是NestedScrollingParent2和NestedScrollingChild2是在NestedScrollingParent和NestedScrollingChild的基础上扩展的,相对于旧版本他们对嵌套监听提供了触摸类型的区分,使得fling也可以进行嵌套滚动。要想实现嵌套滑动这两个接口必须成对出现。

    组成

    已知实现了NestedScrollingParent的ViewGroup有NestedScrollView、CoordinatorLayout、SwipeRefreshLayout等。已知实现了NestedScrollingChild的接口有BaseGridView、HorizontalGridView、NestedScrollView、RecyclerView、SwipeRefreshLayout、VerticalGridView。所以NestedScrollView、SwipeRefreshLayout既实现类Parent接口也实现类Child接口。

    先了解一下Parent和Child接口的组成

    
    public interface NestedScrollingParent2 extends NestedScrollingParent {
        /**
         * 这个是嵌套滑动控制事件分发的控制方法,只有返回true才能接收到事件分发
         * @param child 包含target的ViewParent的直接子View
         * @param target 发起滑动事件的View
         * @param axes 滑动的方向,数值和水平方向{@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
         *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} 
         * @return true 表示父View接受嵌套滑动监听,否则不接受
         */
        boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,@NestedScrollType int type);
    
        /**
         * 这个方法在onStartNestedScroll返回true之后在正式滑动之前回调
         * @param child 包含target的父View的直接子View
         * @param target 发起嵌套滑动的View
         * @param axes 滑动的方向,数值和水平方向{@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
         *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
         */
        void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,@NestedScrollType int type);
    
        /**
         *
         * @param target View that initiated the nested scroll
         */
        void onStopNestedScroll(@NonNull View target);
    
        /**
         * 在子View滑动过程中会分发这个嵌套滑动的方法,要想这里收到嵌套滑动事件必须在onStartNestedScroll返回true
         * @param dxConsumed 子View在水平方向已经消耗的距离
         * @param dyConsumed 子View在垂直方法已经消耗的距离
         * @param dxUnconsumed 子View在水平方向剩下的未消耗的距离
         * @param dyUnconsumed 子View在垂直方法剩下的未消耗的距离
         * @param type 发起嵌套事件的类型 分为触摸(ViewParent.TYPE_TOUCH)和非触摸(ViewParent.TYPE_NON_TOUCH)
         */
        void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
    
        /**
         * 在子View开始滑动之前让父View有机会先进行滑动处理
         * @param dx 水平方向将要滑动的距离
         * @param dy 竖直方向将要滑动的距离
         * @param consumed Output. 父View在水平和垂直方向要消费的距离,consumed[0]表示水平方向的消耗,consumed[1]表示垂直方向的消耗,
         */
        void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type);
    
    }
    
    

    以上就是NestedScrollingParent2主要的方法介绍,下面看看NestedScrollingChild2的方法

    public interface NestedScrollingChild2 extends NestedScrollingChild {
    
        //返回值true表示找到了嵌套交互的ViewParent,type表示引起滑动事件的类型,这个事件和parent中的onStartNestedScroll是对应的
        boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
    
        
        //停止嵌套滑动的回调
        void stopNestedScroll(@NestedScrollType int type);
    
        //表示有实现了NestedScrollingParent2接口的父类
        boolean hasNestedScrollingParent(@NestedScrollType int type);
    
        //分发嵌套滑动事件的过程
        boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
                @NestedScrollType int type);
    
        //在嵌套滑动之前分发事件
        boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
                @Nullable int[] offsetInWindow, @NestedScrollType int type);
    }
    
    

    嵌套滑动的原理

    下面给出的是NestedScrollView中的onTouchEvent方法的源码,整个嵌套滑动事件和View的事件分发是结合在一起的,相对于在原来view的事件分发里面加了滑动回调给父类,并且把滑动的距离算出来。这样一看就很清晰了。

    @Override
        public boolean onTouchEvent(MotionEvent ev) {
            
            ...
    
            switch (actionMasked) {
                case MotionEvent.ACTION_DOWN: {
                    ...
                    //嵌套滑动的原理是滑动事件先从子View开始,在子View接收到ACTION_DOWN事件的时候开始寻找是否有嵌套滑动的父类并且回调onStartNestedScroll方法
                    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                    break;
                }
                case MotionEvent.ACTION_MOVE:
                
                    ...
                    //在ACTION_MOVE开始时先分发嵌套滑动之前的事件,最后回调onNestedPreScroll方法
                    if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {
                        deltaY -= mScrollConsumed[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    }
                    
                    if (mIsBeingDragged) {
                       
                        ...
                        //滑动过程中回调onNestedScroll方法
                        if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                                ViewCompat.TYPE_TOUCH)) {
                            mLastMotionY -= mScrollOffset[1];
                            vtev.offsetLocation(0, mScrollOffset[1]);
                            mNestedYOffset += mScrollOffset[1];
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    ...
                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                        //松开手回调fling类型的onNestedScroll方法
                        flingWithNestedDispatch(-initialVelocity);
                    }
                    //回调stopNestedScroll方法
                    endDrag();
                    break;
                case MotionEvent.ACTION_CANCEL:
                    ...
                     //回调stopNestedScroll方法
                    endDrag();
                    break;
            
            }
    
            ...
            vtev.recycle();
            return true;
        }
    

    NestedScrollingChildHelper和NestedScrollingParentHelper

    这两个是实际处理嵌套逻辑的代理类,谷歌把嵌套滑动的逻辑已经封装在里面,在需要的地方实现类逻辑的复用,这样设计的好处是避免和onTouchEvent里面事件处理逻辑的耦合,让逻辑更加清晰,调用方便,并且helper里面的方法和接口的方法是一一对应方法名相同的。这种设计模式值得我们在代码里面学习和使用。我们可以看看子View是怎么样找到有嵌套监听等父类的,我们以onTouchEvent里面startNestedScroll方法,最终来到了NestedScrollingChildHelper里面的startNestedScroll方法

        public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
            //已经在嵌套滑动过程中
            if (hasNestedScrollingParent(type)) {
                // Already in progress
                return true;
            }
            //首先检查是否可以嵌套滑动,因为像recyclerview中是可以关闭嵌套滑动的
            if (isNestedScrollingEnabled()) {
                ViewParent p = mView.getParent();
                View child = mView;
                //遍历寻找实现了NestedScrollingParent接口的父类
                while (p != null) {
                    //调用父类的onStartNestedScroll方法,如果返回ture则告诉父类的onNestedScrollAccepted方法已经已经收到了父类的请求
                    if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                        setNestedScrollingParentForType(type, p);
                        ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                        return true;
                    }
                    if (p instanceof View) {
                        child = (View) p;
                    }
                    p = p.getParent();
                }
            }
            return false;
        }
    

    对NestedScrollingParent2和NestedScrollingParent进行区分

        public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes, int type) {
            if (parent instanceof NestedScrollingParent2) {
                // First try the NestedScrollingParent2 API
                return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes, type);
            } else if (type == ViewCompat.TYPE_TOUCH) {
                // Else if the type is the default (touch), try the NestedScrollingParent API
                return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
            }
            return false;
        }
    

    实例

    这里截取了知乎首页内容滚动和tab联动的例子,当然这个实现的方法有很多,这里用嵌套滑动的方法去实现。逻辑是内容部分是一个实现了NestedScrollingChild2的RecyclerView,当然这部分不用我们去做,RecyclerView本来就实现了,父类是一个实现了NestedScrollingParent2接口的ViewGroup,这里可以用FrameLayout。当RecyclerView滚动时,通知FrameLayout的实现下方Tab栏的滚动逻辑,就这么简单。


    image

    首先自定义ViewGroup

    public class NestedFrameLayout extends FrameLayout implements NestedScrollingParent2 {
        public NestedFrameLayout(Context context) {
            super(context);
        }
    
        public NestedFrameLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public NestedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean onStartNestedScroll(@NonNull View view, @NonNull View view1, int i, int i1) {
            return true;
        }
    
        @Override
        public void onNestedScrollAccepted(@NonNull View view, @NonNull View view1, int i, int i1) {
    
        }
    
        @Override
        public void onStopNestedScroll(@NonNull View view, int i) {
    
        }
    
        @Override
        public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
    
            if (mListener != null) {
                if (Math.abs(dyConsumed)>5){
                    if (dyConsumed>0){
                        mListener.onScroll(true);
                    }else {
                        mListener.onScroll(false);
                    }
                }
            }
        }
    
        @Override
        public void onNestedPreScroll(@NonNull View view, int i, int i1, @NonNull int[] ints, int i2) {
    
        }
    
        public Listener mListener;
    
        public void setListener(Listener listener) {
            mListener = listener;
        }
    
        public interface Listener {
            void onScroll(boolean isScrollUp);
        }
    
    }
    

    然后在MainActivity监听这个滑动事件和处理tab的联动,并且加上动画就看到了下图的效果了

      @Override
        public void onScroll(boolean isScrollUp) {
    
            if (isScrollUp) {
                hideBottomNavigationBar();
            } else {
                showBottomNavigationBar();
            }
        }
    
        private void showBottomNavigationBar() {
            if (!mIsNavigationBarHide) {
                animateOffset(mBottomTabLayout.getHeight());
                mIsNavigationBarHide = true;
            }
        }
    
        private void hideBottomNavigationBar() {
            animateOffset(0);
            mIsNavigationBarHide = false;
        }
    
        private void animateOffset(final int offset) {
            if (mTranslationAnimator == null) {
                mTranslationAnimator = ViewCompat.animate(mBottomTabLayout);
                mTranslationAnimator.setDuration(300);
                mTranslationAnimator.setInterpolator(new LinearOutSlowInInterpolator());
                mTranslationAnimator.setUpdateListener(new ViewPropertyAnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(View view) {
    
                    }
                });
            } else {
                mTranslationAnimator.cancel();
            }
            mTranslationAnimator.translationY(offset).start();
        }
    

    这里面就可以做到上方无论切到哪个tab滑动时,下方的tab都能联动,因为从上面NestedScrollingChildHelper的分析可知RecyclerView会遍历寻找想要监听嵌套滑动的ViewGroup。这样就做到了全局联动。


    image

    相关文章

      网友评论

        本文标题:Android嵌套滑动讲解

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