天天看点

Android 嵌套滑动机制(NestedScrolling)

​Lollipop​

​版本之后,为了更好的用户体验,Google为Android的滑动机制提供了​

​NestedScrolling​

​特性​

​NestedScrolling​

​的特性可以体现在哪里呢?

比如你使用了​

​Toolbar​

​,下面一个​

​ScrollView​

​,向上滚动隐藏​

​Toolbar​

​,向下滚动显示​

​Toolbar​

​,这里在逻辑上就是一个​

​NestedScrolling​

​ —— 因为你在滚动整个​

​Toolbar​

​在内的View的过程中,又​

​嵌套​

​滚动了里面的​

​ScrollView​

​。

Android 嵌套滑动机制(NestedScrolling)

效果如上图【别嫌弃我】

​Android​

​对​

​Touch​

​事件的分发是有自己一套机制的。主要是有是三个函数:

​dispatchTouchEvent​

​、​

​onInterceptTouchEvent​

​和​

​onTouchEvent​

​。

这种分发机制有一个漏洞:

如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。

也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。

​NestedScrolling​

​机制就很好的解决了这个问题。

我们看看如何实现这个​

​NestedScrolling​

​,首先有几个类(接口)我们需要关注一下

​​NestedScrollingChild​​​​NestedScrollingParent​​​​NestedScrollingChildHelper​​​​NestedScrollingParentHelper​​

​support-v4​

​包中提供,Lollipop的View默认实现了几种方法。

实现接口很简单,这边我暂时用到了​

​NestedScrollingChild​

​系列的方法(因为Parent是support-design提供的​

​CoordinatorLayout​

​)

@Override
    public void setNestedScrollingEnabled(boolean{
        super.setNestedScrollingEnabled(enabled);
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled(){
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int{
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll(){
        mChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent(){
        return mChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow){
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow){
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean{
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float{
        return      

对,简单的话你就这么实现就好了。

​startNestedScroll​

​方法

/**
     * Start a new nested scroll for this view.
     *
     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
     * method/{@link NestedScrollingChild} interface method with the same signature to implement
     * the standard policy.</p>
     *
     * @param axes Supported nested scroll axes.
     *             See {@link NestedScrollingChild#startNestedScroll(int)}.
     * @return
    public boolean startNestedScroll(int{
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }      

​NestedScrollingParent​

​交互的一些方法。

​ViewParentCompat​

​是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了​

​NestedScrollingParent​

​接口,去调用接口的方法。

那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:

一、startNestedScroll

首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent

二、dispatchNestedPreScroll

​onInterceptTouchEvent​

​或者​

​onTouch​

​中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数​

​offsetInWindow​

​计算偏移量,才能保证下一次的touch事件的计算是正确的。

如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。

这个函数一般在子view处理scroll前调用。

三、dispatchNestedScroll

向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。

如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。

这个函数一般在子view处理scroll后调用。

四、stopNestedScroll

结束整个流程。

整个对应流程是这样

子view 父view
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);