自定义View事件之进阶篇(一)-NestedScrolling(嵌套滑动)机制

最近一段时间,一直都在忙于找工做。虽然花费了三个月的时间,可是结果并非很美满。想去大厂、想去好公司、想碰见更厉害的人的愿望仍是没有实现。或许是本身不够强大,或许本身不够努力,或许须要必定运气。生活老是须要经历一些波折。没有谁老是能一路顺风。接下来一段时间内,会继续更新文章。但愿你们能继续关注。Thanks~java

前言

在Lollipop(Android 5.0)时,谷歌推出了NestedScrolling机制,也就是嵌套滑动。本文将带领你们一块儿去了解谷歌对该机制的设计。经过阅读该文,你能了解以下知识点:git

  • 传统事件分发机制中嵌套滑动的实现与局限性。
  • 谷歌NestedScrolling机制的原理实现。
  • NestedScrollingChild与NestedScrollingParent接口的调用关系。
  • NestedScrollingChild2与NestedScrollingParent2接口出现的意义。

该博客中涉及到的示例,在NestedScrollingDemo项目中都有实现,你们能够按需自取。github

传统事件机制处理嵌套滑动的局限性

在传统的事件分发机制中,当一个事件产生后,它的传递过程遵循以下顺序:父控件->子控件,事件老是先传递给父控件,当父控件不对事件拦截的时候,那么当前事件又会传递给它的子控件。一旦父控件须要拦截事件,那么子控件是没有机会接受该事件的。算法

所以当在传统事件分发机制中,若是有嵌套滑动场景,咱们须要手动解决事件冲突。具体嵌套滑动例子以下图所示:数组

例子分析

上述效果实现,请参看NestedTraditionLayout.javamarkdown

想要实现上图效果,在传统滑动机制中,咱们须要如下几个步骤:app

  • 咱们须要调用父控件中onInterceptTouchEvent方法来拦截向上滑动。
  • 当父控件拦截事件后,须要控制自身的onTouchEvent处理滑动事件,使其滑动至HeaderView隐藏。
  • 当HeaderView滑动至隐藏后,父控件就不拦截事件了,而是交给内部的子控件(RecyclerView或ListView)处理滑动事件。

使用传统的事件拦截机制来处理嵌套滑动,咱们会发现一个问题,就是整个嵌套滑动是不连贯的。也就是当父控件滑动至HeaderView隐藏的时候,这个时候若是想要内部的(RecyclerView或ListView)处理滑动事件。只有抬起手指,从新向上滑动。ide

熟悉事件分发机制的朋友应该知道,之因此产生不连贯的缘由,是由于父控件拦截了事件,因此同一事件序列的事件,仍然会传递给父控件,也就会调用其onTouchEvent方法。而不是调用子控件的onTouchEvent方法。函数

NestedScrolling机制简介

为了实现连贯的嵌套滑动,谷歌在Lollipop(Android 5.0)时,推出了NestedScrolling机制。该机制并无脱离传统的事件分发机制,而是在原有的事件分发机制之上,为系统的自带的ViewGroup和View都增长了手势滑动与处理fling的方法。同时为了兼容低版本(5.0如下,View与ViewGroup是没有对应的API),谷歌也在support v4包中也提供了以下类与接口进行支撑:oop

父控件须要实现的接口与使用到的类:

  • NestedScrollingParent(接口)
  • NestedScrollingParent2(也是接口并继承NestedScrollingParent)
  • NestedScrollingParentHelper(类)

子控件须要实现的接口与使用到的类:

  • NestedScrollingChild(接口)
  • NestedScrollingChild2(也是接口并继承NestedScrollingChild)
  • NestedScrollingChildHelper(类)

须要注意的是,若是你的Android平台在5.0以上,那么你能够直接使用系统ViewGoup与View自带的方法。可是为了向下兼容,建议仍是使用support v4包提供的相应接口来实现嵌套滑动。下文也会着重讲解这些接口的的使用方式与方法说明。

NestedScrollingParent与NestedScrollingChild接口介绍

在了解嵌套滑动具体的使用方式以前,咱们须要了解父控件与子控件对应接口中方法的说明。这里你们能够先忽略掉NestedScrollingParent2与NestedScrollingChild2接口,由于这两个接口是为了解决以前对嵌套滑动处理fling效果的Bug。因此对于目前阶段的咱们只须要了解基础的嵌套滑动规则就够了。关于NestedScrollingParent2与NestedScrollingChild2接口相关的知识点,会在下文具体描述。那如今咱们就先看看基础的接口的方法介绍吧。

NestedScrollingParent

若是采用接口的方式实现嵌套滑动,咱们须要父控件要实现NestedScrollingParent接口。接口具体方法以下:

/** * 有嵌套滑动到来了,判断父控件是否接受嵌套滑动 * * @param child 嵌套滑动对应的父类的子类(由于嵌套滑动对于的父控件不必定是一级就能找到的,可能挑了两级父控件的父控件,child的辈分>=target) * @param target 具体嵌套滑动的那个子类 * @param nestedScrollAxes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定 * @return 父控件是否接受嵌套滑动, 只有接受了才会执行剩下的嵌套滑动方法 */
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {}

    /** * 当onStartNestedScroll返回为true时,也就是父控件接受嵌套滑动时,该方法才会调用 */
    public void onNestedScrollAccepted(View child, View target, int axes) {}

    /** * 在嵌套滑动的子控件未滑动以前,判断父控件是否优先与子控件处理(也就是父控件能够先消耗,而后给子控件消耗) * * @param target 具体嵌套滑动的那个子类 * @param dx 水平方向嵌套滑动的子控件想要变化的距离 dx<0 向右滑动 dx>0 向左滑动 * @param dy 垂直方向嵌套滑动的子控件想要变化的距离 dy<0 向下滑动 dy>0 向上滑动 * @param consumed 这个参数要咱们在实现这个函数的时候指定,回头告诉子控件当前父控件消耗的距离 * consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子控件作出相应的调整 */
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {}

    /** * 嵌套滑动的子控件在滑动以后,判断父控件是否继续处理(也就是父消耗必定距离后,子再消耗,最后判断父消耗不) * * @param target 具体嵌套滑动的那个子类 * @param dxConsumed 水平方向嵌套滑动的子控件滑动的距离(消耗的距离) * @param dyConsumed 垂直方向嵌套滑动的子控件滑动的距离(消耗的距离) * @param dxUnconsumed 水平方向嵌套滑动的子控件未滑动的距离(未消耗的距离) * @param dyUnconsumed 垂直方向嵌套滑动的子控件未滑动的距离(未消耗的距离) */
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}

    /** * 嵌套滑动结束 */
    public void onStopNestedScroll(View child) {}

    /** * 当子控件产生fling滑动时,判断父控件是否处拦截fling,若是父控件处理了fling,那子控件就没有办法处理fling了。 * * @param target 具体嵌套滑动的那个子类 * @param velocityX 水平方向上的速度 velocityX > 0 向左滑动,反之向右滑动 * @param velocityY 竖直方向上的速度 velocityY > 0 向上滑动,反之向下滑动 * @return 父控件是否拦截该fling */
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {}


    /** * 当父控件不拦截该fling,那么子控件会将fling传入父控件 * * @param target 具体嵌套滑动的那个子类 * @param velocityX 水平方向上的速度 velocityX > 0 向左滑动,反之向右滑动 * @param velocityY 竖直方向上的速度 velocityY > 0 向上滑动,反之向下滑动 * @param consumed 子控件是否能够消耗该fling,也能够说是子控件是否消耗掉了该fling * @return 父控件是否消耗了该fling */
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {}

    /** * 返回当前父控件嵌套滑动的方向,分为水平方向与,垂直方法,或者不变 */
    public int getNestedScrollAxes() {}
复制代码

NestedScrollingChild接口介绍

若是采用接口的方式实现嵌套滑动,子控件须要实现NestedScrollingChild接口。接口具体方法以下:

/** * 开启一个嵌套滑动 * * @param axes 支持的嵌套滑动方法,分为水平方向,竖直方向,或不指定 * @return 若是返回true, 表示当前子控件已经找了一块儿嵌套滑动的view */
    public boolean startNestedScroll(int axes) {}

    /** * 在子控件滑动前,将事件分发给父控件,由父控件判断消耗多少 * * @param dx 水平方向嵌套滑动的子控件想要变化的距离 dx<0 向右滑动 dx>0 向左滑动 * @param dy 垂直方向嵌套滑动的子控件想要变化的距离 dy<0 向下滑动 dy>0 向上滑动 * @param consumed 子控件传给父控件数组,用于存储父控件水平与竖直方向上消耗的距离,consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 * @param offsetInWindow 子控件在当前window的偏移量 * @return 若是返回true, 表示父控件已经消耗了 */
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {}


    /** * 当父控件消耗事件后,子控件处理后,又继续将事件分发给父控件,由父控件判断是否消耗剩下的距离。 * * @param dxConsumed 水平方向嵌套滑动的子控件滑动的距离(消耗的距离) * @param dyConsumed 垂直方向嵌套滑动的子控件滑动的距离(消耗的距离) * @param dxUnconsumed 水平方向嵌套滑动的子控件未滑动的距离(未消耗的距离) * @param dyUnconsumed 垂直方向嵌套滑动的子控件未滑动的距离(未消耗的距离) * @param offsetInWindow 子控件在当前window的偏移量 * @return 若是返回true, 表示父控件又继续消耗了 */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {}

    /** * 子控件中止嵌套滑动 */
    public void stopNestedScroll() {}


    /** * 当子控件产生fling滑动时,判断父控件是否处拦截fling,若是父控件处理了fling,那子控件就没有办法处理fling了。 * * @param velocityX 水平方向上的速度 velocityX > 0 向左滑动,反之向右滑动 * @param velocityY 竖直方向上的速度 velocityY > 0 向上滑动,反之向下滑动 * @return 若是返回true, 表示父控件拦截了fling */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {}

    /** * 当父控件不拦截子控件的fling,那么子控件会调用该方法将fling,传给父控件进行处理 * * @param velocityX 水平方向上的速度 velocityX > 0 向左滑动,反之向右滑动 * @param velocityY 竖直方向上的速度 velocityY > 0 向上滑动,反之向下滑动 * @param consumed 子控件是否能够消耗该fling,也能够说是子控件是否消耗掉了该fling * @return 父控件是否消耗了该fling */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {}

    /** * 设置当前子控件是否支持嵌套滑动,若是不支持,那么父控件是不可以响应嵌套滑动的 * * @param enabled true 支持 */
    public void setNestedScrollingEnabled(boolean enabled) {}

    /** * 当前子控件是否支持嵌套滑动 */
    public boolean isNestedScrollingEnabled() {}

    /** * 判断当前子控件是否拥有嵌套滑动的父控件 */
    public boolean hasNestedScrollingParent() {}
复制代码

谷歌嵌套滑动的方法调用设计

经过上文,我相信你们大概基本了解了NestedScrollingParent与NestedScrollingChild两个接口方法的做用,可是咱们并不知道这些方法之间对应的关系与调用的时机。那么如今咱们一块儿来分析谷歌对整个嵌套滑动过程的实现与设计。为了处理嵌套滑动,谷歌将整个过程分为了如下几个步骤:

  • 1.当父控件不拦截事件,子控件收到滑动事件后,会先询问父控件是否支持嵌套滑动。
  • 2.若是父控件支持嵌套滑动,那么父控件进行预先滑动。而后将处理剩余的距离交由给子控件处理。
  • 3.子控件收到父控件剩余的滑动距离并滑动结束后,若是滑动距离还有剩余,又会再问一下父控件是否须要再继续消耗剩下的距离。
  • 4.若是子控件产生了fling,会先询问父控件是否预先拦截fling。若是父控件预先拦截。则交由给父控件处理。子控件则不处理fling
  • 5.若是父控件不预先拦截fling, 那么会将fling传给父控件处理。同时子控件也会处理fling。
  • 6.当整个嵌套滑动结束时,子控件通知父控件嵌套滑动结束。

对fling效果不熟悉的小伙伴能够查看该篇文章---RecyclerView之Scroll和Fling

再结合以前咱们对NestedScrollingParent与NestedScrollingChild中的方法。咱们能够获得相应方法之间的调用关系。具体以下图所示:

方法对应关系

子控件方法调用时机

当咱们了解了接口的调用关系后,咱们须要知道子控件对相应嵌套滑动方法的调用时机。由于在低版本下,子控件向父控件传递事件须要配合NestedScrollingChildHelper类与NestedScrollingChild接口一块儿使用。因为篇幅的限制。这里就不向你们介绍如何构造一个支持嵌套滑动的子控件了。在接下来的知识点中都会在NestedScrollingChildView 的基础上进行讲解。但愿你们能够结合代码与博客一块儿理解。

在接下来的章节中,会先讲解谷歌在NestedScrollingParent与NestedScrollingChild接口下嵌套滑动的API设计。关于NestedScrollingParent2与NestedScrollingChild2接口会单独进行解释。

子控件startNestedScroll方法调用时机

根据嵌套滑动的机制设定,子控件若是想要将事件传递给父控件,那么父控件是不能拦截事件的。当子控件想要将事件交给父控件进行预处理,那么必然会在其onTouchEvent方法,将事件传递给父控件。须要注意的是当子控件调用startNestedScroll方法时,只是判断是否有支持嵌套滑动的父控件,并通知父控件嵌套滑动开始。这个时候并无真正的传递相应的事件。故该方法只能在子控件的onTouchEvent方法中事件为MotionEvent.ACTION_DOWN时调用。伪代码以下所示:

public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = x;
                mLastY = y;
                //查找嵌套滑动的父控件,并通知父控件嵌套滑动开始。这里默认是设置的竖直方向
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            }
        }
        return super.onTouchEvent(event);
    }
复制代码

那子view仅仅经过startNestedScroll方法是如何找到父控件并通知父控件嵌套滑动开始的呢?咱们来看看startNestedScroll方法的具体实现,startNestedScroll方法内部会调用NestedScrollingChildHelper的startNestedScroll方法。具体代码以下所示:

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {//判断子控件是否支持嵌套滑动
            //获取当前的view的父控件
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                 //判断当前父控件是否支持嵌套滑动
                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;
    }
复制代码

从代码中咱们能够看出,当子控件支持嵌套滑动时,子控件会获取当前父控件,并调用ViewParentCompat.onStartNestedScroll方法。咱们继续查看该方法:

public static boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {//判断父控件是否实现NestedScrollingParent2
            // First try the NestedScrollingParent2 API
            return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {//若是父控件实现NestedScrollingParent
            // Else if the type is the default (touch), try the NestedScrollingParent API
            return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
        }
        return false;
    }
复制代码

观察代码,咱们能够发现,当父控件实现NestedScrollingParent接口后,会走IMPL.onStartNestedScroll方法,咱们继续跟下去:

public boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
            return false;
        }
复制代码

最后会调用ViewParetCompat中的onStartNestedScroll方法,该方法最终会调用父控件的onStartNestedScroll方法。绕了一大圈,也就调用了父控件的onStartNestedScroll来判断是否支持嵌套滑动。

那如今咱们再回到子控件的startNestedScroll方法中。咱们能够得知,若是当前父控件不支持嵌套滑动,那么会一直向上寻找,直到找到为止。若是仍然没有找到,那么接下来的子父控件的嵌套滑动方法都不会调用。若是子控件找到了支持嵌套滑动的父控件,那么接下来会调用父控件的onNestedScrollAccepted方法,表示父控件接受嵌套滑动。

子控件dispatchNestedPreScroll方法调用时机

当父控件接受嵌套滑动后,那么子控件须要将手势滑动传递给父控件,由于这里已经产生了滑动,故会在onTouchEvent中筛选MotionEvent.ACTION_MOVE中的事件,而后调用dispatchNestedPreScroll方法这些将滑动事件传递给父控件。伪代码以下所示:

private final int[] mScrollConsumed = new int[2];//记录父控件消耗的距离

    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                int dy = mLastY - y;
                int dx = mLastX - x;
                //将事件传递给父控件,并记录父控件消耗的距离。
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                }
            }
        }

        return super.onTouchEvent(event);
    }
复制代码

在上述代码中,dy与dx分别为子控件竖直与水平方向上的距离,int[] mScrollConsumed竖直用于记录父控件消耗的距离。那么当咱们调用dispatchNestedPreScroll的方法,将事件传递给父控件进行消耗时,那么子控件实际能处理的距离为:

  • 水平方向: dx -= mScrollConsumed[0];
  • 竖直方向: dy -= mScrollConsumed[1];

接下来,咱们继续查看dispatchNestedPreScroll的方法。

在dispatchNestedPreScroll方法内部会调用NestedScrollingChildHelper的dispatchNestedPreScroll方法具体代码以下:

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            //获取当前嵌套滑动的父控件,若是为null,直接返回
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                consumed[0] = 0;
                consumed[1] = 0;
                //调用父控件的onNestedPreScroll处理事件
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
复制代码

在该方法中,会先判断获取当前嵌套滑动的父控件。若是父控件不为null且支持嵌套滑动,那么接下来会调用ViewParentCompat.onNestedPreScroll()方法。代码以下所示:

public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed) {
            if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
            }
        }

复制代码

观察代码最终会调用父控件的onNestedPreScroll方法。须要注意的是,父控件可能会将子控件传递的滑动事件所有消耗。那么子控件就没有继续可处理的事件了。

onNestedPreScroll方法在嵌套滑动时判断父控件的滑动距离时尤其重要。

子控件dispatchNestedScroll方法调用时机

当父控件预先处理滑动事件后,也就是调用onNestedPreScroll方法并把消耗的距离传递给子控件后,子控件会获取剩下的事件并消耗。若是子控件仍然没有消耗完,那么会调用dispatchNestedScroll将剩下的事件传递给父控件。若是父控件不处理。那么又会传递给子控件进行处理。伪代码以下所示:

private final int[] mScrollConsumed = new int[2];//记录父控件消耗的距离

    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                int dy = mLastY - y;
                int dx = mLastX - x;
                //将事件传递给父控件,并记录父控件消耗的距离。
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    scrollNested(dx,dy);//处理嵌套滑动
                }
            }
        }

        return super.onTouchEvent(event);
    }
    //处理嵌套滑动
    private void scrollNested(int x, int y) {
        int unConsumedX = 0, unConsumedY = 0;
        int consumedX = 0, consumedY = 0;

        //子控件消耗多少事件,由本身决定
        if (x != 0) {
            consumedX = childConsumeX(x);
            unConsumedX = x - consumedX;
        }
        if (y != 0) {
            consumedY = childConsumeY(y);
            unConsumedY = y - consumedY;
        }

        //子控件处理事件
        childScroll(consumedX, consumedY);

        //子控件处理后,又将剩下的事件传递给父控件
        if (dispatchNestedScroll(consumedX, consumedY, unConsumedX, unConsumedY, mScrollOffset)) {
            //传给父控件处理后,剩下的逻辑本身实现
        }
        //传递给父控件,父控件不处理,那么子控件就继续处理。
        childScroll(unConsumedX, unConsumedY);

    }
    /** * 子控件滑动逻辑 */
    private void childScroll(int x, int y) {
        //子控件怎么滑动,本身实现
    }
    /** * 子控件水平方向消耗多少距离 */
    private int childConsumeX(int x) {
        //具体逻辑由本身实现
        return 0;
    }
    /** * 子控件竖直方向消耗距离 */
    private int childConsumeY(int y) {
        //具体逻辑由本身实现
        return 0;
    }
复制代码

在上述代码中,由于子控件消耗多少距离,是由子控件进行决定的,因此将这些方法抽象了出来了。在子控件的dispatchNestedScroll方法内部会调用NestedScrollingChildHelper的dispatchNestedScroll方法,具体代码以下所示:

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }
                //调用父控件的onNestedScroll方法。
                ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
复制代码

该方法内部会调用ViewParentCompat.onNestedScroll方法。继续跟踪最终会调用ViewParentCompat中非静态的的onNestedScroll方法,代码以下所示:

public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
            }
        }
复制代码

该方法中,最终会调用父控件的onNestedScroll方法来处理子控件剩余的距离。

子控件stopNestedScroll方法调用时机

当整个事件序列结束的时候(当手指抬起或取消滑动的时候),须要通知父控件嵌套滑动已经结束。故咱们须要在OnTouchEvent中筛选MotionEvent.ACTION_UP、MotionEvent.ACTION_CANCEL中的事件,并经过stopNestedScroll()方法通知父控件。伪代码以下所示:

public boolean onTouchEvent(MotionEvent event) {

        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_UP: {   //当手指抬起的时,结束事件传递
                stopNestedScroll();
                break;
            }
            case MotionEvent.ACTION_CANCEL: {   //当手指抬起的时,结束事件传递
                stopNestedScroll();
                break;
            }
        }
        return super.onTouchEvent(event);
    }
复制代码

在stopNestedScroll()方法中,最终会调用父控件的onStopNestedScroll()方法,这里就不作更多的分析了。

子控件fling分发时机

如今就剩下最后一个嵌套滑动的方法了!!!对!就是fling。在了解子控件对fling的处理过程以前,咱们先要知道fling表明什么样的效果。在Android系统下,手指在屏幕上滑动而后松手,控件中的内容会顺着惯性继续往手指滑动的方向继续滚动直到中止,这个过程叫作fling。也就是咱们须要在onTouchEvent方法中筛选MotionEvent.ACTION_UP的事件并获取须要的滑动速度。伪代码以下:

fling的中文意思为抛、扔、掷。

public boolean onTouchEvent(MotionEvent event) {
         //添加速度检测器,用于处理fling
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                int xvel = (int) mVelocityTracker.getXVelocity();
                int yvel = (int) mVelocityTracker.getYVelocity();  
             if (!dispatchNestedPreFling(velocityX, velocityY)) {
                boolean consumed = canScroll();
                //将fling效果传递给父控件
                dispatchNestedFling(velocityX, velocityY, consumed);
                 //而后子控件再处理fling
                childFling();//子控件本身实现怎么处理fling
                stopNestedScroll();//子控件通知父控件滚动结束
              }
              stopNestedScroll();//通知父控件结束滑动
                break;
            }
        }
        return super.onTouchEvent(event);
    }
复制代码

这里就不在对fling效果是怎么分发到父控件进行解释啦~~。必定要结合NestedScrollingChildView类进行理解。那么假设你们都看了源码,那么咱们能够获得以下几点:

  • 子控件dispatchNestedPreFling最终会调用父控件的onNestedPreFling方法。
  • 子控件的dispatchNestedFling最终会调用onNestedFling方法。
  • 若是父控件的拦截fling(也就是onNestedPreFling方法返回为true)。那么子控件是没有机会处理fling的。
  • 若是父控件拦截fling(也就是onNestedPreFling方法返回为false),则父控件会调用onNestedFling方法与子控件同时处理fling。
  • 当父控件与子控件同时处理fling时,子控件会当即调用stopNestedScroll方法通知父控件嵌套滑动结束。

NestedScrollingChild2与NestedScrollingParent2简介

最后一个知识点了,你们加油啊!!!!!!

在本文章前半部,咱们都是围绕NestedScrollingChild与NestedScrollingParent进行讲解。并无说起NestedScrollingChild2与NestedScrollingParent2接口。那这两个接口是处理什么的呢?这又要回到上文咱们提到的NestedScrollingChild处理fling时的流程了,在谷歌以前的NestedScrollingParent与NestedScrollingChild的API设计中。并无考虑以下问题:

  • 父控件根本不可能知道子控件是否fling结束。子控件只是在ACTION_UP中调用了stopNestedScroll方法。虽然通知了父控件结束嵌套滑动,可是子控件仍然可能处于fling中。
  • 子控件没有办法将部分fling传递给父控件。父控件必须处理整个fling。

而使用NestedScrollingChild2与NestedScrollingParent2这两个接口,子控件就能将fling传递给父控件,而且父控件处理了部分fling后,又能够将剩余的fling再传递给子控件。当子控件中止fling时,通知父控件fling结束了。这和咱们以前分析的嵌套滑动是否是很像呢?直接讲知识点,你们不是很好理解,看下面这个例子:

NestedScrollingParent效果展现

上述效果实现,请参看NestedScrollingParentLayout.java

在上面例子中是实现了NestedScrollingChild(NestedScrollView或RecyclerView等)与NestedScrollingParent接口的嵌套滑动,咱们能够明显的看出,当咱们手指快速向下滑动并抬起的时,子控件将fling分发给父控件,由于处理的距离不一样,这个时候父控件已经处理滑动并fling结束,而内部的子控件(RecyclerView或NestedScrollView还在滚动,这种给咱们的感受就很是不连贯,好像每一个控件在独自滑动。

在一样的滑动条件下,实现了NestedScrollingChild2(NestedScrollView或RecyclerView等)与NestedScrollingParent2接口的嵌套滑动.看下面的例子:

NestedScrollingParent2效果展现

上述效果实现,请参看NestedScrollingParent2Layout.java

观察上图,咱们能发现父控件与子控件(RecyclerView或NestedScrollView)的滑动更为顺畅与合理。那接下来咱们看看谷歌对其的设计。

NestedScrollingChild2与NestedScrollingParent2分别继承了NestedScrollingChild与NestedScrollingParent,在继承的接口部分方法上增长了type参数。其中type的取值为TYPE_TOUCH(0)TYPE_NON_TOUCH(1)。用于区分手势滑动与fling。具体差别以下图所示:

接口差别

图片较大,可能阅读不清晰,建议放大观看。

谷歌在fling的处理上也与以前的NestedScrollingChild与NestedScrollingParent有所差别,在onTouchEvent方法中的逻辑进行了修改,伪代码以下所示:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();

        int y = (int) event.getY();
        int x = (int) event.getX();

        //添加速度检测器,用于处理fling效果
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        switch (action) {
            case MotionEvent.ACTION_UP: {//当手指抬起的时,结束嵌套滑动传递,并判断是否产生了fling效果
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                int xvel = (int) mVelocityTracker.getXVelocity();
                int yvel = (int) mVelocityTracker.getYVelocity();
                fling(xvel, yvel);//具体处理fling的方法
                mVelocityTracker.clear();
                stopNestedScroll(ViewCompat.TYPE_TOUCH));//注意这里stop的是带了参数的
                break;
            }

        }
        return super.onTouchEvent(event);
    }
复制代码

当子控件手指抬起的时候,咱们发现是调用stopNestedScroll(ViewCompat.TYPE_TOUCH)的方式来通知父控件当前手势滑动已经结束,继续查看fling方法。伪代码以下所示:

private boolean fling(int velocityX, int velocityY) {
        //判断速度是否足够大。若是够大才执行fling
        if (Math.abs(velocityX) < mMinFlingVelocity) {
            velocityX = 0;
        }
        if (Math.abs(velocityY) < mMinFlingVelocity) {
            velocityY = 0;
        }
        if (velocityX == 0 && velocityY == 0) {
            return false;
        }
        if (dispatchNestedPreFling(velocityX, velocityY)) {
            boolean canScroll = canScroll();
            //将fling效果传递给父控件
            dispatchNestedFling(velocityX, velocityY, canScroll);

            //子控件在处理fling效果
            if (canScroll) {
                //通知父控件开始fling事件,
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                doFling(velocityX, velocityY);
                return true;
            }

        }
        return false;

    }
复制代码

从代码中,咱们能够看见,在新接口的处理逻辑中,仍是会调用dispatchNestedPreFling与dispatchNestedFling方法。也就是以前的处理fling方式是没有被替代的,可是这并不说明没有变化。咱们发现子控件调用了startNestedScroll方法,并设置了当前类型为TYPE_NON_TOUCH(fling),那么也就是说,在实现了NestedScrollingParent2的父控件中,咱们能够在onStartNestedScroll方法中知道当前的滑动类型究竟是fling,仍是手势滑动。咱们继续查看doFling方法。伪代码以下:

/** * 实际的fling处理效果 */
    private void doFling(int velocityX, int velocityY) {
        mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        postInvalidate();
    }

复制代码

doFling方法其实很简单,就是调用OverScroller的fing方法,并调用postInvalidate方法(为了帮助你们理解,这里并无采用 postOnAnimation()的方式)。其中OverScroller的fing方法主要是根据当前传入的速度,计算出在匀减速状况下,实际运动的距离。这里也就解释了为何,在只有速度的状况下,子控件能够将fling传递给父控件,由于速度最后变成了实际的运动距离。

这里就不对Scroller的fling方法中如何将速度转换成距离的算法进行讲解了。不熟悉的小伙伴能够自行谷歌或百度。

熟悉Scroller的小伙伴必定知道,为了获取到fling所产生的距离,咱们须要调用postInvalidate()方法或Invalidate()方法。同时在子控件的computeScroll()方法中获取实际的运动距离。那么也就说最终的子控件的fing的分发实际是在computeScroll()方法中。继续查看该方法的伪代码:

public void computeScroll() {
       if (mScroller.computeScrollOffset()) {
           int x = mScroller.getCurrX();
           int y = mScroller.getCurrY();
           int dx = x - mLastFlingX;
           int dy = y - mLastFlingY;

           mLastFlingX = x;
           mLastFlingY = y;
           //在子控件处理fling以前,先判断父控件是否消耗
           if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, null, TYPE_NON_TOUCH)) {
               //计算父控件消耗后,剩下的距离
               dx -= mScrollConsumed[0];
               dy -= mScrollConsumed[1];

               //由于以前默认向父控件传递的竖直方向,因此这里子控件也消耗剩下的竖直方向
               int hResult = 0;
               int vResult = 0;
               int leaveDx = 0;//子控件水平fling 消耗的距离
               int leaveDy = 0;//父控件竖直fling 消耗的距离

               if (dx != 0) {
                   leaveDx = childFlingX(dx);
                   hResult = dx - leaveDx;//获得子控件消耗后剩下的水平距离
               }
               if (dy != 0) {
                   leaveDy = childFlingY(dy);//获得子控件消耗后剩下的竖直距离
                   vResult = dy - leaveDy;
               }

               dispatchNestedScroll(leaveDx, leaveDy, hResult, vResult, null, TYPE_NON_TOUCH);

           }
       } else {
           //当fling 结束时,通知父控件
           stopNestedScroll(TYPE_NON_TOUCH);

       }
   }

复制代码

观察代码,咱们能够发现,子控件中分发fling的方式在与以前分发手势滚动的逻辑很是一致。

  • 产生fing时,调用带type(TYPE_NON_TOUCH)参数的dispatchNestedPreScroll方法,判断父控件是否处理fling事件。
  • 若是父控件处理,那么父控件消耗后,子控件再消耗剩余的距离
  • 子控件消耗后,若是还有剩余的距离,则调用带type(TYPE_NON_TOUCH)参数的dispatchNestedScroll方法,将剩下的距离传递给父控件。
  • 当子控件fling结束时,则调用stopNestedScroll(TYPE_NON_TOUCH)方法,通知父控件fling已经结束。

那么也就是说,NestedScrollingChild2与NestedScrollingParent2接口,只是在原有的方法中增长了TYPE_NON_TOUCH参数来让父控件区分究竟是手势滑动仍是fling。不得不佩服谷歌大佬的设计。不只兼容还解决了实际的问题。

总结

经过上文的分析,咱们能获得以下结论:

  • NestedScrolling(嵌套滑动)机制是创建在原有的事件机制之上,要实现嵌套滑动,父控件是不能拦截事件。
  • NestedScrolling(嵌套滑动)机制中接口要成对使用。如NestedScrollingChild2与NestedScrollingParent2成对。NestedScrollingChild与NestedScrollingParent成对。
  • 当咱们须要子控件分发fling给父控件时,咱们须要使用NestedScrollingChild2与NestedScrollingParent2。并在相应的方法中经过type(TYPE_TOUCH(0)TYPE_NON_TOUCH(1)),来判断是手势滑动仍是fling。

最后

到如今整个NestedScrolling(嵌套滑动)机制就讲解完毕了,在接下来的文章中,会讲解相应嵌套滑动例子、CoordinatorLayout与Behavior、自定义Behavior等相关知识点,若是你们有兴趣的话,能够持续关注~。谢谢你们花时间阅读文章啦。Thanks

相关文章
相关标签/搜索