Android嵌套滑动讲解

在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嵌套滑动讲解)