Android嵌套滑动机制源码分析

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事件机制源码解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容