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

    原文作者:菜鸟的姿态
    原文地址: https://www.jianshu.com/p/f6ae2638e406
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞