Android在发布 5.0(Lollipop)版本之后,Google为我们提供了嵌套滑动的特性。下面,我们从源码角度去分析Android嵌套滑动的实现机制。

首先,我们先来看一下以下嵌套滑动相关的4个核心类的实现:

NestedScrollingChild

NestedScrollingChildHelper

NestedScrollingParent

NestedScrollingParentHelper


NestedScrollingChild

package android.support.v4.view;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.ViewConfiguration;import android.view.ViewParent;public interface NestedScrollingChild {    /**     * 设置是否允许嵌套滑动     *     * @param enabled     */    public void setNestedScrollingEnabled(boolean enabled);    /**     * 判断是否允许嵌套滑动     *      * @return true if nested scrolling is enabled     */    public boolean isNestedScrollingEnabled();    /**     * 开始嵌套滑动     *     * @param axes 表示滑动方向(3个可能值)     *             ViewCompat#SCROLL_AXIS_HORIZONTAL     *             ViewCompat#SCROLL_AXIS_VERTICAL     *             ViewCompat#SCROLL_AXIS_HORIZONTAL | ViewCompat#SCROLL_AXIS_VERTICAL     * @return true if a cooperative parent was found and nested scrolling has been enabled for the current gesture.     */    public boolean startNestedScroll(int axes);    /**     * 停止嵌套滑动     */    public void stopNestedScroll();    /**     * 判断是否有父类支持嵌套滑动     */    public boolean hasNestedScrollingParent();   /**     * ChildView执行完scroll后调用,通知ParentView其实际的滑动距离和未消耗的滑动距离     *     * @param dxConsumed ChildView在x轴实际滑动的距离     * @param dyConsumed ChildView在y轴实际滑动的距离     * @param dxUnconsumed ChildView在x轴未消耗的距离(一般ParentView根据此数据处理自身的x轴滑动)     * @param dyUnconsumed ChildView在y轴未消耗的距离(一般ParentView根据此数据处理自身的y轴滑动)     * @param offsetInWindow ChildView的窗体偏移量     */    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);   /**     * ChildView执行scroll前,用于通知ParentView意图滑动的距离(onInterceptTouchEvent或者onTouch中调用)     *     * @param dx ChildView在x轴上意图滑动的距离     * @param dy ChildView在y轴上意图滑动的距离     * @param consumed 输出参数,记录ParentView消费的滑动距离(x轴:consumed[0],y轴:consumed[1])     * @param offsetInWindow ChildView的窗体偏移量     */    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);   /**     * ChildView进行fling滑动时调用,通知ParentView     *     * @param velocityX ChildView在x轴的fling速率     * @param velocityY ChildView在y轴的fling速率     * @param consumed ParentView是否消费本次fling操作     * @return true if the nested scrolling parent consumed or otherwise reacted to the fling     */    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);       /**     * ChildView进行fling操作前调用,通知ParentView其fling的速率     *     * @param velocityX ChildView在x轴的fling速率     * @param velocityY ChildView在y轴的fling速率     * @return true if a nested scrolling parent consumed the fling     */    public boolean dispatchNestedPreFling(float velocityX, float velocityY);}

以上NestedScrollingChild接口的方法描述已经比较详细,实现嵌套滑动时,ChildView必须实现该接口。通过查看RecycleView源码,我们可以知道NestedScrollingChild的接口实现完全由NestedScrollingChildHelper代理实现。因此,我们接下来分析以下NestedScrollingChildHelper的代码实现!


NestedScrollingChildHelper

package android.support.v4.view;import android.view.View;import android.view.ViewParent;    public class NestedScrollingChildHelper {    /**     * Helper对应的ChildView     */    private final View mView;        /**     * A nested scrolling parent view currently receiving events for a nested scroll in progress.     */    private ViewParent mNestedScrollingParent;        /**     * 是否允许嵌套滑动     */     private boolean mIsNestedScrollingEnabled;       /**     * 记录上次scroll event中ParentView消耗的滑动距离     */    private int[] mTempNestedScrollConsumed;    public NestedScrollingChildHelper(View view) {        mView = view;    }    /**     * 设置是否允许嵌套滑动     *     * @param enabled true to enable nested scrolling dispatch from this view, false otherwise     */    public void setNestedScrollingEnabled(boolean enabled) {        if (mIsNestedScrollingEnabled) {            ViewCompat.stopNestedScroll(mView);        }        mIsNestedScrollingEnabled = enabled;    }    /**     * 判断是否允许嵌套滑动     *     * @return true if nested scrolling is enabled for this view     */    public boolean isNestedScrollingEnabled() {        return mIsNestedScrollingEnabled;    }    /**     * 判断ChildView此时是否存在一个接收嵌套滑动事件的ParentView     *     * @return true if this view has a nested scrolling parent, false otherwise     */    public boolean hasNestedScrollingParent() {        return mNestedScrollingParent != null;    }    /**     * ChildView接收到滑动请求时调用     *(查找符合要求的Nested Scrolling ParentView)     *     * @param axes ChildView的滑动方向     * @return true if a cooperating parent view was found and nested scrolling started successfully     */    public boolean startNestedScroll(int axes) {        if (hasNestedScrollingParent()) {            // 目前处于嵌套滑动处理阶段            return true;        }        if (isNestedScrollingEnabled()) {            // 接受嵌套滑动的ParentView            ViewParent p = mView.getParent();            // ParentView的直接子控件:ChildView本身或包含ChildView            View child = mView;            while (p != null) {                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {                    mNestedScrollingParent = p;                    // 找到符合要求的Nested Scrolling ParentView时,通知ParentView准备                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);                    return true;                }                if (p instanceof View) {                    child = (View) p;                }                p = p.getParent();            }        }        return false;    }    /**     * 停止当前的嵌套滑动     */    public void stopNestedScroll() {        if (mNestedScrollingParent != null) {            // 通知ParentView停止嵌套滑动            ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);            mNestedScrollingParent = null;        }    }    /**     * ChildView执行完scroll操作后,通知ParentView     *(参考NestedScrollingChild#dispatchNestedScroll函数的注释说明)     */    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];                }               // 通知ParentView,ChildView的实际滑动情况(ParentView一般根据dxUnconsumed| dyUnconsumed处理自身滑动)                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;    }    /**     * ChildView执行scroll前,通知Parent其意图滑动的距离     *(参考NestedScrollingChild#dispatchNestedPreScroll函数注释说明)     */    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;    }    /**     * ChildView执行fling滑动操作后,通知ParentView     * (参考NestedScrollingChild#dispatchNestedFling函数)     */    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {            return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,                    velocityY, consumed);        }        return false;    }    /**     * ChildView执行fling滑动操作前,通知ParentView,由ParentView确定是否消费fling滑动事件     * (参考NestedScrollingChild#dispatchNestedPreFling函数)     *        * @return arentView是否消费fling滑动事件     */    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {            return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,                    velocityY);        }        return false;    }    /**     * 在ChildView的onDetachedFromWindow回调函数中调用,结束嵌套滑动     */    public void onDetachedFromWindow() {        ViewCompat.stopNestedScroll(mView);    }    /**     * 停止嵌套滑动     *(nested scroll ChildView停止本次嵌套滑动时,处理ChildView自身的状态)     */    public void onStopNestedScroll(View child) {        ViewCompat.stopNestedScroll(mView);    }}

NestedScrollingChildHelper可以说是嵌套滑动机制中最重要的类,ChildView实现NestedScrollingChild接口时,所有NestedScrollingChild的函数实现都交给NestedScrollingChildHelper代理实现,包括:查找接受嵌套滑动的ParentView,通知ParentView嵌套滑动的滑动距离、方向,通知ParentView结束嵌套滑动等。


NestedScrollingParent

package android.support.v4.view;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.ViewConfiguration;/** * 实现嵌套滑动的ParentView必须实现该接口 */public interface NestedScrollingParent {    /**     * ParentView判断是否进行嵌套滑动     *     * @param child  ParentView的直接子控件(包含target)     * @param target 嵌套滑动的ChildView     * @param nestedScrollAxes ChildView的嵌套滑动方向     * @return true if this ViewParent accepts the nested scroll operation     */    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);    /**     * 当onStartNestedScroll返回true时调用     *(一般直接调用NestedScrollingParentHelper#onNestedScrollAccepted,记录嵌套滑动方向)     */    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);    /**     * ParentView结束嵌套滑动     *(一般直接调用NestedScrollingParentHelper#onStopNestedScroll,重置嵌套滑动方向flag为0)     */    public void onStopNestedScroll(View target);    /**     * 接收ChildView的滑动通知(ChildView scroll后)     *(当ChildView调用dispatchNestedScroll函数时间接调用,一般ParentView在函数中实现滑动操作)     */    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,            int dxUnconsumed, int dyUnconsumed);    /**     * 接收ChildView的滑动通知(ChildView scroll前)     *(当ChildView调用 dispatchNestedPreScroll函数时间接调用,由ParentView决定自身消耗的滑动距离)     */    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);    /**     * 类似onNestedScroll,只是针对fling操作     */    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);    /**     * 类似onNestedPreScroll,只是针对fling操作     */    public boolean onNestedPreFling(View target, float velocityX, float velocityY);    /**     * 返回嵌套滑动的方向     * (直接调用NestedScrollingParentHelper#getNestedScrollAxes)     */    public int getNestedScrollAxes();}

实现嵌套滑动的ParentView必须实现NestedScrollingParent接口类,用于判断是否接受嵌套滑动、计算ParentView消耗的滑动距离以及接收到ChildView的滑动通知后,ParentView本身进行滑动等逻辑。以上函数都不是ParentView自身直接调用,而是由ChildView实现NestedScrollingChild接口的方法间接调用!


NestedScrollingParentHelper

public class NestedScrollingParentHelper {    /**     * 实现嵌套滑动的ParentView     */    private final ViewGroup mViewGroup;       /**     * 记录嵌套滑动的方向flag     */    private int mNestedScrollAxes;    public NestedScrollingParentHelper(ViewGroup viewGroup) {        mViewGroup = viewGroup;    }    /**     * 返回嵌套滑动的方向flag     */    public int getNestedScrollAxes() {        return mNestedScrollAxes;    }    /**     * 当ParentView接收嵌套滑动操作时调用     * (在NestedScrollingParent#onNestedScrollAccepted接口实现中调用)     */    public void onNestedScrollAccepted(View child, View target, int axes) {        // 记录嵌套滑动方向         mNestedScrollAxes = axes;    }        /**     * 当嵌套滑动结束时调用     * (在NestedScrollingParent#onStopNestedScroll接口实现中调用)     */    public void onStopNestedScroll(View target) {        // 重置嵌套滑动方向        mNestedScrollAxes = 0;    }}

以上NestedScrollingParentHelper的实现比较简单,基本都是在NestedScrollingParent的对应接口调用,只是在ParentView接受嵌套滑动和结束嵌套滑动时,改变记录嵌套滑动方向flag的值mNestedScrollAxes。


建议熟悉以上4个类后,查看一下RecycleView中NestedScrollingChild接口的实现,以及调用NestedScrollingChild接口的时机;查看一下NestedScrollView中NestedScrollingParent接口的实现,即可对嵌套滑动机制大致掌握!

下一篇,我将通过简单分析RecycleView和NestedScrollView源码,以及实现简单的demo,帮助大家更深刻地了解嵌套滑动机制!

总结一下,嵌套滑动的工作流程如下:

(1)ChildView接收到TouchEvent(down类型)后,先查找符合嵌套滑动的ParentView :

NestedScrollingChild#startNestedScroll(int axes) -》 NestedScrollingChildHelper#startNestedScroll(int axes) -》递归查找符合嵌套滑动要求的ParentView(即NestedScrollingParent#startNestedScroll(int axes)返回true的父控件)

(2)ChildView(实现NestedScrollingChild接口)接收到滑动请求时,先通知ParentView(实现NestedScrollingParent接口)其意图滑动的方向nestedScrollAxes和距离dx & dy:

// consumed作为输出参数,获取ParentView的消耗滑动距离NestedScrollingChild#dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)

(3)ParentView可以根据自定义规则,计算需要消耗滑动距离int[] consumed,再通知ChildView;

//consumed作为输出参数,记录ParentView的消耗滑动距离NestedScrollingParent#onNestedPreScroll(View target, int dx, int dy, int[] consumed)

(4)ChildView根据ParentView返回的数据int[] consumed,重新计算需要滑动的距离dxConsumed & dyConsumed以及不消耗的滑动距离dxUnconsumed & dyUnconsumed;

(5)ChildView根据步骤4计算得到实际的滑动距离后,先自身调用scroll函数进行滑动,然后通知ParentView其滑动情况;

NestedScrollingChild#dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

(6)ParentView接收到ChildView的实际滑动情况时,可以根据dxUnconsumed & dyUnconsumed,确定ParentView自身的滑动。

NestedScrollingParent#onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);

注意:

  • 仅当步骤(1)中找到接受嵌套滑动的parentView,才会进行后面的步骤;

  • 以上步骤(2)~(6)为循环步骤,在同一系列的Touch Events事件流中,每一个move类型的Touch Event都会触发一次(2)~(6)的组合步骤,从而可以根据手指在屏幕的触摸动态处理ChildView和ParentView的滑动!

  • 直至,ChildView调用NestedScrollingChild#stopNestedPreScroll函数(一般为接收到类型up或cancel的Touch Event时调用),则嵌套滑动结束。


参考链接:
NestedScrolling事件机制源码解析

更多相关文章

  1. Android(安卓)一个自定义View需要实现哪些方法
  2. Lock-free atomic operations in Android
  3. Android(安卓)调用相册 拍照 实现系统控件缩放 切割图片
  4. android客户端通过Get方式提交参数给服务器,使用URL和HttpURLConn
  5. Android(安卓)Bander设计与实现 - 设计篇
  6. android手势翻页效果
  7. Android实现模拟时钟(简单+漂亮)--时针、分针、秒针
  8. Android应用最上层悬浮窗实现不依赖于Activity的弹框
  9. 浅谈Java中Collections.sort对List排序的两种方法

随机推荐

  1. android第一天(小有成就,散分)
  2. Android Marekt 上下平板 Android Market
  3. 将已有的Android从Eclipse手动迁移到Andr
  4. 最近Android真的凉凉了?
  5. Android 的消息机制之 Handler
  6. Android(安卓)仿微信聊天图片
  7. Android--自定义视图控件(一)(Android St
  8. Android编译系统中头文件搜索路径顺序的
  9. 往android的内核添加驱动(其实就是添加lin
  10. Android桌面小部件AppWidget:音乐播放器桌