Android 嵌套滑动分析

       嵌套滑动具体点就是当子View有滑动的时候把滑动的情况告诉父View,让父View也可以做一些相应的滑动动作。父View做完动作之后又把滑动的情况告诉子View。可以看出子View是主动的一方,父View是被动的一方(父View也可能是父父View这里为了方便一点就直接说的是父View)。

分两部分来介绍嵌套滑动。一个是简单的从源码的层次分析嵌套滑动,一个是具体的实例。

源码层次分析嵌套滑动

要分析嵌套滑动的具体实现得先知道嵌套滑动四个相关的类NestedScrollingChild,NestedScrollingChildHelper,NestedScrollingParent,NestedScrollingParentHelper。

NestedScrollingChild:在子View中要实现的接口。NestedScrollingChild函数的相关解释。

public interface NestedScrollingChild {
    // 设置是否允许嵌套滑动
    public void setNestedScrollingEnabled(boolean enabled);

    // 是否允许嵌套滑动
    public boolean isNestedScrollingEnabled();

    /** 告诉开始嵌套滑动流程,调用这个函数的时候会去找嵌套滑动的父控件。如果找到了父控件并且父控件说可以滑动就返回true,否则返回false
     * (一般ACTION_DOWN里面调用)
     * @param axes:支持嵌套滚动轴。水平方向,垂直方向,或者不指定
     * @return true 父控件说可以滑动,false 父控件说不可以滑动
     */
    public boolean startNestedScroll(int axes);

    // 停止嵌套滑动流程(一般ACTION_UP里面调用)
    public void stopNestedScroll();

    // 是否有嵌套滑动对应的父控件
    public boolean hasNestedScrollingParent();

    /**
     * 在嵌套滑动的子View滑动之后再调用该函数向父View汇报滑动情况。
     * @param dxConsumed 子View水平方向滑动的距离
     * @param dyConsumed 子View垂直方向滑动的距离
     * @param dxUnconsumed 子View水平方向没有滑动的距离
     * @param dyUnconsumed 子View垂直方向没有滑动的距离
     * @param offsetInWindow 出参 如果父View滑动导致子View的窗口发生了变化(子View的位置发生了变化)
     *                       该参数返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的变化
     *                       如果你记录了手指最后的位置,需要根据参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
     * @return true 如果父View有滑动做了相应的处理, false 父View没有滑动.
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    /**
     * 在嵌套滑动的子View滑动之前,告诉父View滑动的距离,让父View做相应的处理。
     * @param dx 告诉父View水平方向需要滑动的距离
     * @param dy 告诉父View垂直方向需要滑动的距离
     * @param consumed 出参. 如果不是null, 则告诉子View父View滑动的情况, consumed[0]父View告诉子View水平方向滑动的距离(dx)
     *                 consumed[1]父View告诉子View垂直方向滑动的距离(dy).
     * @param offsetInWindow 可选 length=2的数组,如果父View滑动导致子View的窗口发生了变化(子View的位置发生了变化)
     *                       该参数返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的变化
     *                       如果你记录了手指最后的位置,需要根据参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
     * @return true 父View滑动了,false 父View没有滑动。
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    /**
     * 在嵌套滑动的子View fling之后再调用该函数向父View汇报fling情况。
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @param consumed true 如果子View fling了, false 如果子View没有fling
     * @return true 如果父View fling了
     */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
     * 在嵌套滑动的子View fling之前告诉父View fling的情况。
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @return 如果父View fling了
     */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

NestedScrollingChildHelper:子View嵌套滑动的帮助类,在子View实现的接口NestedScrollingChild里面简单掉下NestedScrollingChildHelper里面的对应的方法就好了,用于告诉嵌套滑动对应的父View相关的嵌套事件。

public class NestedScrollingChildHelper {
    private final View mView;
    private ViewParent mNestedScrollingParent;
    private boolean mIsNestedScrollingEnabled;
    private int[] mTempNestedScrollConsumed;

    /**
     * 通过给定的子View构造NestedScrollingChildHelper
     */
    public NestedScrollingChildHelper(View view) {
        mView = view;
    }

    /**
     * 设置是否允许嵌套滑动(如果当前View已经设置了嵌套滑动则会调用到当前View的stopNestedScroll)
     * @param enabled true 是否允许嵌套滑动
     */
    public void setNestedScrollingEnabled(boolean enabled) {
        if (mIsNestedScrollingEnabled) {
            ViewCompat.stopNestedScroll(mView);
        }
        mIsNestedScrollingEnabled = enabled;
    }

    /**
     * 检查是否允许嵌套滑动
     */
    public boolean isNestedScrollingEnabled() {
        return mIsNestedScrollingEnabled;
    }

    /**
     * 检查是否有嵌套滑动对于的父View
     */
    public boolean hasNestedScrollingParent() {
        return mNestedScrollingParent != null;
    }

    /**
     * 告诉父View准备开始嵌套滑动(这个函数做的事情就是去找嵌套滑动对应父View并且判断该父View是否接收嵌套滑动的事件)
     * 讲道理的话该函数会调用到嵌套对应的父View的onStartNestedScroll 和 onNestedScrollAccepted函数
     * @param axes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
     * @return true 找到了嵌套滑动的父View,并且父View会接受嵌套滑动事件。
     */
    public boolean startNestedScroll(int axes) {
        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;
    }

    /**
     * 告诉嵌套滑动对于的父View嵌套滑动结束
     * 讲道理的话该函数会调用到嵌套对应的父View的onStopNestedScroll函数
     */
    public void stopNestedScroll() {
        if (mNestedScrollingParent != null) {
            ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
            mNestedScrollingParent = null;
        }
    }

    /**
     * 在子View处理了滑动动作之后告诉嵌套滑动对于的父View子View的滑动情况。参数和NestedScrollingChild里面的对应。
     * 讲道理的话该函数会调用到嵌套对应的父View的onNestedScroll函数
     * 这里注意下offsetInWindow这个参数是个出参 是子View位置的变化值。这个参数的变化值,不用我们在嵌套滑动的父View里面去设置,在这个函数里面已经设置了。
     * @return true 如果嵌套滑动对应的父View有滑动
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            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];
                }

                ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                                                dyConsumed, dxUnconsumed, dyUnconsumed);

                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;
    }

    /**
     * 在子View处理滑动事件之前,告诉嵌套滑动对应的父View滑动的情况。
     * 参数的意思和NestedScrollingChild里面的对应
     * 讲道理的话该函数会调用到嵌套对应的父View的onNestedPreScroll函数
     * 同时也可以看到offsetInWindow不用我们在父View里面去设置,但是consumed这个参数是要我们在父View里面去设置的。
     * @return true 嵌套滑动对应的父View有滑动。
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            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;
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                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;
    }

    /**
     * 子View fling之后把fling的情况报告给嵌套滑动对应的父View
     * 讲道理的话该函数会调用到嵌套对应的父View的onNestedFling函数
     * @return true 嵌套滑动对于的父View fling了
     */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
                                                  velocityY, consumed);
        }
        return false;
    }

    /**
     * 子View fling之前 告诉嵌套滑动对应的父View fling的情况
     * 讲道理的话该函数会调用到嵌套对应的父View的onNestedPreFling函数
     * @return true 嵌套滑动对应的父View消耗了fling事件
     */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
                                                     velocityY);
        }
        return false;
    }

    /**
     * 当子View脱离窗口的时候调用该函数告知停止嵌套滑动
     * 该函数会调用到当前View(嵌套滑动的子View)的stopNestedScroll函数
     */
    public void onDetachedFromWindow() {
        ViewCompat.stopNestedScroll(mView);
    }

    /**
     * 告知停止嵌套滑动
     * 该函数会调用到当前View(嵌套滑动的子View)的stopNestedScroll函数
     */
    public void onStopNestedScroll(View child) {
        ViewCompat.stopNestedScroll(mView);
    }
}

这里NestedScrollingChildHelper里面的函数是怎么和嵌套滑动的父View建立联系的。每个函数的处理方式基本上都是通过ViewCompat的帮助来实现的。
第45行,NestedScrollingChildHelper类的startNestedScroll函数,该函数是用于告诉嵌套滑动对应的父View嵌套滑动要开始了,函数里面有个while循环一级一级的遍历父View去找嵌套滑动对应的父View。while前面都是一些判断性的语句直接看54行 ViewParentCompat.onStartNestedScroll(p, child, mView, axes),该函数的实现在ViewParentCompat的内部类ViewParentCompatStubImpl里面具体代码如下

    @Override
    public boolean onStartNestedScroll(ViewParent parent, View child, View target,
                                       int nestedScrollAxes) {
        if (parent instanceof NestedScrollingParent) {
            return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                                                                        nestedScrollAxes);
        }
        return false;
    }

就是判断panrent是否实现了NestedScrollingParent接口(所以说父View一定要实现NestedScrollingParent接口)。调用parent的onStartNestedScroll方法,到这里处理逻辑就到parent里面去了,给parent自己处理了。继续看NestedScrollingChildHelper的startNestedScroll函数第56行ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);通过这句的调用也是直接调用了parent的onNestedScrollAccepted方法。到此startNestedScroll函数分析结束。
这里可以看出NestedScrollingChildHelper类的startNestedScroll函数会调用到parent的onStartNestedScroll和onNestedScrollAccepted方法。

第85行 NestedScrollingChildHelper类的dispatchNestedScroll和其他的函数处理逻辑是一样的调用到parent的onNestedPreScroll函数,并且这里我们也可以看出onNestedScroll每个参数的意义。同时在这里也可以看到dispatchNestedScroll的最后一个参数这里已经帮我们算好了,里面放的是子View位置的变化。

第122行 NestedScrollingChildHelper类的dispatchNestedPreScroll函数调用了parent的onNestedPreScroll函数。看到dispatchNestedPreScroll的倒数第一个参数offsetInWindow已经帮我们处理好了,但是倒数第二个参数consumed却没有帮我们处理,我们要在parent的onNestedPreScroll处理赋值。

NestedScrollingChildHelper的其他的函数也基本上是这样的流程 都是要不调用到嵌套滑动对于的父View里面的函数(NestedScrollingParent里面的某个函数),要不就是调用到子View里面的函数(NestedScrollingChild里面的某个函数)

NestedScrollingParent:父View要实现的接口。相关函数的意义如下

public interface NestedScrollingParent {
    /**
     * 有嵌套滑动到来了,问下该父View是否接受嵌套滑动
     * @param child 嵌套滑动对应的父类的子类(因为嵌套滑动对于的父View不一定是一级就能找到的,可能挑了两级父View的父View,child的辈分>=target)
     * @param target 具体嵌套滑动的那个子类
     * @param nestedScrollAxes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
     * @return 是否接受该嵌套滑动
     */
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    /**
     * 该父View接受了嵌套滑动的请求该函数调用。onStartNestedScroll返回true该函数会被调用。
     * 参数和onStartNestedScroll一样
     */
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    /**
     * 停止嵌套滑动
     * @param target 具体嵌套滑动的那个子类
     */
    public void onStopNestedScroll(View target);

    /**
     * 嵌套滑动的子View在滑动之后报告过来的滑动情况
     *
     * @param target 具体嵌套滑动的那个子类
     * @param dxConsumed 水平方向嵌套滑动的子View滑动的距离(消耗的距离)
     * @param dyConsumed 垂直方向嵌套滑动的子View滑动的距离(消耗的距离)
     * @param dxUnconsumed 水平方向嵌套滑动的子View未滑动的距离(未消耗的距离)
     * @param dyUnconsumed 垂直方向嵌套滑动的子View未滑动的距离(未消耗的距离)
     */
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                               int dxUnconsumed, int dyUnconsumed);

    /**
     * 在嵌套滑动的子View未滑动之前告诉过来的准备滑动的情况
     * @param target 具体嵌套滑动的那个子类
     * @param dx 水平方向嵌套滑动的子View想要变化的距离
     * @param dy 垂直方向嵌套滑动的子View想要变化的距离
     * @param consumed 这个参数要我们在实现这个函数的时候指定,回头告诉子View当前父View消耗的距离 
     *                    consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子view做出相应的调整
     */
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    /**
     * 嵌套滑动的子View在fling之后报告过来的fling情况
     * @param target 具体嵌套滑动的那个子类
     * @param velocityX 水平方向速度
     * @param velocityY 垂直方向速度
     * @param consumed 子view是否fling了
     * @return true 父View是否消耗了fling
     */
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    /**
     * 在嵌套滑动的子View未fling之前告诉过来的准备fling的情况
     * @param target 具体嵌套滑动的那个子类
     * @param velocityX 水平方向速度
     * @param velocityY 垂直方向速度
     * @return true 父View是否消耗了fling
     */
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    /**
     * 获取嵌套滑动的轴
     * @see ViewCompat#SCROLL_AXIS_HORIZONTAL 垂直
     * @see ViewCompat#SCROLL_AXIS_VERTICAL 水平
     * @see ViewCompat#SCROLL_AXIS_NONE 都支持
     */
    public int getNestedScrollAxes();
}

NestedScrollingParentHelper:父View嵌套滑动的帮助类,这个类就简单了

public class NestedScrollingParentHelper {

    private final ViewGroup mViewGroup;
    private       int       mNestedScrollAxes;

    public NestedScrollingParentHelper(ViewGroup viewGroup) {
        mViewGroup = viewGroup;
    }

    public void onNestedScrollAccepted(View child, View target, int axes) {
        mNestedScrollAxes = axes;
    }

    public int getNestedScrollAxes() {
        return mNestedScrollAxes;
    }

    public void onStopNestedScroll(View target) {
        mNestedScrollAxes = 0;
    }
}

四个重要的类NestedScrollingChild,NestedScrollingChildHelper,NestedScrollingParent,NestedScrollingParentHelper。大概的知道了每个函数的意思。从上面的分析也可以看出子View是主动的一方,把相关的滑动信息通过NestedScrollingChildHelper告诉父View让父View做相应的处理。接下来就是该怎么使用了。

嵌套滑动使用实例

嵌套滑动具体使用。注意只能子View通知父View,父View里面的嵌套滑动都是被动调用的。

嵌套滑动子View 一般使用方法(用scroll来举例)
1. 实现NestedScrollingChild接口。
2. 定义NestedScrollingChildHelper变量。
3. 在实现的NestedScrollingChild每个接口中调用。NestedScrollingChildHelper对应的函数。
4. setNestedScrollingEnabled(true); 一般在初始化里面调用设置可以嵌套滑动。
5. onTouchEvent 或者 dispatchTouchEvent 方法里面case ACTION_DOWN 调用startNestedScroll函数 告诉父View开始嵌套滑动。
6. onTouchEvent 或者 dispatchTouchEvent 方法里面case ACTION_MOVE 调用dispatchNestedPreScroll或者dispatchNestedScroll 这个就视情况而定了告诉父View滑动的情况。
7. onTouchEvent 或者 dispatchTouchEvent 方法里面case ACTION_UP 调用stopNestedScroll 告诉父View结束嵌套滑动。
8. 重写onDetachedFromWindow方法,调用NestedScrollingChildHelper的onDetachedFromWindow方法

嵌套滑动父View 一般使用方法
1. 实现NestedScrollingParent接口。
2. 定义NestedScrollingParentHelper变量。
3. 在实现的NestedScrollingParent几个接口中(onNestedScrollAccepted, onStopNestedScroll, getNestedScrollAxes)调用NestedScrollingParentHelper对应的函数。
4. 视情况而定onNestedScroll onNestedPreScroll onNestedFling onNestedPreFling 做相应的处理。

给出一个具体demo,当子View滑动的时候碰到了父View边缘的时候让父View也一起滑动。例子的代码来源http://blog.csdn.net/zty19901005/article/details/50974388

demo下载地址

:上面借助NestedScrollingChild NestedScrollingChildHelper NestedScrollingParent NestedScrollingParentHelper这四个类只能做到子View通知父View。但有的时候仅仅只是通知到父View还满足不了我们的需求。最好有一种方法通知到了父View然后在通知到兄弟View。这个时候就该是Behavior出场了。借助Behavior可以帮助父View通知到兄弟View。比如 CoordinatorLayout + AppBarLayout + Toolbar + RecyclerView 实现RecyclerView上滑隐藏Toolbar下滑显示Toolbar的效果
要实现这个效果分两步
1. RecyclerView 通知到 CoordinatorLayout :RecyclerView肯定实现了NestedScrollingChild,CoordinatorLayout实现了NestedScrollingParent。
2. CoordinatorLayout 通知到 AppBarLayout :AppBarLayout 里面肯定有一个Behavior。CoordinatorLayout 在onNestedPreScroll函数里面会去找CoordinatorLayout子View 是否有Behavior。然后把情况告诉Behavior。
关于Behavior的简单实现以后再分析。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值